为什么要解析您的代码? 例如,为了在提交之前找到被遗忘的console.log。 但是,如果您需要在代码的数百个条目中更改函数的签名,该怎么办? 正则表达式在这里可以应付吗? 本文将向您展示抽象语法树为开发人员提供的可能性。

在剪辑下-来自
HolyJS 2018 Piter会议的Kirill Cherkashin(
z6Dabrata )报告的视频和文字记录。
关于作者
西里尔(Cyril)出生在莫斯科,现在住在纽约,在Firebase工作。 不仅在Google,而且在全球范围内教授Angular。 世界上最大的Angular mitap的组织者是AngularNYC(以及VueNYC和ReactNYC)。 在编程的业余时间里,他喜欢探戈,书籍和愉快的对话。钢锯或木头?
让我们从一个例子开始:假设您调试了一个程序并将对git所做的更改发送给git,然后您就安静地上床了。 早晨,事实证明您的同事已下载您的更改,并且由于您忘记了前一天将调试信息的输出删除到控制台,因此它将显示并阻塞输出。 许多人面临这个问题。
有一些工具(例如
EsLint )可以解决这种情况,但是出于教育目的,让我们尝试自己寻找解决方案。
我应该使用哪个工具从代码中删除所有
console.log()
?
我们在正则表达式和使用Abstract Sitax树(ASD)之间进行选择。 让我们尝试通过编写一些
findConsoleLog
函数来使用正则表达式解决此问题。 在输入处,如果在程序文本中的某个位置找到console.log(),它将接收程序代码作为参数,并显示true。
function findConsoleLog(code) { return !!code.match(/console.log/); }
我编写了17个测试,试图提出各种破坏功能的方法。 此列表远非完整。

最简单的测试通过了。
如果函数名称中包含字符串“ console.log”怎么办?
function findConsoleLog(code) { return !!code.match(/\bconsole.log/); }
添加了一个字符,指示
console.log
应该出现在单词的开头。

仅通过了两个测试,但是如果
console.log
在注释中并且不需要删除,该怎么办?
我们重写它,以使解析器不会触及注释。
function findConsoleLog(code) { return !!code .replace(/\/\/.*/) .match(/\bconsole.log/); }

从以下各行中排除“ console.log”的删除:
function findConsoleLog(code) { return !!code .replace(/\/\/.*|'.*'/, '') .match(/\bconsole.log/); }

不要忘记,我们还有空格和其他字符,可能会阻止某些测试通过:

尽管想法不是很简单,但是可以通过使用正则表达式的所有17个测试。 因此,在这种情况下,解决方案代码将如下所示:
function findConsoleLog(code) { return code .replace(/\/\/.*|'.*?[^\\]'|".*?"|`[\s\S]*`|\/\*[\s\S]*\*\//) .match(/\bconsole\s*.log\(/); }
问题在于该代码无法涵盖所有可能的情况,因此很难对其进行维护。
考虑如何使用ASD解决此问题。
树木如何生长?
作为解析器使用您的应用程序代码的结果,获得了抽象语法树。 解析器
@ babel /解析器用于演示
。例如,使用字符串
console.log('holy')
,将其通过解析器。
import { parse } from 'babylon'; parse("console.log('holy')");
作为他的工作的结果,获得了大约300行的JSON文件。 我们从电话号码中排除服务信息。 我们对正文部分感兴趣。 元信息也不会让我们感兴趣。 结果大约是100行。 与浏览器为一个主体变量(大约300行)生成的结构相比,这并不多。
让我们看一些示例,说明语法树中的代码如何表示各种文字:

这是一个包含数字文字(数字文字)的表达式。

已经熟悉的console.log表达式。 它具有一个具有属性的对象。

如果log是函数调用,则描述如下:有一个调用表达式,它具有参数-数字文字。 同时,调用表达式具有一个名称-log。
文字可以不同:数字,字符串,正则表达式,布尔值,空值。
返回console.log调用

这是一个内部有“成员表达式”的调用表达式。 从中可以明显看出,控制台对象内部具有一个名为log的属性。
ASD旁路
现在,让我们尝试在代码中使用此结构。
babel遍历库将用于遍历树
。给出了相同的17个测试。 通过分析程序的语法树并搜索“ console.log”的条目来获得这样的代码:
function traverseConsoleLog(code, {babylon, babelTraverse, types, log}) { const ast = babylon.parse(code); let hasConsoleLog = false; babelTraverse(ast, { MemberExpression(path){ if ( path.node.property.type === 'Identifier' && path.node.property.name === 'log' && path.node.object.type === 'Identifier' && path.node.object.name === 'console' && path.parent.type === 'CallExpression' && path.Parentkey === 'callee' ) { hasConsoleLog = true; } } }) return hasConsoleLog; }
让我们分析一下这里写的内容。
const ast = babylon.parse(code);
进入ast变量,我们从代码中解析语法树。 接下来,我们将这棵树提供给babel-parse库进行处理。 我们正在寻找调用表达式中具有匹配名称的节点和属性。 如果找到了所需的节点及其名称的组合,请将hasConsoleLog变量设置为true。
我们可以在树上四处走动,获取节点,后代的父代,查找它们具有的参数和属性,查看这些属性,类型的名称-这非常方便。
有一个令人不愉快的细微差别,可以使用babel-types库轻松修复。 为了避免在树中搜索时由于名称错误而产生错误,例如,您意外地用
path.parent.type === 'callExpression'
-types代替
path.parent.type === 'CallExpression'
编写了
path.parent.type === 'callExpression'
,您可以这样编写:
我们使用babel-types重写前面的代码:
function traverseConsoleLogSolved2(code, {babylon, babelTraverse, types}) { const ast = babylon.parse(code); let hasConsoleLog = false; babelTraverse(ast, { MemberExpression(path) { if ( types.isIdentifier(path.node.object, { name: 'console'}) && types.isIdentifier(path.node.property, { name: 'log'}) && types.isCallExpression(path.parent) && path.parentKey === 'callee' ) { hasConsoleLog = true; } } }); return hasConsoleLog; }
使用babel-traverse转换ASD
为了减少人工成本,我们需要立即从代码中删除
console.log
而不是发出代码已包含在代码中的信号。
因为我们不需要删除MemberExpression本身,而是它的父项,
hasConsoleLog = true;
我们写
path.parentPath.remove();
。
从
removeConsoleLog
函数,我们仍然返回一个布尔值。 我们将其输出替换为将生成babel-generator的代码,如下所示:
hasConsoleLog
=>
babelGenerator(ast).code
Babel-generator接收修改后的抽象语法树作为参数,返回一个具有code属性的对象,该对象内部是没有
console.log
再生代码。 顺便说一句,如果要获取代码映射,可以调用此对象的sourceMaps属性。
如果您需要找到调试器?
这次,我们将使用
ASTexplorer完成任务。 调试器是调试器语句节点的一种。 我们不需要看整个结构,因为这是一种特殊的节点,只需找到调试器语句即可。 我们将为ESLint(在ASTexplorer上)编写一个插件。
ASTexplorer的设计方式是,您在左侧编写代码,在右侧编写完成的ASD。 您可以选择要接收的格式:JSON或树格式。

由于我们使用ESLint,它将为我们完成所有搜索文件的工作,并提供必要的文件,以便我们可以在其中找到调试器行。 该工具使用其他ASD解析器。 但是,JavaScript中有几种类型的ASD。 当不同的浏览器以不同的方式实现该规范时,让人想起过去。 因此,我们实现了调试器搜索:
export default function(context) { return { DebuggerStatement(node) { // , console.log path, - , path context.report(node, 'LOL Debugger!!!'); // ESLint , debugger, node , , debugger } } }
检查书面插件的工作:

同样,您可以从代码中删除调试器。
还有什么有用的ASD
我个人使用ASD来简化Angular和其他前端框架的工作。 您可以通过单击按钮来导入,扩展,添加接口,方法,装饰器和其他任何东西。 尽管我们在这种情况下讨论的是Javascript,但是TypeScript也有自己的ASD,唯一的区别是节点类型的名称和结构之间的区别。 在同一ASTExplorer中,可以选择语言TypeScript。
因此:
- 我们对代码有更多的控制权,更易于重构和codemods。 例如,在提交之前,您可以按一个键以按照准则格式化整个代码。 Codemods意味着根据所需的框架版本自动进行代码匹配。
- 减少关于代码设计的争议。
- 您可以创建游戏项目。 例如,自动向程序员提供有关他编写的代码的反馈。
- 更好地了解JavaScript。
一些有关Babel的有用链接
- 所有Babel转换都使用以下API: 插件和预设 。
- 向ECMAScript添加新功能的过程的一部分是为Babel创建一个插件。 这是必需的,以便人们可以测试新功能。 如果单击该链接 ,则可以看到在其内部使用了ASD的功能。 例如, 逻辑分配运算符 。
- Babel Generator在生成代码时会丢失格式。 这在某种程度上是好的,因为如果开发团队使用了此工具,则在从ASD生成代码后,每个人的外观都一样。 但是,如果要保留格式,可以使用以下工具之一: Recast或Babel CodeMod 。
- 从此链接,您可以找到有关Babel Awesome Babel的大量信息。
- Babel是一个开源项目,一个志愿者团队正在研究该项目。 你可以帮忙 执行此操作的方法有以下三种:财务支持,可以支持patreon网站,babel的主要贡献者之一亨利·朱(Henry Zhu)可以与patreon网站一起使用,以获取opencollective.com/babel上的代码帮助。
红利
我们还能如何找到我们的
console.log
代码? 使用您的IDE! 在选择要查找代码的位置之后,使用查找和替换工具。
Intellij IDEA还具有“结构搜索”工具,可以使用ASD帮助您在代码中找到正确的位置。
11月24日至25日, Kirill将在Moscow HolyJS上发表有关JavaScript * LOVES *二进制数据的演示文稿:我们将深入到二进制数据级别,以* .gif文件为例深入研究二进制文件,并处理诸如Protobuf或Thrift之类的序列化框架。 报告完成后,将有可能与Cyril进行对话并讨论讨论区域中所有感兴趣的问题。