Praktische Anwendung der AST-Baumtransformation am Beispiel von Putout

Einführung


Jeden Tag, wenn an dem Code gearbeitet wird, werden auf dem Weg zur Implementierung einer für den Benutzer nützlichen Funktion erzwungene (unvermeidliche oder einfach wünschenswerte) Änderungen am Code. Dies kann das Refactoring, das Aktualisieren einer Bibliothek oder eines Frameworks auf eine neue Hauptversion und das Aktualisieren der JavaScript-Syntax sein (was in letzter Zeit nicht ungewöhnlich ist). Selbst wenn die Bibliothek Teil eines Arbeitsprojekts ist, sind Änderungen unvermeidlich. Die meisten dieser Änderungen sind Routine. Es gibt nichts Interessantes für den Entwickler in ihnen, einerseits bringt es nichts für das Geschäft, und andererseits müssen Sie während des Aktualisierungsprozesses sehr vorsichtig sein, um Brennholz und Funktionalität nicht zu beschädigen. Wir kommen daher zu dem Schluss, dass es besser ist, eine solche Routine auf die Schultern von Programmen zu verlagern, damit diese alles selbst tun und die Person wiederum kontrolliert, ob alles richtig gemacht wurde. Dies wird im heutigen Artikel diskutiert.


AST


Für die Verarbeitung von Programmcode ist es erforderlich, ihn in eine spezielle Darstellung zu übersetzen, mit der Programme bequem arbeiten können. Eine solche Darstellung existiert, sie heißt Abstract Syntax Tree (AST).
Verwenden Sie Parser, um es zu erhalten. Der resultierende AST kann beliebig transformiert werden. Um das Ergebnis zu speichern, benötigen Sie einen Codegenerator. Lassen Sie uns jeden der Schritte genauer betrachten. Beginnen wir mit dem Parser.


Parser


Und so haben wir den Code:


a + b 

Parser sind normalerweise in zwei Teile unterteilt:


  • Lexikalische Analyse

Bricht den Code in Token auf, von denen jeder einen Teil des Codes beschreibt:


 [{ "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "+", }, { "type": "Identifier", "value": "b" }] 

  • Parsen

Erstellt einen Syntaxbaum aus Token:


 { "type": "BinaryExpression", "left": { "type": "Identifier", "name": "a" }, "operator": "+", "right": { "type": "Identifier", "name": "b" } } 

Und jetzt haben wir bereits genau diese Idee, mit der Sie programmgesteuert arbeiten können. Es sollte klargestellt werden, dass es eine große Anzahl von JavaScript Parsern gibt. Hier einige davon:


  • babel-parser - ein Parser, der babel ;
  • espree - ein Parser, der eslint ;
  • Eichel - der Parser, auf dem die beiden vorherigen basieren;
  • esprima - ein beliebter Parser, der JavaScript bis EcmaScript 2017 unterstützt;
  • cherow ist ein neuer Player unter den JavaScript-Parsern, der behauptet, der schnellste zu sein.

Es gibt einen Standard-JavaScript-Parser, der ESTree heißt und definiert, welche Knoten analysiert werden sollen.
Für eine detailliertere Analyse des Implementierungsprozesses des Parsers (sowie des Transformators und des Generators) können Sie den Super-Winzling-Compiler lesen.


Transformator


Um den AST-Baum zu transformieren, können Sie das Besuchermuster verwenden , z. B. die Bibliothek @ babel / traverse . Der folgende Code gibt die Namen aller JavaScript-Code-IDs aus der Codevariablen aus.


 import * as parser from "@babel/parser"; import traverse from "@babel/traverse"; const code = `function square(n) { return n * n; }`; const ast = parser.parse(code); traverse(ast, { Identifier(path) { console.log(path.node.name); } }); 

Generator


Sie können Code beispielsweise mit @ babel / generator auf folgende Weise generieren:


 import {parse} from '@babel/parser'; import generate from '@babel/generator'; const code = 'class Example {}'; const ast = parse(code); const output = generate(ast, code); 

Zu diesem Zeitpunkt sollte der Leser eine grundlegende Vorstellung davon haben, was zum Transformieren von JavaScript-Code erforderlich ist und mit welchen Tools er implementiert wird.


Es lohnt sich auch, ein Online-Tool wie astexplorer hinzuzufügen , das eine große Anzahl von Parsern, Transformatoren und Generatoren kombiniert.


Putout


Putout ist ein Plugin-fähiger Codetransformator . Tatsächlich ist es eine Kreuzung zwischen Eslint und Babel , die die Vorteile beider Werkzeuge kombiniert.


Wie eslint putout Problembereiche im Code eslint , aber im Gegensatz zu eslint putout ändert es das Verhalten des Codes, putout es kann alle gefundenen Fehler beheben.


Wie babel putout konvertiert es den Code, versucht ihn jedoch nur minimal zu ändern, sodass er für die Arbeit mit Code verwendet werden kann, der im Repository gespeichert ist.


Hübscher ist auch erwähnenswert, es ist ein Formatierungswerkzeug und es unterscheidet sich radikal.


Jscodeshift befindet sich nicht weit von putout , unterstützt jedoch keine Plugins, zeigt keine Fehlermeldungen an und verwendet anstelle von @ babel / types auch Ast- Typen .


Erscheinungsgeschichte


Dabei hilft mir eslint sehr mit meinen Tipps. Aber manchmal will ich mehr von ihm. Um beispielsweise den Debugger zu entfernen , korrigieren Sie test.only und löschen Sie auch nicht verwendete Variablen. Der letzte Punkt bildete die Grundlage für das putout . Während des Entwicklungsprozesses wurde klar, dass dies nicht einfach ist und viele andere Transformationen viel einfacher zu implementieren sind. Somit wuchs putout reibungslos von einer Funktion zum Plugin-System. Das Entfernen nicht verwendeter Variablen ist jetzt der schwierigste Prozess, aber dies hindert uns nicht daran, viele andere gleichermaßen nützliche Transformationen zu entwickeln und zu unterstützen.


Wie Putout im Inneren funktioniert


putout Arbeit kann in zwei Teile unterteilt werden: Motor und Plugins. Diese Architektur ermöglicht es Ihnen, sich bei der Arbeit mit der Engine nicht von Transformationen ablenken zu lassen, und bei der Arbeit an Plug-Ins konzentrieren Sie sich so weit wie möglich auf deren Zweck.


Eingebaute Plugins


putout basiert auf einem Plugin-System. Jedes Plugin repräsentiert eine Regel. Mit den integrierten Regeln können Sie Folgendes tun:


  • Suchen und löschen:


    • nicht verwendete Variablen
    • debugger
    • Rufen Sie nur test.only
    • Rufen Sie test.skip
    • Rufen Sie console.log
    • Rufen Sie process.exit
    • leere Blöcke
    • leere Muster

  • Variablendeklaration suchen und teilen:


     //  var one, two; //  var one; var two; 

  • Konvertieren Sie esm in commonjs :



  //  import one from 'one'; //  const one = require('one'); 

  • Destrukturierung anwenden:

 //  const name = user.name; //  const {name} = user; 

  1. Destruktionseigenschaften kombinieren:

 //  const {name} = user; const {password} = user; //  const { name, password } = user; 

Jedes Plugin basiert auf der Unix-Philosophie , das heißt, sie sind so einfach wie möglich, jedes führt eine Aktion aus und lässt sich leicht kombinieren, da es sich im Wesentlichen um Filter handelt.


Zum Beispiel mit folgendem Code:


 const name = user.name; const password = user.password; 

Es wird zuerst mithilfe von Apply-Destructuring konvertiert in:


 const {name} = user; const {password} = user; 

Anschließend wird mithilfe von Merge-Destructuring-Eigenschaften Folgendes konvertiert:


 const { name, password } = user; 

Somit können Plugins sowohl einzeln als auch zusammen arbeiten. Wenn Sie Ihre eigenen Plug-Ins erstellen, wird empfohlen, diese Regel einzuhalten und ein Plug-In mit minimaler Funktionalität zu implementieren, das nur das tut, was Sie benötigen. Den Rest erledigen integrierte und benutzerdefinierte Plug-Ins.


Anwendungsbeispiel


Nachdem wir uns mit den integrierten Regeln vertraut gemacht haben, können wir ein Beispiel mit putout .
Erstellen Sie eine example.js Datei mit folgendem Inhalt:


 const x = 1, y = 2; const name = user.name; const password = user.password; console.log(name, password); 

Führen putout nun putout aus, indem Sie example.js als Argument übergeben:


 coderaiser@cloudcmd:~/example$ putout example.js /home/coderaiser/example/example.js 1:6 error "x" is defined but never used remove-unused-variables 1:13 error "y" is defined but never used remove-unused-variables 6:0 error Unexpected "console" call remove-console 1:0 error variables should be declared separately split-variable-declarations 3:6 error Object destructuring should be used apply-destructuring 4:6 error Object destructuring should be used apply-destructuring 6 errors in 1 files fixable with the `--fix` option 

Wir erhalten Informationen mit 6 Fehlern, die oben genauer betrachtet wurden. Jetzt werden wir sie korrigieren und sehen, was passiert ist:


 coderaiser@cloudcmd:~/example$ putout example.js --fix coderaiser@cloudcmd:~/example$ cat example.js const { name, password } = user; 

Infolge der Korrektur wurden nicht verwendete Variablen und Aufrufe von console.log gelöscht und auch eine Destrukturierung angewendet.


Einstellungen


Die Standardeinstellungen sind möglicherweise nicht immer und nicht für alle putout .putout.json putout unterstützt .putout.json Konfigurationsdatei .putout.json . Sie besteht aus den folgenden Abschnitten:


  • Regeln
  • Ignorieren
  • Match
  • Plugins

Regeln

Der Abschnitt rules enthält ein Regelsystem. Die Regeln sind standardmäßig wie folgt festgelegt:


 { "rules": { "remove-unused-variables": true, "remove-debugger": true, "remove-only": true, "remove-skip": true, "remove-process-exit": false, "remove-console": true, "split-variable-declarations": true, "remove-empty": true, "remove-empty-pattern": true, "convert-esm-to-commonjs": false, "apply-destructuring": true, "merge-destructuring-properties": true } } 

Um remove-process-exit zu aktivieren remove-process-exit setzen Sie es einfach in der Datei .putout.json auf true :


 { "rules": { "remove-process-exit": true } } 

Dies reicht aus, um alle im Code gefundenen Aufrufe von --fix zu melden und zu löschen, wenn die Option --fix .


Ignorieren

Wenn Sie der Liste der Ausnahmen einige Ordner hinzufügen müssen, fügen Sie einfach den Abschnitt zum ignore :


 { "ignore": [ "test/fixture" ] } 

Match

Wenn Sie beispielsweise ein verzweigtes Regelsystem benötigen, aktivieren Sie process.exit für das bin Verzeichnis. Verwenden Sie dazu einfach den Abschnitt match :


 { "match": { "bin": { "remove-process-exit": true, } } } 

Plugins

Wenn Sie Plugins verwenden, die nicht integriert sind und das Präfix putout-plugin- , müssen Sie diese in den Abschnitt " plugins " aufnehmen, bevor Sie sie im Abschnitt " rules aktivieren. Um beispielsweise das putout-plugin-add-hello-world zu verbinden und die putout-plugin-add-hello-world aktivieren, geben Sie einfach Folgendes an:


 { "rules": { "add-hello-world": true }, "plugins": [ "add-hello-world" ] } 

Motor ausschalten


Die putout Engine ist ein Befehlszeilentool, das Einstellungen liest, Dateien analysiert, Plugins lädt und putout und dann das Ergebnis der Plugins schreibt.


Er verwendet die neu zusammengestellte Bibliothek, mit deren Hilfe eine sehr wichtige Aufgabe ausgeführt werden kann: Sammeln Sie nach dem Parsen und der Transformation den Code in einem Zustand, der dem vorherigen so ähnlich wie möglich ist.


Zum Parsen wird ein ESTree kompatibler Parser verwendet ( babel ist derzeit mit dem estree Plugin verbunden, Änderungen sind jedoch in Zukunft möglich), und babel Tools werden für die Transformation verwendet. Warum genau babel ? Alles ist einfach. Tatsache ist, dass dies ein sehr beliebtes Produkt ist, das viel beliebter als andere ähnliche Tools ist und sich viel schneller entwickelt. Jeder neue Vorschlag im EcmaScript-Standard ist ohne ein Babel-Plugin nicht vollständig . Babel hat auch ein Buch, Babel Handbook , das alle Funktionen und Werkzeuge zum Durchlaufen und Transformieren eines AST-Baums sehr gut beschreibt.


Benutzerdefiniertes Plugin für Putout


Das putout Plugin-System ist recht einfach und den Eslint-Plugins sowie den Babel-Plugins sehr ähnlich. putout , anstelle einer Funktion sollte das putout Plugin 3 exportieren. Dies geschieht, um die Wiederverwendung von Code zu erhöhen, da das Duplizieren von Funktionen in 3 Funktionen nicht sehr praktisch ist. Es ist viel einfacher, sie in separate Funktionen zu integrieren und sie einfach an den richtigen Stellen aufzurufen.


Plugin Struktur

Das Putout Plugin besteht also aus 3 Funktionen:


  • report - gibt eine Nachricht zurück;
  • find - sucht nach fehlerhaften Orten und gibt sie zurück;
  • fix - behebt diese Stellen;

Der wichtigste Punkt, den Sie beim Erstellen eines Plugins für putout ist der Name. Es sollte mit putout-plugin- . Als nächstes kann der Name der Operation angegeben werden, die das Plugin ausführt. Beispielsweise sollte das Plugin zum remove-wrong putout-plugin-remove-wrong wie putout-plugin-remove-wrong aufgerufen werden: putout-plugin-remove-wrong .


Sie sollten auch die Wörter putout und putout-plugin zum Abschnitt package.json im keywords "putout": ">=3.10" und "putout": ">=3.10" in peerDependencies "putout": ">=3.10" oder die Version, die zum Zeitpunkt des Schreibens des Plugins die letzte sein wird.


Beispiel-Plugin für Putout

Lassen Sie uns ein Beispiel-Plugin schreiben, das das Wort debugger aus dem Code entfernt. Ein solches Plugin existiert bereits, es ist @ putout / plugin-remove-debugger und es ist einfach genug, es jetzt in Betracht zu ziehen.


Es sieht so aus:


 //        module.exports.report = () => 'Unexpected "debugger" statement'; //     ,  debugger    Visitor module.exports.find = (ast, {traverse}) => { const places = []; traverse(ast, { DebuggerStatement(path) { places.push(path); } }); return places; }; //  ,     module.exports.fix = (path) => { path.remove(); }; 

Wenn die .putout.json remove-debugger Regel in .putout.json , wird das @putout/plugin-remove-debugger geladen. Zunächst wird die find aufgerufen, die mit der traverse Funktion die Knoten des AST-Baums umgeht und alle erforderlichen Stellen speichert.


Im nächsten Schritt wird die report putout , um die gewünschte Nachricht zu erhalten.


Wenn das Flag --fix verwendet wird, wird die Plugin- fix Funktion aufgerufen und die Transformation durchgeführt. In diesem Fall wird der Knoten gelöscht.


Plugin Test Beispiel

Um das Testen von Plugins zu vereinfachen, wurde das Tool @ putout / test geschrieben. Im Kern ist es nichts weiter als ein Wrapper-over- Tape mit verschiedenen Methoden zur Bequemlichkeit und Vereinfachung des Testens.


Der Test für das remove-debugger Plugin könnte folgendermaßen aussehen:


 const removeDebugger = require('..'); const test = require('@putout/test')(__dirname, { 'remove-debugger': removeDebugger, }); //        test('remove debugger: report', (t) => { t.reportCode('debugger', 'Unexpected "debugger" statement'); t.end(); }); //    test('remove debugger: transformCode', (t) => { t.transformCode('debugger', ''); t.end(); }); 

Codemods

Nicht jede Transformation muss jeden Tag verwendet werden. Für einmalige Transformationen reicht es aus, dasselbe zu tun. Statt sie in npm legen Sie sie im ~/.putout . Beim Start putout in diesem Ordner, nimmt die Transformation auf und startet sie.


Hier ist eine Beispieltransformation, die die tape und Try-to-Tape- Verbindung durch einen Supertape- Aufruf ersetzt: Konvertieren von Band zu Supertape .


eslint-plugin-putout


Am Ende lohnt es sich, einen Punkt hinzuzufügen: putout versucht, den Code minimal zu ändern, aber wenn einem Freund passiert, dass einige Formatierungsregeln nicht eingehalten werden, ist eslint --fix immer bereit zu eslint --fix , und zu diesem Zweck gibt es ein spezielles eslint-plugin-putout-Plugin . Es kann viele Formatierungsfehler aufhellen und natürlich an die Vorlieben der Entwickler für ein bestimmtes Projekt angepasst werden. Das Anschließen ist einfach:


 { "extends": [ "plugin:putout/recommended", ], "plugins": [ "putout" ] } 

Bisher gibt es nur eine Regel: one-line-destructuring , die folgende:


 //  const { one } = hello; //  const {one} = hello; 

Es gibt viele weitere eslint Regeln, mit denen Sie sich genauer vertraut machen können.


Fazit


Ich möchte dem Leser für die Aufmerksamkeit danken, die diesem Text geschenkt wurde. Ich hoffe aufrichtig, dass das Thema AST-Transformationen populärer wird und Artikel über diesen faszinierenden Prozess häufiger erscheinen. Ich wäre sehr dankbar für Kommentare und Vorschläge im Zusammenhang mit der Weiterentwicklung von putout . Erstellen Sie ein Problem , senden Sie einen Pool von Anforderungen , testen Sie, schreiben Sie, welche Regeln Sie sehen möchten und wie Sie Ihren Code programmgesteuert programmieren. Wir werden zusammenarbeiten, um das AST-Transformationstool zu verbessern.

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


All Articles