引言
每天在处理代码时,在实现对用户有用的功能的途中,对代码进行强制(不可避免的或仅是理想的)更改就变得越来越重要。 这可以是重构,将库或框架更新到新的主要版本,更新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
解析器,其中一些是:
有一个标准的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是启用了插件的代码转换器。 实际上,这是eslint和babel之间的交叉,结合了这两种工具的优点。
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
- 空块
- 空模式
查找和拆分变量声明:
将commonjs
转换为commonjs
:
- 结合解构属性:
每个插件都是根据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
部分中添加单词: putout
和putout-plugin
,并在peerDependencies
"putout": ">=3.10"
指定"putout": ">=3.10"
peerDependencies
"putout": ">=3.10"
,或在编写插件时将是最后一个版本。
推杆示例插件
让我们编写一个示例插件,该插件将从代码中删除debugger
一词。 这样的插件已经存在,它是@ putout / plugin-remove-debugger ,现在已经足够简单了。
看起来像这样:
如果.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, });
Codemods
并非每天都需要使用每个转换,一次转换就足以完成相同的事情,但是,除了将其发布到npm
请将其放置在~/.putout
。 启动时, putout
将在此文件夹中查找,拾取并开始转换。
这是一个示例转换,该转换将tape
和try-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
,它执行以下操作:
您还可以包括更多的eslint
规则,以使自己更加熟悉。
结论
我要感谢读者对本文的关注。 我衷心希望AST转换的话题将变得更加流行,并且有关此引人入胜的过程的文章也会更多地出现。 对于与putout
的进一步发展有关的任何意见和建议,我将不胜感激。 创建问题 , 发送请求池 ,进行测试,编写您希望看到的规则以及如何以编程方式编写代码,我们将共同努力以改善AST转换工具。