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

Hoje, estamos publicando a segunda parte de uma tradução da extensão de sintaxe JavaScript usando Babel.



→ Dizzying primeira parte

Como a análise funciona


O analisador recebe uma lista de tokens do sistema de tokenização de código e, examinando os tokens um de cada vez, cria um AST. Para tomar uma decisão sobre como usar tokens e entender qual token pode ser esperado a seguir, o analisador se refere à especificação da gramática do idioma.

A especificação gramatical se parece com isso:

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

Ele descreve a prioridade da execução de expressões ou instruções. Por exemplo, uma expressão AdditiveExpression pode representar uma das seguintes construções:

  • Expressão MultiplicativeExpression .
  • Uma expressão AdditiveExpression , seguida por um operador + token, seguido por uma expressão MultiplicativeExpression .
  • Uma expressão AdditiveExpression , seguida por um token “ - ”, seguido por uma expressão MultiplicativeExpression .

Como resultado, se tivermos a expressão 1 + 2 * 3 , ela ficará assim:

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

Mas não será assim:

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

O programa, usando essas regras, é convertido em código emitido pelo analisador:

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

Observe que aqui está uma versão extremamente simplificada do que realmente está presente no Babel. Mas espero que esse pedaço de código permita ilustrar a essência do que está acontecendo.

Como você pode ver, o analisador é, por natureza, recursivo. Ele passa dos designs de menor prioridade para os de maior prioridade. Por exemplo, parseAdditiveExpression chama parseMultiplicativeExpression , e essa construção chama parseExponentiationExpression e assim por diante. Esse processo recursivo é chamado de Análise de Descida Recursiva .

Funções this.eat, this.match, this.next


Você deve ter notado que nos exemplos anteriores algumas funções auxiliares foram usadas, como this.eat , this.match , this.next e outras. Essas são as funções internas do analisador Babel. Essas funções, no entanto, não são exclusivas de Babel; elas geralmente estão presentes em outros analisadores.

  • A função this.match retorna um valor booleano indicando se o token atual atende à condição especificada.
  • A função this.next na lista de tokens para o próximo token.
  • A função this.eat retorna o mesmo que a função this.match e, se this.match retornar true , o this.eat executará, antes de retornar true , uma chamada para this.next .
  • A função this.lookahead permite obter o próximo token sem avançar, o que ajuda a tomar uma decisão no nó atual.

Se você olhar novamente para o código do analisador que alteramos, você descobrirá que a leitura se tornou muito mais fácil:

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

Eu sei que não me aprofundei em explicar os recursos dos analisadores. Portanto, aqui e ali - alguns recursos úteis sobre este tópico. Eu aprendi muitos deles e posso recomendar a você.

Você pode estar interessado em aprender sobre como consegui visualizar a sintaxe que criei no Babel AST Explorer quando mostrei o novo atributo " curry " que apareceu no AST.

Isso se tornou possível devido ao fato de eu ter adicionado um novo recurso no Babel AST Explorer que permite carregar seu próprio analisador nesta ferramenta de pesquisa AST.

Se você seguir o caminho packages/babel-parser/lib , poderá encontrar uma versão compilada do analisador e um mapa de código. No painel Babel AST Explorer , você pode ver o botão para carregar seu próprio analisador. Ao fazer o download de packages/babel-parser/lib/index.js é possível visualizar o AST gerado usando seu próprio analisador.


Visualização AST

Nosso plugin para Babel


Agora que o analisador está completo, vamos escrever um plugin para Babel.

Mas, talvez agora você tenha algumas dúvidas sobre como exatamente usaremos nosso próprio analisador Babel, especialmente considerando a pilha de tecnologia que usamos para criar o projeto.

É verdade que não há nada a temer. O plug-in Babel pode fornecer recursos de analisador. Documentação relacionada pode ser encontrada no site da 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);    },  }; } 

Desde que criamos uma bifurcação do analisador Babel, isso significa que todos os recursos existentes do analisador, bem como os plug-ins internos, continuarão a funcionar completamente bem.

Depois de nos livrarmos dessas dúvidas, vamos dar uma olhada em como criar uma função que suporte o curry.

Se você não conseguiu suportar as expectativas e já tentou adicionar nosso plug-in ao sistema de construção do projeto, pode observar que as funções que suportam currying são compiladas em funções regulares.

Isso acontece porque, após analisar e transformar o código, Babel usa @babel/generator para gerar código a partir do AST transformado. Como o @babel/generator não sabe nada sobre o novo atributo curry , ele simplesmente o ignora.

Se algum dia as funções que suportam curry forem para o padrão JavaScript, convém fazer um PR para adicionar um novo código aqui .

Para fazer com que a função ofereça curry, você pode envolvê-la em uma função de ordem superior currying :

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

Se você está interessado nos recursos da implementação do mecanismo de funções de curry no JS - dê uma olhada neste material.

Como resultado, nós, transformando uma função que suporta currying, podemos fazer o seguinte:

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

Por enquanto, não veremos o mecanismo de criação de funções no JavaScript, que permite chamar a função foo antes de ser definida.

Aqui está a aparência do código de transformação:

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

Será muito mais fácil descobrir se você ler este material sobre transformações em Babel.

Agora nos deparamos com a questão de como fornecer a esse mecanismo acesso à função de currying . Aqui você pode usar uma das duas abordagens.

Abordagem nº 1: pode-se supor que a função de curry seja declarada no escopo global


Nesse caso, o trabalho já está concluído.

Se, ao executar o código compilado, a função currying não estiver definida, encontraremos uma mensagem de erro parecida com " currying is not defined ". É muito semelhante à mensagem " regeneratorRuntime não está definido ".

Portanto, se alguém usar o babel-plugin-transformation-curry-function , talvez seja necessário informá-lo de que ele precisa instalar o polyfill de currying para garantir que esse plug-in funcione corretamente.

▍ Abordagem # 2: você pode usar babel / helpers


Você pode adicionar uma nova função auxiliar ao @babel/helpers . É improvável que esse desenvolvimento seja combinado com o @babel/helpers oficial @babel/helpers . Como resultado, você terá que encontrar uma maneira de mostrar ao @babel/core a localização do seu @babel/helpers :

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

Eu não tentei isso sozinho, mas acredito que esse mecanismo funcionará. Se você tentar e tiver problemas, discutirei com prazer.

@babel/helpers nova função auxiliar ao @babel/helpers muito simples.

Primeiro, vá para o arquivo packages / babel-helpers / src / helpers.js e adicione uma nova entrada:

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

Ao descrever uma função auxiliar, a versão requerida @babel/core indicada. Algumas dificuldades aqui podem ser causadas pelo export default função de currying .

Para usar uma função auxiliar, basta chamar 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),      ])    ),  ]) ); 

O comando this.addHelper , se necessário, incorporará a função auxiliar na parte superior do arquivo e retornará um Identifier indicando a função implementada.

Anotações


Estou envolvido no trabalho em Babel há algum tempo, mas ainda não precisei adicionar recursos para dar suporte à nova sintaxe JavaScript do analisador. Eu trabalhei principalmente na correção de bugs e na melhoria do que é relevante para os recursos do idioma oficial.

No entanto, há algum tempo eu estava ocupado com a idéia de adicionar novas construções de sintaxe ao idioma. Como resultado, eu decidi escrever material sobre isso e experimentar. É incrivelmente bom ver que tudo funciona exatamente como o esperado.

A capacidade de controlar a sintaxe do idioma que você usa é uma poderosa fonte de inspiração. Isso possibilita, ao implementar algumas construções complexas, escrever menos código ou escrever um código mais simples do que antes. Os mecanismos para transformar código simples em construções complexas são automatizados e transferidos para o estágio de compilação. Isso é uma reminiscência de como o async/await resolve os problemas de retornos infernais e longas cadeias de promessas.

Sumário


Aqui falamos sobre como modificar os recursos do analisador Babel, escrevemos nosso próprio plug-in de transformação de código, falamos brevemente sobre o @babel/generator e a criação de funções auxiliares usando o @babel/helpers . Informações sobre a transformação do código são fornecidas apenas esquematicamente. Leia mais sobre eles aqui .

No processo, abordamos alguns recursos dos analisadores. Se você está interessado neste tópico - então, aqui e ali - recursos que são úteis para você.

A sequência de ações que realizamos é muito semelhante a parte do processo que é realizado quando um novo recurso JavaScript é enviado ao TC39. Aqui está a página do repositório do TC39, onde você pode encontrar informações sobre as ofertas atuais. Aqui você pode encontrar informações mais detalhadas sobre como trabalhar com ofertas semelhantes. Ao propor um novo recurso JavaScript, quem o oferece normalmente escreve polyfills ou, ao bifurcar Babel, prepara uma demonstração que prova que a frase funciona. Como você pode ver, criar uma bifurcação de um analisador ou gravar um polyfill não é a parte mais difícil do processo de propor novos recursos de JS. É difícil determinar a área de inovação, planejar e pensar em opções para seu uso e casos limítrofes; É difícil reunir as opiniões e sugestões dos membros da comunidade de programadores JavaScript Portanto, gostaria de expressar minha gratidão a todos aqueles que encontrarem forças para oferecer novos recursos de JavaScript ao TC39, desenvolvendo assim essa linguagem.

Aqui está uma página no GitHub que permitirá que você veja o panorama geral do que fizemos aqui.

Caros leitores! Você sempre quis estender a sintaxe do JavaScript?


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


All Articles