Arbeiten Sie mit abstrakten JavaScript-Syntaxbäumen

Warum Ihren Code analysieren? Zum Beispiel, um das vergessene console.log vor dem Festschreiben zu finden. Was aber, wenn Sie die Signatur der Funktion in Hunderten von Einträgen im Code ändern müssen? Werden reguläre Ausdrücke hier zurechtkommen? Dieser Artikel zeigt Ihnen, welche Möglichkeiten abstrakte Syntaxbäume einem Entwickler bieten.



Under the cut - ein Video- und Textprotokoll eines Berichts von Kirill Cherkashin ( z6Dabrata ) von der HolyJS 2018 Piter- Konferenz.


Über den Autor
Cyril wurde in Moskau geboren, lebt heute in New York und arbeitet bei Firebase. Unterrichtet Angular nicht nur bei Google, sondern auf der ganzen Welt. Der Organisator des größten Angular Mitap der Welt ist AngularNYC (sowie VueNYC und ReactNYC). In seiner Freizeit vom Programmieren liebt er Tango, Bücher und angenehme Gespräche.

Bügelsäge oder Holz?


Beginnen wir mit einem Beispiel: Nehmen wir an, Sie haben ein Programm debuggt und die an git vorgenommenen Änderungen gesendet. Danach sind Sie leise ins Bett gegangen. Am Morgen stellte sich heraus, dass Ihre Kollegen Ihre Änderungen heruntergeladen haben. Da Sie am Vortag vergessen haben, die Ausgabe der Debugging-Informationen auf der Konsole zu entfernen, werden sie angezeigt und die Ausgabe verstopft. Viele standen vor diesem Problem.

Es gibt Tools wie EsLint , um die Situation zu beheben. Versuchen wir jedoch zu Bildungszwecken, selbst eine Lösung zu finden.
Mit welchem ​​Tool soll ich alle console.log() aus dem Code entfernen?
Wir wählen zwischen regulären Ausdrücken und der Verwendung von abstrakten Sitax-Bäumen (ASD). Versuchen wir, dies mit regulären Ausdrücken zu lösen, indem findConsoleLog eine findConsoleLog Funktion schreiben. Bei der Eingabe erhält es den Programmcode als Argument und zeigt true an, wenn console.log () irgendwo im Programmtext gefunden wird.

 function findConsoleLog(code) { return !!code.match(/console.log/); } 

Ich habe 17 Tests geschrieben und versucht, verschiedene Möglichkeiten zu finden, um unsere Funktion zu unterbrechen. Diese Liste ist bei weitem nicht vollständig.



Der einfachste Test bestanden.
Und was ist, wenn eine Funktion die Zeichenfolge "console.log" in ihrem Namen enthält?

 function findConsoleLog(code) { return !!code.match(/\bconsole.log/); } 

Es wurde ein Zeichen hinzugefügt, das angibt, dass console.log am Wortanfang stehen soll.



Es wurden nur zwei Tests bestanden, aber was ist, wenn console.log im Kommentar enthalten ist und nicht gelöscht werden muss?

Wir schreiben es neu, damit der Parser die Kommentare nicht berührt.

 function findConsoleLog(code) { return !!code   .replace(/\/\/.*/)   .match(/\bconsole.log/); } 



Wir schließen das Entfernen von "console.log" aus den Zeilen aus:

 function findConsoleLog(code) { return !!code   .replace(/\/\/.*|'.*'/, '')   .match(/\bconsole.log/); } 



Vergessen Sie nicht, dass wir noch Leerzeichen und andere Zeichen haben, die das Bestehen einiger Tests verhindern können:



Trotz der Tatsache, dass die Idee nicht ganz einfach war, können alle 17 Tests mit regulären Ausdrücken bestanden werden. In diesem Fall sieht der Lösungscode also so aus:

 function findConsoleLog(code) { return code   .replace(/\/\/.*|'.*?[^\\]'|".*?"|`[\s\S]*`|\/\*[\s\S]*\*\//)   .match(/\bconsole\s*.log\(/); } 


Das Problem ist, dass dieser Code nicht alle möglichen Fälle abdeckt und es ziemlich schwierig ist, ihn zu pflegen.

Überlegen Sie, wie Sie dieses Problem mit ASD lösen können.

Wie wachsen Bäume?


Der abstrakte Syntaxbaum wird als Ergebnis des Parsers erhalten, der mit dem Code Ihrer Anwendung arbeitet. Der Parser @ babel / parser wurde zur Demonstration verwendet .
Nehmen Sie als Beispiel die Zeichenfolge console.log('holy') und übergeben Sie sie durch den Parser.

 import { parse } from 'babylon'; parse("console.log('holy')"); 

Als Ergebnis seiner Arbeit wird eine JSON-Datei mit etwa 300 Zeilen erhalten. Wir schließen von ihren Nummernzeilen mit Serviceinformationen aus. Wir interessieren uns für den Körperteil. Meta-Informationen interessieren uns auch nicht. Das Ergebnis sind ungefähr 100 Zeilen. Im Vergleich zu der Struktur, die der Browser für eine Body-Variable (ca. 300 Zeilen) generiert, ist dies nicht viel.

Schauen wir uns einige Beispiele an, wie verschiedene Literale in einem Syntaxbaum im Code dargestellt werden:



Dies ist ein Ausdruck, in dem es das numerische Literal gibt, ein numerisches Literal.



Der bereits bekannte Ausdruck console.log. Es hat ein Objekt, das eine Eigenschaft hat.



Wenn log ein Funktionsaufruf ist, lautet die Beschreibung wie folgt: Es gibt einen Aufrufausdruck mit Argumenten - numerische Literale. Gleichzeitig hat der aufrufende Ausdruck ein Namensprotokoll.

Literale können unterschiedlich sein: Zahlen, Zeichenfolgen, reguläre Ausdrücke, Boolesche Werte, Null.
Zurück zum Aufruf console.log



Dies ist ein Aufrufausdruck, in dem sich der Mitgliedsausdruck befindet. Daraus geht hervor, dass das darin enthaltene Konsolenobjekt eine Eigenschaft namens log hat.

ASD-Bypass


Versuchen wir nun, mit dieser Struktur im Code zu arbeiten. Die Babel-Traverse- Bibliothek wird verwendet, um den Baum zu durchlaufen .

Die gleichen 17 Tests werden gegeben. Ein solcher Code wird erhalten, indem der Syntaxbaum des Programms analysiert und nach Einträgen von "console.log" gesucht wird:

 function traverseConsoleLog(code, {babylon, babelTraverse, types, log}) { const ast = babylon.parse(code); let hasConsoleLog = false; babelTraverse(ast, {   MemberExpression(path){     if (       path.node.property.type === 'Identifier' &&       path.node.property.name === 'log' &&       path.node.object.type === 'Identifier' &&       path.node.object.name === 'console' &&       path.parent.type === 'CallExpression' &&       path.Parentkey === 'callee'     ) {       hasConsoleLog = true;     }   } }) return hasConsoleLog; } 

Lassen Sie uns analysieren, was hier geschrieben steht. const ast = babylon.parse(code); In die ast-Variable analysieren wir den Syntaxbaum aus dem Code. Als nächstes geben wir der Babel-Parse-Bibliothek diesen Baum zur Verarbeitung. Wir suchen nach Knoten und Eigenschaften mit übereinstimmenden Namen in Aufrufausdrücken. Setzen Sie die Variable hasConsoleLog auf true, wenn die erforderliche Kombination von Knoten und ihren Namen gefunden wird.

Wir können uns im Baum bewegen, die Eltern von Knoten und Nachkommen übernehmen, nach den Argumenten und Eigenschaften suchen, die Namen dieser Eigenschaften und Typen anzeigen - das ist sehr praktisch.

Es gibt eine unangenehme Nuance, die mit der Babel-Typ-Bibliothek leicht behoben werden kann. Um Fehler bei der Suche im Baum aufgrund des falschen Namens zu vermeiden, haben Sie beispielsweise anstelle von path.parent.type === 'CallExpression' versehentlich path.parent.type === 'callExpression' . Mit Babel-Typen können Sie so schreiben ::

 // Before path.node.property.type === 'Identifier' path.node.property.name === 'log' // with babel types import {isIdentifier} from 'babel-types'; isIdentifier(path.node.property, {name: log}) //         ,  ,    isIdentifier,      

Wir schreiben den vorherigen Code mit Babel-Typen neu:
 function traverseConsoleLogSolved2(code, {babylon, babelTraverse, types}) { const ast = babylon.parse(code); let hasConsoleLog = false; babelTraverse(ast, {   MemberExpression(path) {     if (       types.isIdentifier(path.node.object, { name: 'console'}) &&       types.isIdentifier(path.node.property, { name: 'log'}) &&       types.isCallExpression(path.parent) &&       path.parentKey === 'callee'     ) {       hasConsoleLog = true;     }   } }); return hasConsoleLog; } 

ASD mit Babel-Traverse transformieren


Um die Arbeitskosten zu senken, muss console.log sofort aus dem Code entfernt werden - anstelle eines Signals, dass es im Code enthalten ist.

Da wir nicht den MemberExpression selbst, sondern seinen übergeordneten entfernen müssen, hat hasConsoleLog = true; wir schreiben path.parentPath.remove(); .

Von der Funktion removeConsoleLog wir immer noch einen booleschen Wert zurück. Wir ersetzen seine Ausgabe durch den Code, der den Babel-Generator generiert, wie folgt:
hasConsoleLog => babelGenerator(ast).code

Babel-Generator empfängt den modifizierten abstrakten Syntaxbaum als Parameter, gibt ein Objekt mit der Code-Eigenschaft zurück, innerhalb dieses Objekts wird Code ohne console.log . Wenn wir eine Codezuordnung erhalten möchten, können wir übrigens die sourceMaps-Eigenschaft für dieses Objekt aufrufen.

Und wenn Sie einen Debugger finden müssen?


Dieses Mal werden wir ASTexplorer verwenden , um die Aufgabe abzuschließen. Debugger ist eine Art Debugger-Anweisungsknoten. Wir müssen nicht die gesamte Struktur betrachten, da dies eine spezielle Art von Knoten ist. Finden Sie einfach die Debugger-Anweisung. Wir werden ein Plugin für ESLint (auf ASTexplorer) schreiben.

ASTexplorer ist so konzipiert, dass Sie den Code links schreiben und rechts die fertige ASD erhalten. Sie können wählen, in welchem ​​Format Sie es empfangen möchten: JSON oder im Baumformat.



Da wir ESLint verwenden, erledigt es die ganze Arbeit, Dateien für uns zu finden und gibt uns die gewünschte Datei, damit wir die Debugger-Zeile darin finden können. Dieses Tool verwendet einen anderen ASD-Parser. Es gibt jedoch verschiedene Arten von ASD in JavaScript. Etwas, das an die Vergangenheit erinnert, als verschiedene Browser die Spezifikation auf unterschiedliche Weise implementierten. Daher implementieren wir die Debugger-Suche:

 export default function(context) { return {   DebuggerStatement(node) { // ,     console.log    path,    -  ,     path         context.report(node, 'LOL Debugger!!!'); //   ESLint ,   debugger, node     ,    ,    debugger   } } } 

Überprüfen der Arbeit eines geschriebenen Plugins:



Ebenso können Sie den Debugger aus dem Code entfernen.

Was sind sonst noch nützliche ASD


Ich persönlich verwende ASD, um die Arbeit mit Angular und anderen Front-End-Frameworks zu vereinfachen. Sie können mit einem Klick auf eine Schaltfläche eine Schnittstelle, eine Methode, einen Dekorator und alles andere importieren, erweitern, hinzufügen. Obwohl es sich in diesem Fall um Javascript handelt, hat TypeScript auch eigene ASDs. Der einzige Unterschied besteht im Unterschied zwischen den Namen der Knotentypen und der Struktur. Im selben ASTExplorer kann als Sprache TypeScript ausgewählt werden.

Also:

  • Wir haben mehr Kontrolle über den Code, einfacheres Refactoring und Codemods. Sie können beispielsweise vor dem Festschreiben eine einzige Taste drücken, um den gesamten Code gemäß den Richtlinien zu formatieren. Codemods impliziert einen automatischen Code-Abgleich gemäß der erforderlichen Version des Frameworks.
  • Weniger Streitigkeiten über das Code-Design.
  • Sie können Spielprojekte erstellen. Geben Sie dem Programmierer beispielsweise automatisch Feedback zu dem Code, den er schreibt.
  • Besseres Verständnis von JavaScript.

Einige nützliche Links für Babel


  1. Alle Babel-Transformationen verwenden diese API: Plugins und Presets .
  2. Ein Teil des Prozesses zum Hinzufügen neuer Funktionen zu ECMAScript ist das Erstellen eines Plugins für Babel. Dies ist erforderlich, damit Benutzer die neue Funktionalität testen können. Wenn Sie dem Link folgen, können Sie sehen, dass die Funktionen der ASD auf dieselbe Weise verwendet werden. Zum Beispiel logischer Zuweisungsoperator .
  3. Babel Generator verliert beim Generieren von Code die Formatierung. Dies ist teilweise gut, denn wenn dieses Tool im Entwicklungsteam verwendet wird, sieht es nach dem Generieren des Codes aus der ASD für alle gleich aus. Wenn Sie jedoch Ihre Formatierung beibehalten möchten, können Sie eines der folgenden Tools verwenden: Recast oder Babel CodeMod .
  4. Über diesen Link finden Sie eine Fülle von Informationen zu Babel Awesome Babel .
  5. Babel ist ein Open Source Projekt und ein Team von Freiwilligen arbeitet daran. Sie können helfen. Es gibt drei Möglichkeiten, dies zu tun: Finanzielle Unterstützung. Sie können die Patreon-Website unterstützen, mit der Henry Zhu, einer der wichtigsten Mitwirkenden des Babel, zusammenarbeitet und mit dem Code auf opencollective.com/babel hilft .

Bonus


Wie sonst können wir unser console.log im Code finden? Verwenden Sie Ihre IDE! Verwenden Sie das Such- und Ersetzungswerkzeug, nachdem Sie ausgewählt haben, wo nach Code gesucht werden soll.
Intellij IDEA verfügt außerdem über ein Tool zur strukturellen Suche, mit dem Sie die richtigen Stellen in Ihrem Code finden können. Übrigens wird eine ASD verwendet.

Vom 24. bis 25. November wird Kirill bei Moscow HolyJS eine Präsentation über JavaScript * LOVES * -Binärdaten halten : Wir werden auf die Ebene der Binärdaten gehen, am Beispiel von * .gif-Dateien in Binärdateien graben und uns mit Serialisierungs-Frameworks wie Protobuf oder Thrift befassen. Nach dem Bericht wird es möglich sein, mit Cyril zu sprechen und alle interessanten Themen im Diskussionsbereich zu diskutieren.

Source: https://habr.com/ru/post/de428628/


All Articles