Aplicación práctica de la transformación del árbol AST utilizando Putout como ejemplo

Introduccion


Todos los días, cuando se trabaja en el código, en la forma de implementar una función útil para el usuario, se hacen cambios forzados (inevitables o simplemente deseables) en el código. Esto puede ser refactorizar, actualizar una biblioteca o marco a una nueva versión principal, actualizar la sintaxis de JavaScript (que no es raro recientemente). Incluso si la biblioteca es parte de un proyecto en funcionamiento, el cambio es inevitable. La mayoría de estos cambios son rutinarios. No hay nada interesante para el desarrollador en ellos, por un lado, por otro, no aporta nada al negocio y, por otro, durante el proceso de actualización, debe tener mucho cuidado de no romper la leña y no romper la funcionalidad. Por lo tanto, llegamos a la conclusión de que es mejor cambiar esa rutina sobre los hombros de los programas para que hagan todo por sí mismos, y la persona, a su vez, controle si todo se hizo correctamente. Esto es lo que se discutirá en el artículo de hoy.


AST


Para el procesamiento del código del programa, es necesario traducirlo en una representación especial, con la cual sería conveniente que los programas funcionen. Tal representación existe, se llama Árbol de sintaxis abstracta (AST).
Para obtenerlo, use analizadores. El AST resultante se puede transformar a su gusto, y luego para guardar el resultado necesita un generador de código. Consideremos con más detalle cada uno de los pasos. Comencemos con el analizador.


Analizador


Y entonces tenemos el código:


a + b 

Los analizadores generalmente se dividen en dos partes:


  • Análisis léxico

Rompe el código en tokens, cada uno de los cuales describe una parte del código:


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

  • Analizando

Crea un árbol de sintaxis a partir de tokens:


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

Y ahora ya tenemos esa idea, con la que puede trabajar mediante programación. Vale la pena aclarar que hay una gran cantidad de analizadores JavaScript , estos son algunos de ellos:


  • babel-parser : un analizador sintáctico que usa babel ;
  • espree : un analizador sintáctico que usa eslint ;
  • bellota : el analizador en el que se basan los dos anteriores;
  • esprima : un analizador popular que admite JavaScript hasta EcmaScript 2017;
  • cherow es un nuevo jugador entre los analizadores de JavaScript que dice ser el más rápido;

Hay un analizador de JavaScript estándar, se llama ESTree y define qué nodos deben analizarse.
Para un análisis más detallado del proceso de implementación del analizador (así como el transformador y el generador), puede leer el compilador súper pequeño .


Transformador


Para transformar el árbol AST, puede usar el patrón Visitor , por ejemplo, usando la biblioteca @ babel / traverse . El siguiente código generará los nombres de todos los identificadores de código JavaScript de la variable 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); } }); 

Generador


Puede generar código, por ejemplo, usando @ babel / generator , de esta manera:


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

Y así, en esta etapa, el lector debería haber tenido una idea básica de lo que se necesita para transformar el código JavaScript y con qué herramientas se implementa.


También vale la pena agregar una herramienta en línea como astexplorer , que combina una gran cantidad de analizadores, transformadores y generadores.


Putout


Putout es un transformador de código habilitado para complementos. De hecho, es un cruce entre eslint y babel , que combina las ventajas de ambas herramientas.


Cómo eslint putout muestra áreas problemáticas en el código, pero a diferencia de eslint putout cambia el comportamiento del código, es decir, es capaz de corregir todos los errores que puede encontrar.


Al igual que babel putout convierte el código, pero trata de cambiarlo mínimamente, por lo que puede usarse para trabajar con código almacenado en el repositorio.


También vale la pena mencionar Prettier , es una herramienta de formateo y difiere radicalmente.


Jscodeshift se encuentra no muy lejos de la putout , pero no admite complementos, no muestra mensajes de error y también utiliza ast-types en lugar de @ babel / types .


Historia de la apariencia


En el proceso, eslint me ayuda mucho con mis consejos. Pero a veces quiero más de él. Por ejemplo, para eliminar el depurador , arregle test.only y también elimine las variables no utilizadas. El último punto formó la base de la putout , durante el proceso de desarrollo, quedó claro que no es fácil y muchas otras transformaciones son mucho más fáciles de implementar. Por lo tanto, la putout creció sin problemas de una función al sistema de complementos. Eliminar las variables no utilizadas es ahora el proceso más difícil, pero esto no nos impide desarrollar y soportar muchas otras transformaciones igualmente útiles.


Cómo funciona Putout adentro


putout trabajo de putout se puede dividir en dos partes: motor y complementos. Esta arquitectura le permite no distraerse con las transformaciones cuando trabaje con el motor, y cuando trabaje en complementos, se centrará lo más posible en su propósito.


Complementos incorporados


putout se basa en un sistema de complemento. Cada complemento representa una regla. Usando las reglas integradas, puede hacer lo siguiente:


  • Encuentra y elimina:


    • variables no utilizadas
    • debugger
    • llamada test.only
    • llamada test.skip
    • llame a console.log
    • process.exit llamada process.exit
    • bloques vacíos
    • patrones vacíos

  • Encontrar y dividir declaración de variable:


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

  • Convierte commonjs a commonjs :



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

  • Aplicar desestructuración:

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

  1. Combina propiedades de desestructuración:

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

Cada complemento se construye de acuerdo con la filosofía de Unix , es decir, son lo más simples posible, cada uno realiza una acción, lo que los hace fáciles de combinar, ya que son, en esencia, filtros.


Por ejemplo, tener el siguiente código:


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

Primero se convierte utilizando apply-destructuring para:


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

Luego, usando merge-destructuring-properties, se convierte a:


 const { name, password } = user; 

Por lo tanto, los complementos pueden funcionar tanto por separado como juntos. Al crear sus propios complementos, se recomienda cumplir con esta regla e implementar un complemento con una funcionalidad mínima, haciendo solo lo que necesita, y los complementos integrados y personalizados se encargarán del resto.


Ejemplo de uso


Después de familiarizarnos con las reglas integradas, podemos considerar un ejemplo usando putout .
Cree un archivo example.js con los siguientes contenidos:


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

Ahora ejecute putout pasando 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 

Recibiremos información que contiene 6 errores, considerados con más detalle anteriormente, ahora los corregiremos y veremos qué sucedió:


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

Como resultado de la corrección, se eliminaron las variables no utilizadas y las llamadas de console.log , y también se aplicó la desestructuración.


Configuraciones


La configuración predeterminada puede no siempre y no para todos, por lo que putout admite el archivo de configuración .putout.json , consta de las siguientes secciones:


  • Reglas
  • Ignorar
  • Partido
  • Complementos

Reglas

La sección de rules contiene un sistema de reglas. Las reglas, por defecto, se establecen de la siguiente manera:


 { "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 simplemente .putout.json true en el archivo .putout.json :


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

Esto será suficiente para informar todas las llamadas a process.exit encontradas en el código y eliminarlas si se --fix opción --fix .


Ignorar

Si necesita agregar algunas carpetas a la lista de excepciones, simplemente agregue la sección de ignore :


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

Partido

Si necesita un sistema ramificado de reglas, por ejemplo, habilite process.exit para el directorio bin , solo use la sección de match :


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

Complementos

Si utiliza complementos que no están integrados y tienen el prefijo putout-plugin- , debe incluirlos en la sección de plugins antes de activarlos en la sección de rules . Por ejemplo, para conectar el putout-plugin-add-hello-world y habilitar la regla add-hello-world , solo especifique:


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

Motor de apagado


El motor de putout es una herramienta de línea de comandos que lee configuraciones, analiza archivos, carga y ejecuta complementos, y luego escribe el resultado de los complementos.


Utiliza la biblioteca de refundición , que ayuda a llevar a cabo una tarea muy importante: después de analizar y transformar, recopilar el código en un estado lo más similar posible al anterior.


Para el análisis, se ESTree un analizador compatible con ESTree ( babel está actualmente con el complemento estree , pero los cambios son posibles en el futuro), y las herramientas de babel se utilizan para la transformación. ¿Por qué exactamente babel ? Todo es simple El hecho es que este es un producto muy popular, mucho más popular que otras herramientas similares, y se está desarrollando mucho más rápido. Cada nueva propuesta en el estándar EcmaScript no está completa sin un complemento de babel . Babel también tiene un libro, Babel Handbook , que describe muy bien todas las características y herramientas para atravesar y transformar un árbol AST.


Complemento personalizado para Putout


El sistema de complementos de putout es bastante simple y muy similar a los complementos de Eslint , así como a los complementos de Babel . Es cierto que en lugar de una función, putout plugin debería exportar 3. Esto se hace para aumentar la reutilización del código, ya que duplicar la funcionalidad en 3 funciones no es muy conveniente, es mucho más fácil ponerlo en funciones separadas y simplemente llamarlo en los lugares correctos.


Estructura del complemento

Por lo Putout complemento Putout consta de 3 funciones:


  • report : devuelve un mensaje;
  • find : busca lugares con errores y los devuelve;
  • fix - arregla estos lugares;

El punto principal a recordar al crear un complemento para putout es su nombre, debe comenzar con putout-plugin- . El siguiente puede ser el nombre de la operación que realiza el complemento, por ejemplo, el complemento remove-wrong debería llamarse así: putout-plugin-remove-wrong .


También debe agregar las palabras: putout y putout-plugin a la sección package.json en la sección de keywords y especificar "putout": ">=3.10" en peerDependencies "putout": ">=3.10" , o la versión que será la última en el momento de escribir el complemento.


Plugin de muestra para Putout

Escribamos un complemento de ejemplo que eliminará la palabra debugger del código. Tal complemento ya existe, es @ putout / plugin-remove-debugger y es lo suficientemente simple como para considerarlo ahora.


Se ve así:


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

Si la regla remove-debugger está incluida en .putout.json , se .putout.json el @putout/plugin-remove-debugger . Primero, se llama a la función de find que, utilizando la función traverse , omitirá los nodos del árbol AST y guardará todos los lugares necesarios.


El siguiente paso se convertirá en report para obtener el mensaje deseado.


Si se --fix indicador --fix , se --fix la función de fix del complemento y se realizará la transformación, en este caso, se eliminará el nodo.


Ejemplo de prueba de complemento

Para simplificar la prueba de complementos, se escribió la herramienta @ putout / test . En esencia, no es más que una envoltura sobre cinta , con varios métodos para la conveniencia y simplificación de las pruebas.


La prueba para el complemento remove-debugger podría verse así:


 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

No todas las transformaciones deben usarse todos los días, para las transformaciones de una sola vez es suficiente hacer lo mismo, pero en lugar de publicar en npm colóquelo en la ~/.putout . Al inicio, el aspecto se verá en esta carpeta, se iniciará la transformación.


Aquí hay un ejemplo de transformación que reemplaza la tape y la conexión de prueba a cinta con una llamada de supertape : convert-tape-to-supertape .


eslint-plugin-putout


Al final, vale la pena agregar un punto: putout intenta cambiar el código mínimamente, pero si le sucede a un amigo que se rompen algunas reglas de formato, eslint --fix siempre está listo para eslint --fix , y para este propósito hay un complemento especial de eslint-plugin-putout . Puede aclarar muchos errores de formato y, por supuesto, se puede personalizar de acuerdo con las preferencias de los desarrolladores en un proyecto específico. Conectarlo es fácil:


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

Hasta ahora, solo hay una regla: one-line-destructuring , hace lo siguiente:


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

Hay muchas más reglas de inclusión incluidas con las que puede familiarizarse con más detalle .


Conclusión


Quiero agradecer al lector por la atención prestada a este texto. Espero sinceramente que el tema de las transformaciones de AST se vuelva más popular, y que los artículos sobre este fascinante proceso aparezcan con mayor frecuencia. Estaría muy agradecido por cualquier comentario y sugerencia relacionada con el desarrollo posterior de la putout . Cree un problema , envíe un grupo de solicitudes , pruebe, escriba qué reglas le gustaría ver y cómo programar su código mediante programación, trabajaremos juntos para mejorar la herramienta de transformación AST.

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


All Articles