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:
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" }]
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 .
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:
- Aplique a desestruturação:
- Combine propriedades de desestruturação:
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:
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, });
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:
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.