Créez des constructions de syntaxe JavaScript personnalisées à l'aide de Babel. Partie 1

Aujourd'hui, nous publions la première partie de la traduction du matériel, qui est dédiée à la création de vos propres constructions syntaxiques pour JavaScript à l'aide de Babel.



Revue


Tout d'abord, regardons ce que nous réaliserons lorsque nous arriverons à la fin de ce document:

//  '@@'   `foo`   function @@ foo(a, b, c) {   return a + b + c; } console.log(foo(1, 2)(3)); // 6 

Nous allons implémenter la syntaxe @@ qui permet les fonctions de curry . Cette syntaxe est similaire à celle utilisée pour créer des fonctions de générateur , mais dans notre cas, au lieu du signe * , une séquence de caractères @@ est placée entre le mot-clé de la function et le nom de la function . Par conséquent, lors de la déclaration de fonctions, vous pouvez utiliser une construction de la function @@ name(arg1, arg2) formulaire function @@ name(arg1, arg2) .

Dans l'exemple ci-dessus, lorsque vous travaillez avec la fonction foo , vous pouvez utiliser son application partielle . Appeler la fonction foo en lui passant autant de paramètres qui est inférieur au nombre d'arguments dont elle a besoin, renverra une nouvelle fonction qui peut prendre les arguments restants:

 foo(1, 2, 3); // 6 const bar = foo(1, 2); // (n) => 1 + 2 + n bar(3); // 6 

J'ai choisi la séquence de caractères @@ car le symbole @ ne peut pas être utilisé dans les noms de variables. Cela signifie qu'une construction de la function@@foo(){} formulaire function@@foo(){} sera également syntaxiquement correcte. De plus, "l'opérateur" @ est utilisé pour les fonctions de décorateur , et je voulais utiliser quelque chose de complètement nouveau. En conséquence, j'ai choisi la construction @@ .

Pour atteindre notre objectif, nous devons effectuer les actions suivantes:

  • CrĂ©ez une fourchette de l'analyseur Babel.
  • CrĂ©ez votre propre plugin Babel pour la transformation de code.

On dirait quelque chose d'impossible?
En fait, il n'y a rien de terrible ici, nous analyserons tout en détail ensemble. J'espère que lorsque vous lirez ceci, vous maîtriserez magistralement les subtilités de Babel.

Création d'une fourchette Babel


Accédez au référentiel Babel sur GitHub et cliquez sur le bouton Fork , qui se trouve dans le coin supérieur gauche de la page.


Création d'une fourchette de Babel ( image en taille réelle )

Et en passant, si vous venez de créer le fork du projet open source populaire pour la première fois - félicitations!

Maintenant, clonez la fourche Babel sur votre ordinateur et préparez-la pour le travail .

 $ git clone https://github.com/tanhauhau/babel.git # set up $ cd babel $ make bootstrap $ make build 

Permettez-moi maintenant de parler brièvement de l'organisation du référentiel Babel.

Babel utilise un monorepositaire. Tous les packages (par exemple @babel/core , @babel/parser , @babel/plugin-transform-react-jsx et ainsi de suite) se trouvent dans le dossier packages/ . Cela ressemble Ă  ceci:

 - doc - packages  - babel-core  - babel-parser  - babel-plugin-transform-react-jsx  - ... - Gulpfile.js - Makefile - ... 

Je note que Babel utilise un Makefile pour automatiser les tâches. Lors de la construction d'un projet par la make build , Gulp est utilisé comme gestionnaire de tâches.

Conversion de code en AST Short Course


Si vous n'êtes pas familier avec des concepts tels que «analyseur» et «arbre de syntaxe abstraite» (AST), puis avant de continuer la lecture, je vous recommande fortement de jeter un œil à ce matériel.

Si vous parlez très brièvement de ce qui se passe lors de l'analyse (analyse) du code, vous obtenez les informations suivantes:

  • Le code prĂ©sentĂ© sous forme de chaĂ®ne (type string ) ressemble Ă  une longue liste de caractères: f, u, n, c, t, i, o, n, , @, @, f, ...
  • Au tout dĂ©but, Babel effectue la tokenisation du code. Dans cette Ă©tape, Babel scanne le code et crĂ©e des jetons. Par exemple, quelque chose comme function, @@, foo, (, a, ...
  • Ensuite, les jetons sont passĂ©s Ă  travers l'analyseur pour leur analyse. Ici Babel, basĂ© sur la spĂ©cification du langage JavaScript, crĂ©e un arbre de syntaxe abstrait.

Voici une excellente ressource pour ceux qui veulent en savoir plus sur les compilateurs.

Si vous pensez que le «compilateur» est quelque chose de très complexe et d'incompréhensible, sachez qu'en réalité tout n'est pas si mystérieux. La compilation consiste simplement à analyser le code et à créer un nouveau code sur sa base, que nous appellerons XXX. Le code XXX peut être représenté par le code machine (peut-être, le code machine est ce qui apparaît d'abord dans l'esprit de la plupart d'entre nous lorsque nous pensons au compilateur). Il peut s'agir d'un code JavaScript compatible avec les anciens navigateurs. En fait, l'une des principales fonctions de Babel est la compilation de code JS moderne en code compréhensible pour les navigateurs obsolètes.

DĂ©velopper votre propre analyseur pour Babel


Nous allons travailler dans le dossier packages/babel-parser/ :

 - src/  - tokenizer/  - parser/  - plugins/    - jsx/    - typescript/    - flow/    - ... - test/ 

Nous avons déjà parlé de la tokenisation et de l'analyse. Vous pouvez trouver le code qui implémente ces processus dans des dossiers avec les noms correspondants. Le plugins/ dossier contient des plugins (plugins) qui étendent les capacités de l'analyseur de base et ajoutent la prise en charge de syntaxes supplémentaires dans le système. C'est exactement ainsi, par exemple, que le jsx et le flow sont implémentés.

Résolvons notre problème en utilisant la technologie de développement par le biais de tests (développement piloté par les tests, TDD). À mon avis, il est plus facile d'écrire un test d'abord, puis, en travaillant progressivement sur le système, de faire ce test sans erreurs. Cette approche est particulièrement bonne lorsque vous travaillez dans une base de code inconnue. TDD permet de comprendre facilement où vous devez apporter des modifications au code pour implémenter vos fonctionnalités prévues.

 packages/babel-parser/test/curry-function.js import { parse } from '../lib'; function getParser(code) {  return () => parse(code, { sourceType: 'module' }); } describe('curry function syntax', function() {  it('should parse', function() {    expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();  }); }); 

Vous pouvez exécuter le test de babel-parser comme ceci: TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only . Cela vous permettra de voir les erreurs:

 SyntaxError: Unexpected token (1:9) at Parser.raise (packages/babel-parser/src/parser/location.js:39:63) at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16) at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18) at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23) at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52) 

Si vous trouvez que la visualisation de tous les tests prend trop de temps, vous pouvez, pour exécuter le test souhaité, appeler directement jest :

 BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js 

Notre analyseur a découvert 2 @ jetons, apparemment complètement innocents, où ils ne devraient pas être.

Comment le savais-je? La réponse à cette question nous aidera à trouver l'utilisation du mode de surveillance de code lancé par la make watch .

La visualisation de la pile d'appels nous conduit à packages / babel-parser / src / parser / expression.js , où l'exception this.unexpected() levée.

Ajoutez quelques commandes de journalisation Ă  ce fichier:

 packages/babel-parser/src/parser/expression.js parseIdentifierName(pos: number, liberal?: boolean): string {  if (this.match(tt.name)) {    // ...  } else {    console.log(this.state.type); //      console.log(this.lookahead().type); //      throw this.unexpected();  } } 

Comme vous pouvez le voir, les deux jetons sont @ :

 TokenType {  label: '@',  // ... } 

Comment ai-je découvert que les constructions this.state.type et this.lookahead().type me donneront les jetons actuels et suivants?
J'en parlerai dans la section de ce document consacrée aux fonctions this.eat , this.match et this.next .

Avant de continuer, résumons:

  • Nous avons Ă©crit un test pour babel-parser .
  • Nous avons exĂ©cutĂ© le test en utilisant make test-only .
  • Nous avons utilisĂ© le mode de surveillance du code Ă  l'aide de make watch .
  • Nous avons appris l'Ă©tat de l'analyseur et this.state.type informations sur le type du jeton actuel ( this.state.type ) dans la console.

Et maintenant, nous allons nous assurer que 2 caractères @ ne sont pas perçus comme des jetons séparés, mais comme un nouveau jeton @@ , celui que nous avons décidé d'utiliser pour les fonctions de curry.

Nouveau jeton: "@@"


Tout d'abord, regardons où les types de jetons sont déterminés. Il s'agit du fichier packages / babel-parser / src / tokenizer / types.js .

Vous trouverez ici une liste de jetons. Ajoutez ici la définition du nouveau jeton atat :

 packages/babel-parser/src/tokenizer/types.js export const types: { [name: string]: TokenType } = {  // ...  at: new TokenType('@'),  atat: new TokenType('@@'), }; 

Cherchons maintenant l'endroit dans le code où, dans le processus de tokenisation, les jetons sont créés. La recherche de la séquence de caractères tt.at dans babel-parser/src/tokenizer nous conduit au fichier: packages / babel-parser / src / tokenizer / index.js . Dans babel-parser les types de jetons sont importés en tant que tt .

Maintenant, si après le symbole @ actuel vient un autre @ , créez un nouveau jeton tt.atat au lieu du jeton tt.at :

 packages/babel-parser/src/tokenizer/index.js getTokenFromCode(code: number): void {  switch (code) {    // ...    case charCodes.atSign:      //    -  `@`      if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {        //  `tt.atat`  `tt.at`        this.finishOp(tt.atat, 2);      } else {        this.finishOp(tt.at, 1);      }      return;    // ...  } } 

Si vous relancez le test, vous remarquerez que les informations sur les jetons actuels et suivants ont changé:

 //   TokenType {  label: '@@',  // ... } //   TokenType {  label: 'name',  // ... } 

Ça a déjà l'air plutôt bien. Nous allons continuer le travail.

Nouvel analyseur


Avant de poursuivre, regardez comment les fonctions du générateur sont représentées dans AST.


AST pour la fonction générateur ( image en taille réelle )

Comme vous pouvez le voir, l'attribut generator: true de l'entité FunctionDeclaration indique qu'il s'agit d'une FunctionDeclaration générateur.

Nous pouvons adopter une approche similaire pour décrire une fonction qui prend en charge le curry. À savoir, nous pouvons ajouter l'attribut curry: true à FunctionDeclaration .


AST pour la fonction de curry ( image en taille réelle )

En fait, nous avons maintenant un plan. Traitons de sa mise en Ĺ“uvre.

Si vous recherchez dans le code le mot FunctionDeclaration , vous pouvez accéder à la fonction parseFunction , qui est déclarée dans packages / babel-parser / src / parser / statement.js . Ici vous pouvez trouver la ligne où l'attribut generator est défini. Ajoutez une autre ligne au code:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  // ...  parseFunction<T: N.NormalFunction>(    node: T,    statement?: number = FUNC_NO_FLAGS,    isAsync?: boolean = false  ): T {    // ...    node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);  } } 

Si nous refaisons le test, une agréable surprise nous attendra. Le code est testé avec succès!

 PASS packages/babel-parser/test/curry-function.js  curry function syntax    âś“ should parse (12ms) 

C'est tout? Qu'avons-nous fait pour que le test réussisse miraculeusement?

Pour le savoir, parlons du fonctionnement de l'analyse. Au cours de cette conversation, j'espère que vous comprendrez comment la ligne node.curry = this.eat(tt.atat); .

Ă€ suivre ...

Chers lecteurs! Utilisez-vous babel?


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


All Articles