以Putut为例的AST树转换的实际应用

引言


每天在处理代码时,在实现对用户有用的功能的途中,对代码进行强制(不可避免的或仅是理想的)更改就变得越来越重要。 这可以是重构,将库或框架更新到新的主要版本,更新JavaScript语法(最近并不罕见)。 即使该库是一个正在运行的项目的一部分,更改也是不可避免的。 这些更改大多数都是常规性的。 对于开发人员来说,这没有什么有趣的,一方面,它不会给业务带来任何好处,第三,在更新过程中,您需要非常小心,不要破坏柴火和不破坏功能。 因此,我们得出的结论是,最好将这样的例程移到程序的肩膀上,以使程序可以自己完成所有事情,而人又可以控制一切是否正确完成。 这将在今天的文章中讨论。


AST


对于程序代码处理,有必要将其转换为特殊的表示形式,这样可以方便程序工作。 存在这种表示形式,称为抽象语法树 (AST)。
为了获得它,请使用解析器。 可以根据需要转换生成的AST,然后保存结果需要一个代码生成器。 让我们更详细地考虑每个步骤。 让我们从解析器开始。


解析器


这样我们就有了代码:


a + b 

解析器通常分为两部分:


  • 词法分析

将代码分成令牌,每个令牌都描述了一部分代码:


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

  • 解析中

根据令牌构建语法树:


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

现在我们已经有了一个想法,您可以通过它进行编程工作。 值得澄清的是,有大量的JavaScript解析器,其中一些是:


  • babel- babel使用babel的解析器;
  • espree-使用eslint的解析器;
  • 橡子 -前面两个基于的解析器;
  • esprima-在EcmaScript 2017之前支持JavaScript的流行解析器;
  • cherow是JavaScript解析器中声称是最快的新角色。

有一个标准的JavaScript解析器,称为ESTree ,用于定义应解析的节点。
要更详细地分析解析器(以及转换器和生成器)的实现过程,可以阅读super-tiny-compiler


变形金刚


为了转换AST树,您可以使用Visitor模式,例如,使用@ babel /遍历库。 以下代码将从code变量输出所有JavaScript代码标识符的名称。


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

发电机组


您可以使用以下方式生成代码,例如,使用@ babel / generator


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

因此,在这个阶段,读者应该已经基本了解了转换JavaScript代码所需的内容以及使用哪些工具实现该代码。


还值得添加一个在线工具,例如astexplorer ,它将大量的解析器,转换器和生成器结合在一起。


推杆


Putout是启用了插件的代码转换器。 实际上,这是eslintbabel之间的交叉,结合了这两种工具的优点。


eslint如何显示代码中的问题区域,但是与eslint不同,它可以更改代码的行为,也就是说,它可以修复所有可发现的错误。


babel putout一样putout转换代码,但尝试对其进行最小的更改,因此可以将其用于存储在存储库中的代码。


更漂亮的还值得一提,它是一种格式化工具,与之根本不同。


Jscodeshift的位置离putout不太远,但是它不支持插件,不显示错误消息,并且还使用ast-types而不是@ babel / types


外观故事


在此过程中, eslint提示对我有很大帮助。 但是有时候我想要他更多。 例如,要删除调试器 ,请修复test.only ,并删除未使用的变量。 最后一点构成了putout的基础,在开发过程中,很明显这很困难,许多其他转换也很容易实现。 因此,推出功能从一个功能平稳地增长到插件系统。 现在,删除未使用的变量是最困难的过程,但这并不妨碍我们开发和支持许多其他同样有用的转换。


推杆如何在内部工作


putout工作可分为两部分:引擎和插件。 这种架构使您在使用引擎时不会因转换而分心,在使用插件时,您将最大程度地专注于其用途。


内置插件


putout构建在插件系统上。 每个插件代表一个规则。 使用内置规则,您可以执行以下操作:


  • 查找和删除:


    • 未使用的变量
    • debugger
    • 打电话给test.only
    • 致电test.skip
    • 调用console.log
    • 呼叫process.exit
    • 空块
    • 空模式

  • 查找和拆分变量声明:


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

  • commonjs转换为commonjs



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

  • 应用解构:

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

  1. 结合解构属性:

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

每个插件都是根据Unix哲学构建的,也就是说,它们尽可能地简单,每个插件都执行一个动作,使它们易于组合,因为它们实质上是过滤器。


例如,具有以下代码:


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

首先使用apply-destructuring将其转换为:


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

然后,使用merge-destructuring-properties将其转换为:


 const { name, password } = user; 

因此,插件可以单独工作,也可以一起工作。 在创建自己的插件时,建议遵循此规则,并以最少的功能实现只满足您需求的插件,其余的将由内置和自定义插件来完成。


使用范例


在熟悉了内置规则之后,我们可以考虑使用putout的示例。
创建一个具有以下内容的example.js文件:


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

现在通过传递example.js作为参数来运行putout


 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 

我们将收到包含6个错误的信息,上面已对其进行了详细介绍,现在我们将对其进行更正,然后看看发生了什么:


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

作为更正的结果,未使用的变量和console.log调用被删除,并且还应用了分解。


设定值


默认设置可能并不总是适用于所有人,因此.putout.json支持.putout.json配置文件,它由以下部分组成:


  • 规则
  • 忽略
  • 搭配
  • 外挂程式

规则

rules部分包含规则系统。 默认情况下,规则设置如下:


 { "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 } } 

要启用remove-process-exit只需在.putout.json文件中将其设置为true .putout.json


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

这将足以报告在代码中找到的所有process.exit调用,并在使用--fix选项时将其删除。


忽略

如果需要在例外列表中添加一些文件夹,只需添加ignore部分:


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

搭配

例如,如果您需要分支的规则系统,请为bin目录启用process.exit ,只需使用match部分:


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

外挂程式

如果您使用的插件不是内置的,并且前缀putout-plugin- ,则必须在rules部分将其激活之前,将它们包括在plugins部分中。 例如,要连接putout-plugin-add-hello-world并启用add-hello-world规则,只需指定:


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

推杆引擎


putout引擎是一个命令行工具,可读取设置,解析文件,加载并运行插件,然后写入插件的结果。


它使用重铸库,这有助于执行一项非常重要的任务:解析和转换后,以与上一个状态尽可能相似的状态收集代码。


对于解析,使用了与ESTree兼容的解析器( babel当前与estree插件一起使用,但将来可能会更改),并且使用babel工具进行转换。 为什么是babel ? 一切都很简单。 事实是,这是一个非常受欢迎的产品,比其他类似工具更受欢迎,并且开发速度更快。 没有babel插件,EcmaScript标准中的每个新建议都是不完整的Babel还有一本书《 Babel手册》 ,很好地描述了遍历和转换AST树的所有功能和工具。


自定义插件


putout插件系统非常简单,与eslint插件以及babel插件非常相似。 没错, putout plugin应该导出3,而不是一个函数。这样做是为了增加代码重用性,因为在3个函数中复制功能不是很方便,将它放入单独的函数中并在正确的地方简单调用就容易得多。


插件结构

因此, Putout插件包含3个功能:


  • report -返回消息;
  • find -搜索有错误的地方并返回它们;
  • fix -修复这些地方;

putout创建插件时要记住的主要一点是它的名称,它应该以putout-plugin- 。 接下来可能是该插件执行的操作的名称,例如,应该这样调用remove-wrong插件: putout-plugin-remove-wrong


您还应该在keywords部分的package.json部分中添加单词: putoutputout-plugin ,并在peerDependencies "putout": ">=3.10"指定"putout": ">=3.10" peerDependencies "putout": ">=3.10" ,或在编写插件时将是最后一个版本。


推杆示例插件

让我们编写一个示例插件,该插件将从代码中删除debugger一词。 这样的插件已经存在,它是@ putout / plugin-remove-debugger ,现在已经足够简单了。


看起来像这样:


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

如果.putout.json包含remove-debugger规则,则将加载@putout/plugin-remove-debugger .putout.json @putout/plugin-remove-debugger 。 首先,调用find函数,该函数使用traverse函数绕过AST树的节点并保存所有必要的位置。


下一步输出将转向report以获取所需的消息。


如果使用--fix标志,则将调用插件fix函数并执行转换,在这种情况下,将删除该节点。


插件测试示例

为了简化插件的测试 ,编写了@ putout /测试工具。 从本质上讲,它不过是纸带上的包装纸,并提供了几种方便和简化测试的方法。


remove-debugger插件的测试可能如下所示:


 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

并非每天都需要使用每个转换,一次转换就足以完成相同的事情,但是,除了将其发布到npm请将其放置在~/.putout 。 启动时, putout将在此文件夹中查找,拾取并开始转换。


这是一个示例转换,该转换将tapetry-to-tape 连接替换为supertape调用: convert-tape-to-supertape


eslint插件输出


最后,值得一提的是: putout尝试最小地更改代码,但是如果遇到某个朋友违反了某些格式规则,则eslint --fix随时可以提供eslint --fix ,因此有一个特殊的eslint-plugin-putout插件 。 它可以消除许多格式错误,当然可以根据开发人员在特定项目上的偏好对其进行自定义。 连接起来很容易:


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

到目前为止,其中只有一条规则: one-line-destructuring ,它执行以下操作:


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

您还可以包括更多的eslint规则,以使自己更加熟悉。


结论


我要感谢读者对本文的关注。 我衷心希望AST转换的话题将变得更加流行,并且有关此引人入胜的过程的文章也会更多地出现。 对于与putout的进一步发展有关的任何意见和建议,我将不胜感激。 创建问题发送请求池 ,进行测试,编写您希望看到的规则以及如何以编程方式编写代码,我们将共同努力以改善AST转换工具。

Source: https://habr.com/ru/post/zh-CN439564/


All Articles