Crie construções de sintaxe JavaScript personalizadas usando Babel. Parte 1

Hoje publicamos a primeira parte da tradução do material, que é dedicada à criação de suas próprias construções de sintaxe para JavaScript usando Babel.



Revisão


Primeiro, vamos dar uma olhada no que alcançaremos quando chegarmos ao final deste material:

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

Vamos implementar a sintaxe @@ que permite funções de curry . Essa sintaxe é semelhante à usada para criar funções geradoras , mas, no nosso caso, em vez do sinal * , uma sequência de caracteres @@ é colocada entre a palavra-chave da function e o nome da function . Como resultado, ao declarar funções, você pode usar uma construção da function @@ name(arg1, arg2) formulário function @@ name(arg1, arg2) .

No exemplo acima, ao trabalhar com a função foo , você pode usar sua aplicação parcial . Chamar a função foo com a passagem de tantos parâmetros que é menor que o número de argumentos necessários, retornará uma nova função que pode receber os argumentos restantes:

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

Eu escolhi a sequência de caracteres @@ porque o símbolo @ não pode ser usado em nomes de variáveis. Isso significa que uma construção da function@@foo(){} formulário function@@foo(){} também estará sintaticamente correta. Além disso, o "operador" @ é usado para funções do decorador , e eu queria usar algo completamente novo. Como resultado, escolhi a construção @@ .

Para atingir nosso objetivo, precisamos executar as seguintes ações:

  • Crie uma bifurcação do analisador Babel.
  • Crie seu próprio plug-in Babel para transformação de código.

Parece algo impossível?
De fato, não há nada terrível aqui, analisaremos tudo em detalhes juntos. Espero que, quando você ler isso, domine com maestria os meandros de Babel.

Criando um garfo Babel


Vá para o repositório Babel no GitHub e clique no botão Fork , localizado no canto superior esquerdo da página.


Criando um fork de Babel ( imagem em tamanho real )

E, a propósito, se você acabou de criar a bifurcação do popular projeto de código aberto pela primeira vez - parabéns!

Agora clone o garfo Babel no seu computador e prepare-o para o trabalho .

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

Agora, deixe-me falar brevemente sobre a organização do repositório Babel.

Babel usa um monorepositório. Todos os pacotes (por exemplo, @babel/core , @babel/parser , @babel/plugin-transform-react-jsx e assim por diante) estão localizados na pasta packages/ . É assim:

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

Percebo que Babel usa um Makefile para automatizar tarefas. Ao criar um projeto pelo make build , o Gulp é usado como gerenciador de tarefas.

Curso de Conversão de Código para AST


Se você não estiver familiarizado com conceitos como "analisador" e "Árvore de sintaxe abstrata" (AST), antes de continuar lendo, eu recomendo que você dê uma olhada neste material.

Se você falar muito brevemente sobre o que acontece ao analisar (analisar) o código, você obtém o seguinte:

  • O código apresentado como uma string (tipo string ) se parece com uma longa lista de caracteres: f, u, n, c, t, i, o, n, , @, @, f, ...
  • No início, Babel realiza tokenização de código. Nesta etapa, Babel verifica o código e cria tokens. Por exemplo, algo como function, @@, foo, (, a, ...
  • Em seguida, os tokens são passados ​​pelo analisador para análise. Aqui, Babel, com base na especificação da linguagem JavaScript, cria uma árvore de sintaxe abstrata.

Aqui está um ótimo recurso para quem deseja aprender mais sobre compiladores.

Se você acha que o "compilador" é algo muito complexo e incompreensível, saiba que, na realidade, tudo não é tão misterioso. Compilação é simplesmente analisar o código e criar um novo código em sua base, que chamaremos de XXX. O código XXX pode ser representado por código de máquina (talvez, o código de máquina seja o que primeiro aparece na mente de muitos de nós quando pensamos no compilador). Esse código pode ser compatível com navegadores herdados. Na verdade, uma das principais funções do Babel é a compilação do código JS moderno em código compreensível para navegadores desatualizados.

Desenvolvendo seu próprio analisador para Babel


Vamos trabalhar na pasta packages/babel-parser/ :

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

Já falamos sobre tokenização e análise. Você pode encontrar o código que implementa esses processos em pastas com os nomes correspondentes. A pasta plugins/ plug-ins contém plug-ins (plug-ins) que expandem os recursos do analisador de base e adicionam suporte ao sistema para sintaxes adicionais. É exatamente assim que, por exemplo, o suporte a jsx e flow é implementado.

Vamos resolver nosso problema usando a tecnologia de desenvolvimento através de testes (desenvolvimento orientado a testes , TDD). Na minha opinião, é mais fácil escrever um teste primeiro e depois, gradualmente trabalhando no sistema, executar esse teste sem erros. Essa abordagem é especialmente boa ao trabalhar em uma base de código desconhecida. O TDD facilita a compreensão de onde você precisa fazer alterações no código para implementar a funcionalidade pretendida.

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

Você pode executar o teste do babel-parser seguinte maneira: TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only . Isso permitirá que você veja os erros:

 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) 

Se você achar que visualizar todos os testes leva muito tempo, é possível, para executar o teste desejado, chamar diretamente o jest :

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

Nosso analisador descobriu 2 tokens, aparentemente completamente inocentes, onde eles não deveriam estar.

Como eu sabia disso? A resposta a esta pergunta nos ajudará a encontrar o uso do modo de monitoramento de código iniciado pelo make watch .

Observar a pilha de chamadas nos leva a packages / babel-parser / src / parser / expression.js , onde a exceção this.unexpected() lançada.

Adicione alguns comandos de log neste arquivo:

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

Como você pode ver, os dois tokens são @ :

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

Como descobri que as construções this.state.type e this.lookahead().type me fornecerão os tokens atuais e os próximos?
Vou falar sobre isso na seção deste material dedicada às funções this.eat , this.match e this.next .

Antes de prosseguir, vamos resumir:

  • Escrevemos um teste para o babel-parser .
  • Executamos o teste usando make test-only .
  • Usamos o modo de monitoramento de código usando make watch .
  • Aprendemos sobre o status do analisador e this.state.type informações sobre o tipo do token atual ( this.state.type ) no console.

E agora garantiremos que 2 caracteres @ não sejam percebidos como tokens separados, mas como um novo token @@ , o que decidimos usar para currying de funções.

Novo token: "@@"


Primeiro, vamos ver onde os tipos de tokens são determinados. Este é o arquivo packages / babel-parser / src / tokenizer / types.js .

Aqui você pode encontrar uma lista de tokens. Adicione aqui a definição do novo token de atat :

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

Agora, vamos procurar o lugar no código onde, no processo de tokenização, os tokens são criados. Encontrar a sequência de caracteres tt.at em babel-parser/src/tokenizer nos leva ao arquivo: packages / babel-parser / src / tokenizer / index.js . No babel-parser tipos de token são importados como tt .

Agora, se depois que o símbolo @ atual vier com outro @ , crie um novo token tt.atat vez do 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;    // ...  } } 

Se você executar o teste novamente, notará que as informações sobre os tokens atuais e os próximos foram alteradas:

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

Já parece muito bom. Continuaremos o trabalho.

Novo analisador


Antes de prosseguir, veja como as funções do gerador são representadas no AST.


AST para função de gerador ( imagem em tamanho real )

Como você pode ver, o atributo generator: true da entidade FunctionDeclaration indica que essa é uma FunctionDeclaration gerador.

Podemos adotar uma abordagem semelhante para descrever uma função que suporta currying. Ou seja, podemos adicionar o atributo curry: true a FunctionDeclaration .


AST para função de curry ( imagem em tamanho real )

Na verdade, agora temos um plano. Vamos lidar com sua implementação.

Se você procurar no código a palavra FunctionDeclaration , poderá acessar a função parseFunction , declarada em packages / babel-parser / src / parser / statement.js . Aqui você pode encontrar a linha em que o atributo do generator está definido. Adicione outra linha ao código:

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

Se executarmos o teste novamente, uma agradável surpresa nos aguardará. O código foi testado com sucesso!

 PASS packages/babel-parser/test/curry-function.js  curry function syntaxshould parse (12ms) 

Isso é tudo? O que fizemos para fazer o teste milagrosamente passar?

Para descobrir, vamos falar sobre como a análise funciona. No decorrer desta conversa, espero que você entenda como a linha node.curry = this.eat(tt.atat); .

Para continuar ...

Caros leitores! Você usa babel?


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


All Articles