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.