Aplicação prática da transformação de árvore AST usando Putout como exemplo

1. Introdução


Todos os dias, ao trabalhar no código, no caminho para implementar uma funcionalidade útil para o usuário, tornam-se mudanças forçadas (inevitáveis ​​ou simplesmente desejáveis) no código. Isso pode ser refatoração, atualização de uma biblioteca ou estrutura para uma nova versão principal, atualização da sintaxe JavaScript (o que não é incomum recentemente). Mesmo que a biblioteca faça parte de um projeto em funcionamento, a mudança é inevitável. A maioria dessas mudanças é rotineira. Não há nada de interessante para o desenvolvedor neles, por um lado, por outro lado, não traz nada para os negócios e, por outro lado, durante o processo de atualização, você precisa ter muito cuidado para não quebrar a lenha e não quebrar a funcionalidade. Assim, chegamos à conclusão de que é melhor mudar essa rotina para os ombros dos programas para que eles façam tudo sozinhos e a pessoa, por sua vez, controle se tudo foi feito corretamente. Isso é o que será discutido no artigo de hoje.


AST


Para o processamento do código do programa, é necessário convertê-lo em uma representação especial, com a qual seria conveniente que os programas funcionassem. Essa representação existe, é chamada Árvore de sintaxe abstrata (AST).
Para obtê-lo, use analisadores. O AST resultante pode ser transformado como você quiser e, para salvar o resultado, você precisa de um gerador de código. Vamos considerar com mais detalhes cada uma das etapas. Vamos começar com o analisador.


Analisador


E assim temos o código:


a + b 

Os analisadores geralmente são divididos em duas partes:


  • Análise lexical

Divide o código em tokens, cada um dos quais descreve uma parte do código:


 [{ "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "+", }, { "type": "Identifier", "value": "b" }] 

  • Análise

Constrói uma árvore de sintaxe a partir de tokens:


 { "type": "BinaryExpression", "left": { "type": "Identifier", "name": "a" }, "operator": "+", "right": { "type": "Identifier", "name": "b" } } 

E agora já temos essa ideia, com a qual você pode trabalhar programaticamente. Vale esclarecer que há um grande número de analisadores de JavaScript , eis alguns deles:


  • babel-parser - um analisador que usa babel ;
  • espree - um analisador que usa eslint ;
  • bolota - o analisador no qual as duas anteriores se baseiam;
  • esprima - um analisador popular que suporta JavaScript até o EcmaScript 2017;
  • cherow é um novo player entre os analisadores JavaScript que afirma ser o mais rápido;

Existe um analisador JavaScript padrão, chamado ESTree e define quais nós devem ser analisados.
Para uma análise mais detalhada do processo de implementação do analisador (assim como do transformador e gerador), você pode ler o compilador super minúsculo .


Transformador


Para transformar a árvore AST, você pode usar o padrão Visitor , por exemplo, usando a biblioteca @ babel / traverse . O código a seguir exibirá os nomes de todos os identificadores de código JavaScript da variável de code .


 import * as parser from "@babel/parser"; import traverse from "@babel/traverse"; const code = `function square(n) { return n * n; }`; const ast = parser.parse(code); traverse(ast, { Identifier(path) { console.log(path.node.name); } }); 

Gerador


Você pode gerar código, por exemplo, usando @ babel / generator , desta maneira:


 import {parse} from '@babel/parser'; import generate from '@babel/generator'; const code = 'class Example {}'; const ast = parse(code); const output = generate(ast, code); 

Portanto, nesse estágio, o leitor deve ter uma idéia básica do que é necessário para transformar o código JavaScript e com quais ferramentas ele é implementado.


Também vale a pena adicionar uma ferramenta on-line, como o astexplorer , que combina um grande número de analisadores, transformadores e geradores.


Putout


O Putout é um transformador de código ativado por plug-in. De fato, é um cruzamento entre eslint e babel , combinando as vantagens de ambas as ferramentas.


Como o eslint putout mostra áreas problemáticas no código, mas diferente do eslint putout altera o comportamento do código, ou seja, ele é capaz de corrigir todos os erros que pode encontrar.


Como o babel putout converte o código, mas tenta alterá-lo minimamente, para que possa ser usado para trabalhar com o código armazenado no repositório.


Vale ressaltar também que é mais bonito , é uma ferramenta de formatação e difere radicalmente.


O Jscodeshift não está muito longe do putout , mas não suporta plugins, não mostra mensagens de erro e também usa ast-types em vez de @ babel / types .


História de aparência


No processo, o eslint me ajuda muito com minhas dicas. Mas às vezes eu quero mais dele. Por exemplo, para remover o depurador , corrija test.only e também exclua variáveis ​​não utilizadas. O último ponto formou a base da putout , durante o processo de desenvolvimento, ficou claro que é muito difícil e muitas outras transformações são muito mais fáceis de implementar. Assim, a putout cresceu suavemente de uma função para o sistema de plugins. Remover variáveis ​​não utilizadas agora é o processo mais difícil, mas isso não nos impede de desenvolver e apoiar muitas outras transformações igualmente úteis.


Como o Putout funciona por dentro


putout trabalho de putout pode ser dividido em duas partes: motor e plugins. Essa arquitetura permite que você não se distraia com as transformações ao trabalhar com o mecanismo e, ao trabalhar em plug-ins, você se concentrará o máximo possível em sua finalidade.


Plugins incorporados


putout é construído em um sistema de plugins. Cada plug-in representa uma regra. Usando as regras internas, você pode fazer o seguinte:


  • Encontre e exclua:


    • variáveis ​​não utilizadas
    • debugger
    • chamar test.only
    • chamar test.skip
    • chamar console.log
    • chamar process.exit
    • blocos vazios
    • padrões vazios

  • Encontre e divida a declaração de variável:


     //  var one, two; //  var one; var two; 

  • Converta esm em commonjs :



  //  import one from 'one'; //  const one = require('one'); 

  • Aplique a desestruturação:

 //  const name = user.name; //  const {name} = user; 

  1. Combine propriedades de desestruturação:

 //  const {name} = user; const {password} = user; //  const { name, password } = user; 

Cada plugin é construído de acordo com a Filosofia Unix , ou seja, é o mais simples possível, cada um executa uma ação, facilitando sua combinação, porque são, em essência, filtros.


Por exemplo, com o seguinte código:


 const name = user.name; const password = user.password; 

É primeiro convertido usando a aplicação de destruição para:


 const {name} = user; const {password} = user; 

Em seguida, usando as propriedades de destruição da destruição, ele é convertido em:


 const { name, password } = user; 

Assim, os plugins podem funcionar separadamente e juntos. Ao criar seus próprios plug-ins, é recomendável aderir a esta regra e implementar um plug-in com funcionalidade mínima que faça apenas o que você precisa, e os plug-ins internos e personalizados cuidarão do resto.


Exemplo de uso


Depois de nos familiarizarmos com as regras internas, podemos considerar um exemplo usando o putout .
Crie um arquivo example.js com o seguinte conteúdo:


 const x = 1, y = 2; const name = user.name; const password = user.password; console.log(name, password); 

Agora execute o putout passando o example.js como argumento:


 coderaiser@cloudcmd:~/example$ putout example.js /home/coderaiser/example/example.js 1:6 error "x" is defined but never used remove-unused-variables 1:13 error "y" is defined but never used remove-unused-variables 6:0 error Unexpected "console" call remove-console 1:0 error variables should be declared separately split-variable-declarations 3:6 error Object destructuring should be used apply-destructuring 4:6 error Object destructuring should be used apply-destructuring 6 errors in 1 files fixable with the `--fix` option 

Receberemos informações contendo 6 erros, considerados com mais detalhes acima, agora iremos corrigi-los e ver o que aconteceu:


 coderaiser@cloudcmd:~/example$ putout example.js --fix coderaiser@cloudcmd:~/example$ cat example.js const { name, password } = user; 

Como resultado da correção, as variáveis ​​não utilizadas e as chamadas console.log foram excluídas e a desestruturação também foi aplicada.


Configurações


As configurações padrão podem nem sempre e nem para todos; portanto, o putout suporta o arquivo de configuração .putout.json , que consiste nas seguintes seções:


  • Regras
  • Ignorar
  • Correspondência
  • Plugins

Regras

A seção de rules contém um sistema de regras. As regras, por padrão, são definidas da seguinte maneira:


 { "rules": { "remove-unused-variables": true, "remove-debugger": true, "remove-only": true, "remove-skip": true, "remove-process-exit": false, "remove-console": true, "split-variable-declarations": true, "remove-empty": true, "remove-empty-pattern": true, "convert-esm-to-commonjs": false, "apply-destructuring": true, "merge-destructuring-properties": true } } 

Para habilitar remove-process-exit defina-o como true no arquivo .putout.json :


 { "rules": { "remove-process-exit": true } } 

Isso será suficiente para relatar todas as chamadas process.exit encontradas no código e excluí-las se a opção --fix for --fix .


Ignorar

Se você precisar adicionar algumas pastas à lista de exceções, basta adicionar a seção ignore :


 { "ignore": [ "test/fixture" ] } 

Correspondência

Se você precisar de um sistema de regras ramificado, por exemplo, habilite process.exit para o diretório bin , basta usar a seção de match :


 { "match": { "bin": { "remove-process-exit": true, } } } 

Plugins

Se você usa plug-ins que não estão embutidos e tem o prefixo putout-plugin- , inclua-os na seção de plugins - plugins antes de ativá-los na seção de rules . Por exemplo, para conectar o putout-plugin-add-hello-world e ativar a regra add-hello-world , basta especificar:


 { "rules": { "add-hello-world": true }, "plugins": [ "add-hello-world" ] } 

Motor de retirada


O mecanismo de putout é uma ferramenta de linha de comando que lê configurações, analisa arquivos, carrega e executa plugins e depois grava o resultado dos plugins.


Ele usa a biblioteca de reformulação , que ajuda a executar uma tarefa muito importante: após a análise e transformação, colete o código em um estado o mais semelhante possível ao anterior.


Para a análise, é usado um analisador compatível com ESTree (o babel está atualmente com o estree in estree , mas são possíveis alterações no futuro) e as ferramentas do babel são usadas para transformação. Por que exatamente babel ? Tudo é simples. O fato é que este é um produto muito popular, muito mais popular que outras ferramentas similares, e está se desenvolvendo muito mais rapidamente. Cada nova proposta no padrão EcmaScript não é completa sem um plug-in babel . Babel também possui um livro, Babel Handbook , que descreve muito bem todos os recursos e ferramentas para atravessar e transformar uma árvore AST.


Plug-in personalizado para Putout


O sistema de plug- ins de distribuição é bastante simples e muito semelhante aos plugins eslint , bem como aos plugins babel . É verdade que, em vez de uma função, o putout plugin deve exportar 3. Isso é feito para aumentar a reutilização do código, porque duplicar a funcionalidade em 3 funções não é muito conveniente, é muito mais fácil colocá-lo em funções separadas e simplesmente chamá-lo nos lugares certos.


Estrutura de plugins

O plugin Putout consiste em 3 funções:


  • report - retorna uma mensagem;
  • find - procura por lugares com erros e os retorna;
  • fix - conserta esses locais;

O ponto principal a ser lembrado ao criar um plug-in para o putout é seu nome, ele deve começar com putout-plugin- . Em seguida, pode ser o nome da operação que o plug-in executa, por exemplo, o plug remove-wrong in remove-wrong deve ser chamado assim: putout-plugin-remove-wrong .


Você também deve adicionar as palavras: putout e putout-plugin à seção package.json na seção keywords , e especificar "putout": ">=3.10" em peerDependencies "putout": ">=3.10" ou a versão que será a última no momento da criação do plug-in.


Exemplo de plugin para Putout

Vamos escrever um exemplo de plug-in que removerá a palavra debugger do código. Esse plug-in já existe, é @ putout / plugin-remove-debugger e é simples o suficiente para considerá-lo agora.


É assim:


 //        module.exports.report = () => 'Unexpected "debugger" statement'; //     ,  debugger    Visitor module.exports.find = (ast, {traverse}) => { const places = []; traverse(ast, { DebuggerStatement(path) { places.push(path); } }); return places; }; //  ,     module.exports.fix = (path) => { path.remove(); }; 

Se a regra remove-debugger estiver incluída em .putout.json , o @putout/plugin-remove-debugger será carregado. Primeiro, a função find é chamada, que, usando a função traverse , irá ignorar os nós da árvore AST e salvar todos os locais necessários.


O próximo passo da apresentação será report para obter a mensagem desejada.


Se o sinalizador --fix for usado, a função de fix do plug-in será chamada e a transformação será executada; nesse caso, o nó será excluído.


Exemplo de teste de plug-in

Para simplificar o teste de plug-ins, a ferramenta @ putout / test foi escrita. Na sua essência, nada mais é do que um invólucro sobre fita , com vários métodos para a conveniência e simplificação dos testes.


O teste para o plug remove-debugger in remove-debugger pode ser assim:


 const removeDebugger = require('..'); const test = require('@putout/test')(__dirname, { 'remove-debugger': removeDebugger, }); //        test('remove debugger: report', (t) => { t.reportCode('debugger', 'Unexpected "debugger" statement'); t.end(); }); //    test('remove debugger: transformCode', (t) => { t.transformCode('debugger', ''); t.end(); }); 

Codemods

Nem todas as transformações precisam ser usadas todos os dias; para transformações npm basta fazer a mesma coisa, mas, em vez de publicar para npm coloque-a na ~/.putout . Na inicialização, o putout procurará nesta pasta, pegará e iniciará a transformação.


Aqui está um exemplo de transformação que substitui a conexão de tape e tentativa de fita por uma chamada de supertape : convert-tape-to-supertape .


eslint-plugin-putout


No final, vale acrescentar um ponto: o putout tenta alterar o código minimamente, mas se acontecer a um amigo que algumas regras de formatação quebram, o eslint --fix está sempre pronto para eslint --fix e, para esse fim, existe um plug-in eslint-plugin-putout especial . Ele pode alegrar muitos erros de formatação e, é claro, pode ser personalizado de acordo com as preferências dos desenvolvedores em um projeto específico. Conectar é fácil:


 { "extends": [ "plugin:putout/recommended", ], "plugins": [ "putout" ] } 

Até o momento, existe apenas uma regra: one-line-destructuring , faz o seguinte:


 //  const { one } = hello; //  const {one} = hello; 

Existem muitas outras regras de eslint incluídas, com as quais você pode se familiarizar com mais detalhes .


Conclusão


Quero agradecer ao leitor pela atenção prestada a este texto. Espero sinceramente que o tópico das transformações AST se torne mais popular e os artigos sobre esse fascinante processo apareçam com mais frequência. Eu ficaria muito grato por quaisquer comentários e sugestões relacionados ao desenvolvimento posterior da putout . Crie um problema , envie um pool de solicitações , teste, escreva quais regras você gostaria de ver e como programar seu código programaticamente, trabalharemos juntos para melhorar a ferramenta de transformação AST.

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


All Articles