Application pratique de la transformation d'arbre AST en utilisant le putout comme exemple

Présentation


Chaque jour, lorsque vous travaillez sur le code, sur le chemin de la mise en œuvre d'une fonctionnalité utile à l'utilisateur, des modifications forcées (inévitables ou simplement souhaitables) du code deviennent. Cela peut être la refactorisation, la mise à jour d'une bibliothèque ou d'un framework vers une nouvelle version majeure, la mise à jour de la syntaxe JavaScript (ce qui n'est pas rare récemment). Même si la bibliothèque fait partie d'un projet de travail, le changement est inévitable. La plupart de ces changements sont routiniers. Il n'y a rien d'intéressant pour le développeur en eux, d'une part, d'autre part, cela n'apporte rien à l'entreprise, et troisièmement, pendant le processus de mise à jour, vous devez être très prudent pour ne pas casser le bois de chauffage et ne pas casser la fonctionnalité. Ainsi, nous arrivons à la conclusion qu'il est préférable de déplacer une telle routine sur les épaules des programmes afin qu'ils fassent tout eux-mêmes et que la personne, à son tour, contrôle si tout a été fait correctement. C'est ce qui sera discuté dans l'article d'aujourd'hui.


AST


Pour le traitement de code de programme, il est nécessaire de le traduire en une représentation spéciale, avec laquelle il serait pratique pour les programmes de fonctionner. Une telle représentation existe, elle s'appelle Arbre de Syntaxe Abstraite (AST).
Pour l'obtenir, utilisez des analyseurs. L'AST résultant peut être transformé comme vous le souhaitez, puis pour enregistrer le résultat, vous avez besoin d'un générateur de code. Examinons plus en détail chacune des étapes. Commençons par l'analyseur.


Analyseur


Et nous avons donc le code:


a + b 

Les analyseurs sont généralement divisés en deux parties:


  • Analyse lexicale

Décompose le code en jetons, chacun décrivant une partie du code:


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

  • Analyse

Construit un arbre de syntaxe à partir de jetons:


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

Et maintenant, nous avons déjà cette idée même, avec laquelle vous pouvez travailler par programmation. Il convient de préciser qu'il existe un grand nombre d'analyseurs JavaScript , en voici quelques-uns:


  • babel-parser - un analyseur qui utilise babel ;
  • espree - un analyseur qui utilise eslint ;
  • gland - l'analyseur sur lequel les deux précédents sont basés;
  • esprima - un analyseur populaire qui prend en charge JavaScript jusqu'à EcmaScript 2017;
  • cherow est un nouveau joueur parmi les analyseurs JavaScript qui prétend être le plus rapide;

Il existe un analyseur JavaScript standard, il est appelé ESTree et définit les nœuds à analyser.
Pour une analyse plus détaillée du processus d'implémentation de l'analyseur (ainsi que du transformateur et du générateur), vous pouvez lire le super-minuscule-compilateur .


Transformateur


Afin de transformer l'arborescence AST, vous pouvez utiliser le modèle Visitor , par exemple, en utilisant la bibliothèque @ babel / traverse . Le code suivant affichera les noms de tous les identificateurs de code JavaScript à partir de la variable de code .


 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); } }); 

Générateur


Vous pouvez générer du code, par exemple, en utilisant @ babel / generator , de cette façon:


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

Et donc, à ce stade, le lecteur aurait dû avoir une idée de base de ce qui est nécessaire pour transformer le code JavaScript, et avec quels outils il est implémenté.


Il vaut également la peine d'ajouter un outil en ligne comme astexplorer , il combine un grand nombre d'analyseurs, de transformateurs et de générateurs.


Putout


Putout est un transformateur de code compatible avec les plugins. En fait, c'est un croisement entre eslint et babel , combinant les avantages des deux outils.


Comment eslint putout affiche les zones à problème dans le code, mais contrairement à eslint putout , le comportement du code est modifié, c'est-à-dire qu'il est capable de corriger toutes les erreurs qu'il peut trouver.


Comme babel putout convertit le code, mais essaie de le modifier de façon minimale, afin qu'il puisse être utilisé pour travailler avec du code stocké dans le référentiel.


Prettier mérite également d'être mentionné, il s'agit d'un outil de formatage et il diffère radicalement.


Jscodeshift est situé non loin de putout , mais il ne prend pas en charge les plugins, n'affiche pas de messages d'erreur et utilise également ast-types au lieu de @ babel / types .


Histoire d'apparence


Dans le processus, eslint m'aide beaucoup avec mes conseils. Mais parfois je veux plus de lui. Par exemple, pour supprimer le débogueur , corrigez test.only et supprimez également les variables inutilisées. Le dernier point a constitué la base de la mise au putout , au cours du processus de développement, il est devenu clair que ce n'est pas facile et que de nombreuses autres transformations sont beaucoup plus faciles à mettre en œuvre. Ainsi, putout passé d'une fonction au système de plugins. La suppression des variables inutilisées est désormais le processus le plus difficile, mais cela ne nous empêche pas de développer et de prendre en charge de nombreuses autres transformations tout aussi utiles.


Comment fonctionne le putout à l'intérieur


putout travail de mise au point peut être divisé en deux parties: le moteur et les plugins. Cette architecture vous permet de ne pas être distrait par les transformations lorsque vous travaillez avec le moteur, et lorsque vous travaillez sur des plug-ins, vous vous concentrerez autant que possible sur leur objectif.


Plugins intégrés


putout est construit sur un système de plugins. Chaque plugin représente une règle. En utilisant les règles intégrées, vous pouvez effectuer les opérations suivantes:


  • Rechercher et supprimer:


    • variables inutilisées
    • debugger
    • appelez test.only
    • appelez test.skip
    • appeler console.log
    • appelez process.exit
    • blocs vides
    • motifs vides

  • Déclaration de variable de recherche et de fractionnement:


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

  • Convertir esm en commonjs :



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

  • Appliquer la déstructuration:

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

  1. Combinez les propriétés de déstructuration:

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

Chaque plugin est construit selon la philosophie Unix , c'est-à-dire qu'ils sont aussi simples que possible, chacun effectue une action, ce qui les rend faciles à combiner, car ils sont, par essence, des filtres.


Par exemple, avoir le code suivant:


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

Il est d'abord converti en utilisant la déstructuration d'application pour:


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

Ensuite, en utilisant les propriétés de fusion-destruction, il est converti en:


 const { name, password } = user; 

Ainsi, les plugins peuvent fonctionner à la fois séparément et ensemble. Lors de la création de vos propres plug-ins, il est recommandé de respecter cette règle et d'implémenter un plug-in avec des fonctionnalités minimales, en ne faisant que ce dont vous avez besoin, et les plug-ins intégrés et personnalisés se chargeront du reste.


Exemple d'utilisation


Après nous être familiarisés avec les règles intégrées, nous pouvons considérer un exemple utilisant putout .
Créez un fichier example.js avec le contenu suivant:


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

Exécutez maintenant putout en passant example.js comme argument:


 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 

Nous recevrons des informations contenant 6 erreurs, examinées plus en détail ci-dessus, nous allons maintenant les corriger et voir ce qui s'est passé:


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

À la suite de la correction, les variables inutilisées et les appels console.log ont été supprimés et la déstructuration a également été appliquée.


Paramètres


Les paramètres par défaut peuvent ne pas toujours et pas pour tout le monde, donc putout prend en charge le fichier de configuration .putout.json , il se compose des sections suivantes:


  • Les règles
  • Ignorer
  • Match
  • Plugins

Les règles

La section des rules contient un système de règles. Par défaut, les règles sont définies comme suit:


 { "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 } } 

Pour activer remove-process-exit définissez-le simplement sur true dans le fichier .putout.json :


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

Ce sera suffisant pour signaler tous les appels process.exit trouvés dans le code et les supprimer si l'option --fix est --fix .


Ignorer

Si vous devez ajouter des dossiers à la liste des exceptions, ajoutez simplement la section ignore :


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

Match

Si vous avez besoin d'un système de règles ramifié, par exemple, activez process.exit pour le répertoire bin , utilisez simplement la section de match :


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

Plugins

Si vous utilisez des plugins qui ne sont pas intégrés et qui ont le préfixe putout-plugin- , vous devez les inclure dans la section plugins avant de les activer dans la section rules . Par exemple, pour connecter le putout-plugin-add-hello-world et activer la règle add-hello-world , spécifiez simplement:


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

Moteur de mise hors tension


Le moteur putout est un outil en ligne de commande qui lit les paramètres, analyse les fichiers, charge et exécute les plugins, puis écrit le résultat des plugins.


Il utilise la bibliothèque de refonte , qui aide à effectuer une tâche très importante: après l'analyse et la transformation, collecter le code dans un état aussi similaire que possible au précédent.


Pour l'analyse, un ESTree compatible ESTree est utilisé ( babel est actuellement avec le plugin estree , mais des modifications sont possibles à l'avenir), et les outils babel sont utilisés pour la transformation. Pourquoi babel exactement? Tout est simple. Le fait est que c'est un produit très populaire, beaucoup plus populaire que d'autres outils similaires, et qu'il se développe beaucoup plus rapidement. Chaque nouvelle proposition dans le standard EcmaScript n'est pas complète sans un plugin babel . Babel a également un livre, Babel Handbook , qui décrit très bien toutes les fonctionnalités et les outils pour traverser et transformer un arbre AST.


Plugin personnalisé pour Putout


Le système de plugins putout est assez simple, et très similaire aux plugins eslint , ainsi qu'aux plugins babel . Certes, au lieu d'une fonction, putout plugin devrait exporter 3. Cela est fait afin d'augmenter la réutilisation du code, car la duplication de la fonctionnalité dans 3 fonctions n'est pas très pratique, il est beaucoup plus facile de la mettre dans des fonctions séparées et de simplement l'appeler aux bons endroits.


Structure du plugin

Putout plugin Putout comprend donc 3 fonctions:


  • report - renvoie un message;
  • find - recherche des lieux avec des erreurs et les renvoie;
  • fix - corrige ces endroits;

Le principal point à retenir lors de la création d'un plugin pour putout est son nom, il doit commencer par putout-plugin- . Ensuite peut être le nom de l'opération que le plugin effectue, par exemple, le plugin remove-wrong bad devrait être appelé comme ceci: putout-plugin-remove-wrong .


Vous devez également ajouter les mots: putout et putout-plugin à la section package.json dans la section des keywords - keywords , et spécifier "putout": ">=3.10" dans peerDependencies "putout": ">=3.10" , ou la version qui sera la dernière au moment de l'écriture du plug-in.


Exemple de plugin pour Putout

Écrivons un exemple de plugin qui supprimera le mot debugger du code. Un tel plugin existe déjà, c'est @ putout / plugin-remove-debugger et il est assez simple pour le considérer maintenant.


Cela ressemble à ceci:


 //        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(); }; 

Si la règle remove-debugger est incluse dans .putout.json , le @putout/plugin-remove-debugger sera chargé. Tout d'abord, la fonction find est appelée qui, à l'aide de la fonction traverse , contournera les nœuds de l'arbre AST et enregistrera tous les emplacements nécessaires.


L'étape suivante se transforme en report pour obtenir le message souhaité.


Si l'indicateur --fix est utilisé, la fonction fix du plugin sera appelée et la transformation sera effectuée, dans ce cas, le nœud sera supprimé.


Exemple de test de plugin

Afin de simplifier le test des plugins, l'outil @ putout / test a été écrit. À la base, ce n'est rien de plus qu'un wrapper sur bande , avec plusieurs méthodes pour la commodité et la simplification des tests.


Le test du plugin remove-debugger pourrait ressembler à ceci:


 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

Toutes les transformations ne doivent pas être utilisées tous les jours, pour les transformations ponctuelles, il suffit de faire la même chose, mais au lieu de publier sur npm placez-la dans le ~/.putout . Au démarrage, putout va chercher dans ce dossier, ramasser et démarrer la transformation.


Voici un exemple de transformation qui remplace la connexion de tape et de tentative de bande par un appel de supertape : convert-tape-to-supertape .


eslint-plugin-putout


En fin de compte, cela vaut la peine d'ajouter un point: putout essaie de modifier le code de manière minimale, mais s'il arrive à un ami que certaines règles de formatage ne respectent pas, eslint --fix est toujours prêt à eslint --fix , et à cet effet, il existe un plugin eslint-plugin-putout spécial. Il peut égayer de nombreuses erreurs de formatage et, bien sûr, il peut être personnalisé en fonction des préférences des développeurs sur un projet spécifique. La connexion est simple:


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

Jusqu'à présent, il n'y a qu'une seule règle: la one-line-destructuring , elle fait ce qui suit:


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

Il y a beaucoup plus de règles eslint incluses que vous pouvez vous familiariser plus en détail .


Conclusion


Je remercie le lecteur de l'attention portée à ce texte. J'espère sincèrement que le sujet des transformations AST deviendra plus populaire et que les articles sur ce processus fascinant apparaîtront plus souvent. Je serais très reconnaissant pour tout commentaire et suggestion concernant le développement ultérieur de la putout . Créez un problème , envoyez un pool de demandes , testez, écrivez les règles que vous souhaitez voir et comment programmer votre code par programme, nous travaillerons ensemble pour améliorer l'outil de transformation AST.

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


All Articles