使用抽象的JavaScript语法树

为什么要解析您的代码? 例如,为了在提交之前找到被遗忘的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' ,您可以这样编写:

 // Before path.node.property.type === 'Identifier' path.node.property.name === 'log' // with babel types import {isIdentifier} from 'babel-types'; isIdentifier(path.node.property, {name: log}) //         ,  ,    isIdentifier,      

我们使用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的有用链接


  1. 所有Babel转换都使用以下API: 插件和预设
  2. 向ECMAScript添加新功能的过程的一部分是为Babel创建一个插件。 这是必需的,以便人们可以测试新功能。 如果单击该链接 ,则可以看到在其内部使用了ASD的功能。 例如, 逻辑分配运算符
  3. Babel Generator在生成代码时会丢失格式。 这在某种程度上是好的,因为如果开发团队使用了此工具,则在从ASD生成代码后,每个人的外观都一样。 但是,如果要保留格式,可以使用以下工具之一: RecastBabel CodeMod
  4. 从此链接,您可以找到有关Babel Awesome Babel的大量信息。
  5. 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进行对话并讨论讨论区域中所有感兴趣的问题。

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


All Articles