Trabalhar com árvores abstratas de sintaxe JavaScript

Por que analisar seu código? Por exemplo, para encontrar o console.log esquecido antes de confirmar. Mas e se você precisar alterar a assinatura da função em centenas de entradas no código? Expressões regulares lidarão aqui? Este artigo mostrará quais possibilidades as árvores de sintaxe abstrata oferecem a um desenvolvedor.



Abaixo do corte - uma transcrição em vídeo e texto de um relatório de Kirill Cherkashin ( z6Dabrata ) da conferência HolyJS 2018 Piter .


Sobre o autor
Cyril nasceu em Moscou, agora vive em Nova York e trabalha na Firebase. Ensina Angular não apenas no Google, mas em todo o mundo. O organizador da maior mitap angular do mundo é o AngularNYC (assim como o VueNYC e o ReactNYC). Nas horas vagas em programação, ele gosta de tango, livros e conversas agradáveis.

Serra ou madeira?


Vamos começar com um exemplo: digamos que você depurou um programa e enviou as alterações feitas no git, após o que você foi para a cama em silêncio. De manhã, seus colegas fizeram o download de suas alterações e, como você esqueceu de remover a saída de informações de depuração do console no dia anterior, ele as exibe e obstrui a saída. Muitos enfrentaram esse problema.

Existem ferramentas, como o EsLint , para corrigir a situação, mas, para fins educacionais, vamos tentar encontrar uma solução por conta própria.
Qual ferramenta devo usar para remover todos os console.log() do código?
Escolhemos entre expressões regulares e o uso de árvores Abstract Sitax (ASD). Vamos tentar resolver isso com expressões regulares escrevendo alguma função findConsoleLog . Na entrada, ele receberá o código do programa como argumento e será exibido true se console.log () for encontrado em algum lugar no texto do programa.

 function findConsoleLog(code) { return !!code.match(/console.log/); } 

Escrevi 17 testes, tentando encontrar maneiras diferentes de quebrar nossa função. Esta lista está longe de estar completa.



O teste mais simples passou.
E se alguma função contiver a string "console.log" em seu nome?

 function findConsoleLog(code) { return !!code.match(/\bconsole.log/); } 

Adicionado um caractere que indica que console.log deve ocorrer no início da palavra.



Somente dois testes foram aprovados, mas e se console.log estiver no comentário e não precisar ser excluído?

Nós o reescrevemos para que o analisador não toque nos comentários.

 function findConsoleLog(code) { return !!code   .replace(/\/\/.*/)   .match(/\bconsole.log/); } 



Excluímos a remoção do "console.log" das linhas:

 function findConsoleLog(code) { return !!code   .replace(/\/\/.*|'.*'/, '')   .match(/\bconsole.log/); } 



Não esqueça que ainda temos espaços e outros caracteres que podem impedir a passagem de alguns testes:



Apesar de a idéia não ser tão simples, todos os 17 testes usando expressões regulares podem ser aprovados. Aqui, neste caso, o código da solução será:

 function findConsoleLog(code) { return code   .replace(/\/\/.*|'.*?[^\\]'|".*?"|`[\s\S]*`|\/\*[\s\S]*\*\//)   .match(/\bconsole\s*.log\(/); } 


O problema é que esse código não cobre todos os casos possíveis e é bastante difícil mantê-lo.

Considere como resolver esse problema usando o ASD.

Como as árvores são cultivadas?


A árvore de sintaxe abstrata é obtida como resultado do analisador trabalhando com o código do seu aplicativo. O analisador @ babel / parser foi usado para demonstração .
Como exemplo, pegue a string console.log('holy') , passe-a pelo analisador.

 import { parse } from 'babylon'; parse("console.log('holy')"); 

Como resultado de seu trabalho, um arquivo JSON de cerca de 300 linhas é obtido. Excluímos de suas linhas numéricas informações de serviço. Estamos interessados ​​na seção do corpo. Meta-informação também não nos interessa. O resultado é de cerca de 100 linhas. Comparado à estrutura que o navegador gera para uma variável de corpo (cerca de 300 linhas), isso não é muito.

Vejamos alguns exemplos de como vários literais são representados no código em uma árvore de sintaxe:



Esta é uma expressão na qual existe Numeric Literal, um literal numérico.



A expressão já familiar do console.log. Tem um objeto que possui uma propriedade.



Se log for uma chamada de função, a descrição será a seguinte: existe uma expressão de chamada, ela possui argumentos - literais numéricos. Ao mesmo tempo, a expressão de chamada possui um nome - log.

Os literais podem ser diferentes: números, seqüências de caracteres, expressões regulares, booleano, nulo.
Voltar para a chamada console.log



Esta é uma expressão de chamada que possui Expressão de Membro dentro. A partir dele, fica claro que o objeto do console possui uma propriedade chamada log.

Desvio ASD


Agora vamos tentar trabalhar com essa estrutura no código. A biblioteca babel-atravessar será usada para atravessar a árvore .

Os mesmos 17 testes são dados. Esse código é obtido analisando a árvore de sintaxe do programa e pesquisando entradas do "console.log":

 function traverseConsoleLog(code, {babylon, babelTraverse, types, log}) { const ast = babylon.parse(code); let hasConsoleLog = false; babelTraverse(ast, {   MemberExpression(path){     if (       path.node.property.type === 'Identifier' &&       path.node.property.name === 'log' &&       path.node.object.type === 'Identifier' &&       path.node.object.name === 'console' &&       path.parent.type === 'CallExpression' &&       path.Parentkey === 'callee'     ) {       hasConsoleLog = true;     }   } }) return hasConsoleLog; } 

Vamos analisar o que está escrito aqui. const ast = babylon.parse(code); na variável ast, analisamos a árvore de sintaxe do código. Em seguida, fornecemos à biblioteca babel-parse essa árvore para processamento. Estamos procurando nós e propriedades com nomes correspondentes nas expressões de chamada. Defina a variável hasConsoleLog como true se a combinação necessária de nós e seus nomes for encontrada.

Podemos andar pela árvore, pegar os pais dos nós, descendentes, procurar quais argumentos e propriedades eles possuem, olhar os nomes dessas propriedades, tipos - isso é muito conveniente.

Há uma nuance desagradável que pode ser facilmente corrigida usando a biblioteca de tipos de babel. Para evitar erros ao pesquisar na árvore devido ao nome errado, por exemplo, em vez de path.parent.type === 'CallExpression' você acidentalmente escreveu path.parent.type === 'callExpression' , com tipos de babel você pode escrever assim :

 // Before path.node.property.type === 'Identifier' path.node.property.name === 'log' // with babel types import {isIdentifier} from 'babel-types'; isIdentifier(path.node.property, {name: log}) //         ,  ,    isIdentifier,      

Reescrevemos o código anterior usando babel-types:
 function traverseConsoleLogSolved2(code, {babylon, babelTraverse, types}) { const ast = babylon.parse(code); let hasConsoleLog = false; babelTraverse(ast, {   MemberExpression(path) {     if (       types.isIdentifier(path.node.object, { name: 'console'}) &&       types.isIdentifier(path.node.property, { name: 'log'}) &&       types.isCallExpression(path.parent) &&       path.parentKey === 'callee'     ) {       hasConsoleLog = true;     }   } }); return hasConsoleLog; } 

Transformar ASD usando babel-traverse


Para reduzir os custos de mão-de-obra, precisamos que o console.log imediatamente removido do código - em vez de um sinal de que ele está no código.

Como precisamos remover não a MemberExpression em si, mas seu pai, no lugar hasConsoleLog = true; escrevemos path.parentPath.remove(); .

Da função removeConsoleLog , ainda retornamos um valor booleano. Substituímos sua saída pelo código que irá gerar o gerador de babel, assim:
hasConsoleLog => babelGenerator(ast).code

Babel-generator recebe a árvore de sintaxe abstrata modificada como parâmetro, retorna um objeto com a propriedade code, dentro desse objeto é gerado o código regenerado sem console.log . A propósito, se quisermos obter um mapa de código, podemos chamar a propriedade sourceMaps para este objeto.

E se você precisar encontrar um depurador?


Desta vez, usaremos o ASTexplorer para concluir a tarefa. Depurador é um tipo de nó de instrução do depurador. Não precisamos olhar para toda a estrutura, já que esse é um tipo especial de nó, basta encontrar a instrução debugger. Escreveremos um plugin para o ESLint (no ASTexplorer).

O ASTexplorer foi projetado de maneira que você escreva o código à esquerda e à direita obtenha o ASD finalizado. Você pode escolher em qual formato deseja recebê-lo: JSON ou em formato de árvore.



Como usamos o ESLint, ele fará todo o trabalho de procurar arquivos para nós e nos fornecerá o arquivo necessário para que possamos encontrar a linha de depurador nele. Essa ferramenta usa um analisador ASD diferente. No entanto, existem vários tipos de ASDs em JavaScript. Algo que lembra o passado, quando diferentes navegadores implementaram a especificação de maneiras diferentes. Assim, implementamos a pesquisa do depurador:

 export default function(context) { return {   DebuggerStatement(node) { // ,     console.log    path,    -  ,     path         context.report(node, 'LOL Debugger!!!'); //   ESLint ,   debugger, node     ,    ,    debugger   } } } 

Verificando o trabalho de um plugin escrito:



Da mesma forma, você pode remover o depurador do código.

O que mais são ASD úteis


Pessoalmente, uso o ASD para simplificar o trabalho com estruturas Angular e outras estruturas front-end. Você pode importar, expandir, adicionar uma interface, método, decorador e qualquer outra coisa com o clique de um botão. Embora estejamos falando sobre Javascript, no entanto, o TypeScript também possui seus próprios ASDs, a única diferença é a diferença entre os nomes dos tipos de nós e a estrutura. No mesmo ASTExplorer pode ser selecionado como o idioma TypeScript.

Então:

  • Temos mais controle sobre o código, refatoração mais fácil, modos de código. Por exemplo, antes de confirmar, você pode pressionar uma única tecla para formatar o código inteiro de acordo com as diretrizes. Codemods implica correspondência automática de código de acordo com a versão exigida da estrutura.
  • Menos disputas sobre design de código.
  • Você pode criar projetos de jogos. Por exemplo, forneça automaticamente ao programador feedback sobre o código que ele escreve.
  • Melhor compreensão do JavaScript.

Alguns links úteis para Babel


  1. Todas as transformações do Babel usam esta API: plugins e presets .
  2. Parte do processo de adição de novas funcionalidades ao ECMAScript é a criação de um plug-in para o Babel. Isso é necessário para que as pessoas possam testar a nova funcionalidade. Se você seguir o link , poderá ver que da mesma maneira que os recursos do ASD são usados. Por exemplo, operador de atribuição lógica .
  3. O Babel Generator perde a formatação ao gerar código. Isso é parcialmente bom, porque, se essa ferramenta for usada na equipe de desenvolvimento, depois de gerar o código a partir do ASD, ela será a mesma para todos. Mas se você quiser manter sua formatação, poderá usar uma destas ferramentas: Reformulação ou Babel CodeMod .
  4. Nesse link, você pode encontrar muitas informações sobre Babel Awesome Babel .
  5. Babel é um projeto de código aberto e uma equipe de voluntários está trabalhando nele. Você pode ajudar. Há três maneiras de fazer isso: assistência financeira, você pode oferecer suporte ao site da patreon, com o qual Henry Zhu, um dos principais colaboradores da babel, trabalha, ajuda com o código em opencollective.com/babel .

Bônus


De que outra forma podemos encontrar nosso console.log no código? Use seu IDE! Usando a ferramenta de localização e substituição, depois de selecionar onde procurar o código.
A Intellij IDEA também possui uma ferramenta de "pesquisa estrutural" que pode ajudá-lo a encontrar os lugares certos no seu código, a propósito, ele usa um ASD.

De 24 a 25 de novembro, Kirill fará uma apresentação sobre dados binários JavaScript * LOVES * no Moscow HolyJS : desceremos para o nível de dados binários, cavaremos arquivos binários usando arquivos * .gif como exemplo e trataremos de estruturas de serialização como Protobuf ou Thrift. Após o relatório, será possível conversar com Cyril e discutir todas as questões de interesse na área de discussão.

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


All Articles