Créez des constructions de syntaxe JavaScript personnalisées à l'aide de Babel. 2e partie

Aujourd'hui, nous publions la deuxième partie d'une traduction de l'extension de syntaxe JavaScript utilisant Babel.



Première partie vertigineuse

Comment fonctionne l'analyse


L'analyseur reçoit une liste de jetons du système de tokenisation de code et, en considérant les jetons un par un, construit un AST. Afin de prendre une décision sur la façon d'utiliser les jetons et de comprendre quel jeton peut être attendu ensuite, l'analyseur se réfère à la spécification de la grammaire de la langue.

La spécification de grammaire ressemble à ceci:

... ExponentiationExpression -> UnaryExpression                             UpdateExpression ** ExponentiationExpression MultiplicativeExpression -> ExponentiationExpression                             MultiplicativeExpression ("*" or "/" or "%") ExponentiationExpression AdditiveExpression    -> MultiplicativeExpression                             AdditiveExpression + MultiplicativeExpression                             AdditiveExpression - MultiplicativeExpression ... 

Il décrit la priorité d'exécution d'expressions ou d'instructions. Par exemple, une expression AdditiveExpression peut représenter l'une des constructions suivantes:

  • Expression MultiplicativeExpression .
  • Une expression AdditiveExpression , suivie d'un opérateur de jeton + , suivie d'une expression MultiplicativeExpression .
  • Une expression AdditiveExpression , suivie d'un jeton « - », suivie d'une expression MultiplicativeExpression .

Par conséquent, si nous avons l'expression 1 + 2 * 3 , cela ressemblera à ceci:

 (AdditiveExpression "+" 1 (MultiplicativeExpression "*" 2 3)) 

Mais il n'en sera pas ainsi:

 (MultiplicativeExpression "*" (AdditiveExpression "+" 1 2) 3) 

Le programme, en utilisant ces règles, est converti en code émis par l'analyseur:

 class Parser {  // ...  parseAdditiveExpression() {    const left = this.parseMultiplicativeExpression();    //    -  `+`  `-`    if (this.match(tt.plus) || this.match(tt.minus)) {      const operator = this.state.type;      //          this.nextToken();      const right = this.parseMultiplicativeExpression();      //        this.finishNode(        {          operator,          left,          right,        },        'BinaryExpression'      );    } else {      //  MultiplicativeExpression      return left;    }  } } 

Veuillez noter que voici une version extrêmement simplifiée de ce qui est réellement présent dans Babel. Mais j'espère que ce morceau de code nous permettra d'illustrer l'essence de ce qui se passe.

Comme vous pouvez le voir, l'analyseur est, par nature, récursif. Il passe des conceptions les moins prioritaires aux conceptions les plus prioritaires. Par exemple, parseAdditiveExpression appelle parseMultiplicativeExpression , et cette construction appelle parseExponentiationExpression etc. Ce processus récursif est appelé analyse de descente récursive .

Fonctions this.eat, this.match, this.next


Vous avez peut-être remarqué que dans les exemples précédents, certaines fonctions auxiliaires ont été utilisées, telles que this.eat , this.match , this.next et d'autres. Ce sont les fonctions internes de l'analyseur Babel. Ces fonctions, cependant, ne sont pas propres à Babel; elles sont généralement présentes dans d'autres analyseurs.

  • La fonction this.match renvoie une valeur booléenne indiquant si le jeton actuel remplit la condition spécifiée.
  • La fonction this.next avance dans la liste des jetons jusqu'au jeton suivant.
  • La fonction this.eat renvoie la même chose que la fonction this.match et si this.match renvoie true , alors this.eat effectue, avant de renvoyer true , un appel à this.next .
  • La fonction this.lookahead vous permet d'obtenir le prochain jeton sans avancer, ce qui aide à prendre une décision sur le nœud actuel.

Si vous regardez à nouveau le code de l'analyseur que nous avons modifié, vous constaterez que sa lecture est devenue beaucoup plus facile:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  parseStatementContent(/* ...*/) {    // ...    // NOTE:   match        if (this.match(tt._function)) {      this.next();      // NOTE:     ,          this.parseFunction();    }  }  // ...  parseFunction(/* ... */) {    // NOTE:   eat         node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);    node.id = this.parseFunctionId();  } } 

Je sais que je n'ai pas expliqué en profondeur les caractéristiques des analyseurs. Par conséquent, ici et - quelques ressources utiles sur ce sujet. J'en ai appris beaucoup et je peux vous les recommander.

Vous pourriez être intéressé à savoir comment j'ai pu visualiser la syntaxe que j'ai créée dans Babel AST Explorer lorsque j'ai montré le nouvel attribut « curry » qui est apparu dans AST.

Cela est devenu possible grâce au fait que j'ai ajouté une nouvelle fonctionnalité dans Babel AST Explorer qui vous permet de charger votre propre analyseur dans cet outil de recherche AST.

Si vous suivez le chemin packages/babel-parser/lib , vous pouvez trouver une version compilée de l'analyseur et une carte de code. Dans le panneau Babel AST Explorer , vous pouvez voir le bouton permettant de charger votre propre analyseur. En téléchargeant les packages/babel-parser/lib/index.js vous pouvez visualiser l'AST généré à l'aide de votre propre analyseur.


Visualisation AST

Notre plugin pour Babel


Maintenant que l'analyseur est terminé, écrivons un plugin pour Babel.

Mais, vous avez peut-être maintenant des doutes sur la façon dont nous allons utiliser notre propre analyseur Babel, en particulier compte tenu de la pile technologique que nous utilisons pour construire le projet.

Certes, il n'y a rien à craindre. Le plugin Babel peut fournir des capacités d'analyseur. La documentation connexe est disponible sur le site Web de Babel.

 babel-plugin-transformation-curry-function.js import customParser from './custom-parser'; export default function ourBabelPlugin() {  return {    parserOverride(code, opts) {      return customParser.parse(code, opts);    },  }; } 

Depuis que nous avons créé un fork de l'analyseur Babel, cela signifie que toutes les fonctionnalités existantes de l'analyseur, ainsi que les plug-ins intégrés, continueront de fonctionner parfaitement.

Après nous être débarrassés de ces doutes, regardons comment créer une fonction telle qu'elle supporte le curry.

Si vous ne pouviez pas supporter les attentes et avez déjà essayé d'ajouter notre plug-in à votre système de construction de projet, vous remarquerez peut-être que les fonctions qui prennent en charge le curry sont compilées en fonctions régulières.

Cela se produit car, après avoir analysé et transformé le code, Babel utilise @babel/generator pour générer du code à partir de l'AST transformé. Puisque @babel/generator ne sait rien du nouvel attribut curry , il l'ignore simplement.

Si un jour les fonctions qui prennent en charge le curry entrent dans la norme JavaScript, alors vous voudrez peut-être faire un PR pour ajouter un nouveau code ici .

Afin que la fonction prenne en charge le curry, vous pouvez l'encapsuler dans un currying fonction d'ordre supérieur:

 function currying(fn) {  const numParamsRequired = fn.length;  function curryFactory(params) {    return function (...args) {      const newParams = params.concat(args);      if (newParams.length >= numParamsRequired) {        return fn(...newParams);      }      return curryFactory(newParams);    }  }  return curryFactory([]); } 

Si vous êtes intéressé par les caractéristiques de la mise en œuvre du mécanisme de fonctions de curry dans JS - jetez un œil à ce matériel.

Par conséquent, nous, transformant une fonction qui prend en charge le curry, pouvons le faire:

 //   function @@ foo(a, b, c) {  return a + b + c; } //    const foo = currying(function foo(a, b, c) {  return a + b + c; }) 

Pour l'instant, nous ne ferons pas attention au mécanisme de montée en fonctions en JavaScript, qui vous permet d'appeler la fonction foo avant qu'elle ne soit définie.

Voici à quoi ressemble le code de transformation:

 babel-plugin-transformation-curry-function.js export default function ourBabelPlugin() {  return {    // ... <i>    visitor: {      FunctionDeclaration(path) {        if (path.get('curry').node) {          // const foo = curry(function () { ... });          path.node.curry = false;          path.replaceWith(            t.variableDeclaration('const', [              t.variableDeclarator(                t.identifier(path.get('id.name').node),                t.callExpression(t.identifier('currying'), [                  t.toExpression(path.node),                ])              ),            ])          );        }      },    },</i>  }; } 

Il vous sera beaucoup plus facile de comprendre si vous lisez ce document sur les transformations dans Babel.

Nous sommes maintenant confrontés à la question de savoir comment donner à ce mécanisme un accès à la fonction de currying . Ici, vous pouvez utiliser l'une des deux approches.

▍ Approche n ° 1: on peut supposer que la fonction de curry est déclarée dans le périmètre global


Si oui, alors le travail est déjà fait.

Si, lors de l'exécution du code compilé, il s'avère que la fonction de currying n'est pas définie, alors nous rencontrerons un message d'erreur qui ressemble à "le currying is not defined ". Il est très similaire au message " régénérateurRuntime n'est pas défini ".

Par conséquent, si quelqu'un utilise votre babel-plugin-transformation-curry-function , vous devrez peut-être l'informer qu'il doit installer le polyfill de currying pour s'assurer que ce plugin fonctionne correctement.

▍ Approche n ° 2: vous pouvez utiliser babel / helpers


Vous pouvez ajouter une nouvelle fonction d'assistance à @babel/helpers . Il est peu probable que ce développement soit combiné avec le @babel/helpers officiel @babel/helpers . En conséquence, vous devrez trouver un moyen de montrer à @babel/core l'emplacement de votre @babel/helpers :

 package.json {  "resolutions": {    "@babel/helpers": "7.6.0--your-custom-forked-version",  } 

Je n'ai pas essayé cela moi-même, mais je pense que ce mécanisme fonctionnera. Si vous l'essayez et rencontrez des problèmes, j'en discuterai avec plaisir.

@babel/helpers nouvelle fonction d'aide à @babel/helpers très simple.

Tout d'abord, accédez au fichier packages / babel-helpers / src / helpers.js et ajoutez une nouvelle entrée:

 helpers.currying = helper("7.6.0")`  export default function currying(fn) {    const numParamsRequired = fn.length;    function curryFactory(params) {      return function (...args) {        const newParams = params.concat(args);        if (newParams.length >= numParamsRequired) {          return fn(...newParams);        }        return curryFactory(newParams);      }    }    return curryFactory([]);  } `; 

Lors de la description d'une fonction auxiliaire, la version requise @babel/core indiquée. Certaines difficultés ici peuvent être causées par le export default la fonction de currying .

Pour utiliser une fonction d'assistance, il suffit d'appeler this.addHelper() :

 // ... path.replaceWith(  t.variableDeclaration('const', [    t.variableDeclarator(      t.identifier(path.get('id.name').node),      t.callExpression(this.addHelper("currying"), [        t.toExpression(path.node),      ])    ),  ]) ); 

La commande this.addHelper , si nécessaire, incorporera la fonction d'assistance en haut du fichier et renverra un Identifier indiquant la fonction implémentée.

Remarques


Je travaille depuis longtemps sur Babel, mais je n'ai pas encore eu à ajouter de fonctionnalités pour prendre en charge la nouvelle syntaxe JavaScript de l'analyseur. J'ai principalement travaillé sur la correction de bugs et l'amélioration de ce qui est pertinent pour les fonctionnalités des langues officielles.

Cependant, depuis un certain temps maintenant, j'étais occupé à l'idée d'ajouter de nouvelles constructions de syntaxe au langage. En conséquence, j'ai décidé d'écrire du matériel à ce sujet et de l'essayer. C'est incroyablement agréable de voir que tout fonctionne exactement comme prévu.

La capacité de contrôler la syntaxe du langage que vous utilisez est une puissante source d'inspiration. Cela permet, en implémentant certaines constructions complexes, d'écrire moins de code, ou d'écrire du code plus simple qu'auparavant. Les mécanismes de transformation de code simple en constructions complexes sont automatisés et transférés à l'étape de compilation. Cela rappelle comment async/await résout les problèmes des rappels infernaux et des longues chaînes de promesses.

Résumé


Ici, nous avons parlé de la façon de modifier les capacités de l'analyseur Babel, nous avons écrit notre propre plugin de transformation de code, parlé brièvement de @babel/generator et de la création de fonctions d'assistance à l'aide de @babel/helpers . Les informations concernant la transformation du code ne sont données que schématiquement. En savoir plus à leur sujet ici .

Au cours du processus, nous avons abordé certaines caractéristiques des analyseurs. Si ce sujet vous intéresse - alors, ici et - des ressources qui vous sont utiles.

La séquence d'actions que nous avons effectuée est très similaire à une partie du processus qui est effectué lorsqu'une nouvelle fonctionnalité JavaScript est soumise au TC39. Voici la page du référentiel TC39 où vous pouvez trouver des informations sur les offres en cours. Vous trouverez ici des informations plus détaillées sur la façon de travailler avec des offres similaires. Lors de la proposition d'une nouvelle fonctionnalité JavaScript, celui qui la propose écrit généralement des polyfills ou, en forçant Babel, prépare une démonstration prouvant que la phrase fonctionne. Comme vous pouvez le voir, créer un fork d'un analyseur ou écrire un polyfill n'est pas la partie la plus difficile du processus de proposition de nouvelles fonctionnalités JS. Il est difficile de déterminer le domaine de l'innovation, de planifier et de réfléchir aux options de son utilisation et aux cas limites; Il est difficile de recueillir les opinions et suggestions des membres de la communauté des programmeurs JavaScript. Par conséquent, je voudrais exprimer ma gratitude à tous ceux qui trouvent la force d'offrir de nouvelles fonctionnalités JavaScript TC39, développant ainsi ce langage.

Voici une page sur GitHub qui vous permettra de voir la grande image de ce que nous avons fait ici.

Chers lecteurs! Avez-vous déjà voulu étendre la syntaxe de JavaScript?


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


All Articles