象语法树(Abstract Syntax Trees),简称AST,如果您正在编写代码,那么 AST 很可能已经参与了您的开发流程。它们为您的开发流程的许多部分提供动力。 有些人可能在编译器的上下文中听说过它们,但它们被用于各种工具中。 即使您不编写通用开发工具,AST 也可能是您工具带中的有用工具。 在这篇文章中,我们将讨论什么是 AST,它们在哪里使用以及如何利用它们。
什么是AST
抽象语法树或 AST 是代码的树型数据结构表示。 它们是编译器工作方式的基本部分。 当编译器转换某些代码时,基本上有以下步骤:
词法分析又名标记化
在此步骤中,您编写的代码将被转换为一组描述代码不同部分的标记。 这与基本语法突出显示使用的方法基本相同。 这些标记令牌不了解事物如何组合在一起,并且仅关注文件的组件。
你可以想象这就像你将一个文本分解成单词。 您可能能够区分标点符号、动词、名词、数字等,但在这个阶段,您对句子的组成部分或句子如何组合没有任何更深入的了解。
语法分析又名解析
这是我们将标记列表转换为抽象语法树的步骤。 它将我们的标记转换为表示代码实际结构的树。 以前在标记中我们只有一对 (),现在我们知道它是函数调用、函数定义、分组还是其他东西。
这里的等价物是将我们的单词列表转换为表示诸如句子之类的数据结构,某个名词在句子中扮演什么角色,或者我们是否在列表中。
另一个可以与之比较的例子是 DOM。 上一步只是将 HTML 分解为“标签”和“文本”,而这一步将生成表示为 DOM 树的层次结构。
需要注意的一件事是没有“单一”的 AST 格式。 它们可能会有所不同,这取决于您要转换为 AST 的语言以及您用于解析的工具。 在 JavaScript 中,一个通用标准是 ESTree,但您会看到不同的工具可能会添加不同的属性。
一般来说,AST 是一种树结构,其中每个节点至少有一个类型来指定它所代表的内容。
代码生成
此步骤本身可以是多个步骤。 一旦我们有了抽象语法树,我们就可以操作它,也可以将它“打印”到不同类型的代码中。 使用 AST 操作代码比直接在代码上作为文本或标记列表执行这些操作更安全。
操纵文本总是很危险的; 它显示最少的上下文。 如果您曾经尝试使用字符串替换或正则表达式来操作文本,您可能会注意到很容易出错。而且不容易调试。
甚至操纵令牌也不容易。 虽然我们可能知道变量是什么,但如果我们想重命名它,我们将无法深入了解变量的范围或可能与之冲突的任何变量。
AST 提供了有关代码结构的足够信息,我们可以更有信心地对其进行修改。 例如,我们可以确定变量的声明位置,并确切地知道由于树结构而影响程序的哪个部分。
一旦我们操纵了树,我们就可以打印树以输出任何预期的代码输出。 例如,如果我们要构建一个像 TypeScript 编译器这样的编译器,我们会输出 JavaScript,而另一个编译器可能会输出机器代码。
同样,使用 AST 更容易实现这一点,因为相同结构的不同输出可能具有不同的格式。 使用更线性的输入(如文本或标记列表)生成输出会相当困难。
如何处理 AST?
理论涵盖了哪些实际生活中的 AST 用例? 我们讨论了编译器,但我们并不是整天都在构建编译器。
AST 的用例很广泛,通常可以分为三个总体操作:读取、修改和打印。 它们是一种添加剂,这意味着如果您正在打印 AST,那么您以前也阅读过 AST 并对其进行修改的可能性很高。 但我们将介绍每个主要关注一个用例的示例。
读取/遍历 AST
从技术上讲,使用 AST 的第一步是解析文本以创建 AST,但在大多数情况下,提供解析步骤的库也提供了一种遍历 AST 的方法。遍历 AST 意味着访问树的不同节点以获取细节或执行操作。
最常见的用例之一是 linting。 例如,ESLint 使用 espree 生成 AST,如果您想编写任何自定义规则,您将根据不同的 AST 节点编写这些规则。 ESLint 文档有大量关于如何构建自定义规则、插件和格式化程序的文档。
修改/转换 AST
如前所述,与将代码修改为标记或原始字符串相比,拥有 AST 使修改所述树更容易、更安全。您可能想要使用 AST 修改某些代码的原因有很多种。
例如,Babel 修改 AST 以向下编译新功能或将 JSX 转换为函数调用。例如,当您编译 React 或 Preact 代码时会发生这种情况。
另一个用例是捆绑代码。在模块的世界中,捆绑代码通常比将文件附加在一起要复杂得多。更好地了解各个文件的结构可以更轻松地合并这些文件并在必要时调整导入和函数调用。如果您查看 webpack、parcel 或 rollup 等工具的代码库,您会发现它们都使用 AST 作为其捆绑工作流程的一部分。
打印 AST
在大多数情况下,打印和修改 AST 是齐头并进的,因为您必须输出刚刚修改的 AST。 但是,虽然像 recast 这样的一些库明确专注于以与原始代码样式相同的代码样式打印 AST,但也有各种用例,您希望以不同的方式显式打印您的 AST。
例如,Prettier 使用 AST 根据您的配置重新格式化您的代码,而无需更改代码的内容/含义。 他们这样做的方式是将您的代码转换为完全与格式无关的 AST,然后根据您的规则重写它。
其他常见的用例是用不同的目标语言打印代码或构建自己的缩小工具。
您可以使用几种不同的工具来打印 AST,例如 escodegen 或 astring。 您还可以根据您的用例构建自己的格式化程序,或者为 Prettier 构建一个插件。
最后:
虽然 AST 可能是大多数开发人员每天都不会使用的东西,但我相信了解它对今后的工作会有帮助。感谢阅读。
在开始前:如有不准确的地方希望大家提出,文章可以改知识不能错。
抽象语法树(AST),nodejs,UglifyJS,gulp,through2搜索引擎输入相关关键字会有很多文章这里就不一一阐述。
UglifyJS http://lisperator.net/uglifyjs/
gulp https://www.gulpjs.com.cn/
through https://cnodejs.org/topic/59f220b928137001719a8270
抽象语法树(AST) http://www.iteye.com/news/30731
编码过程中难免会写一些console.log...输出语句用于测试,如果未及时删除发布后会出现莫名其妙的输出打印在控制台,这里叫它们幽灵输出。
造个轮子?
定义一个统一的日志输出方法,通过设置开关变量来控制输出打印
...
function printLog(src){
if(dev){
console.log(src);
}
}
...
轮子不圆改一改嘛
为避免统一定义打印方法使用遗漏问题,决定重新定义系统console
let oldInfo=console.info;
console.info=function(){
if(dev){
oldInfo.apply(console,arguments);
}
console.log=...
...
}
这里来了一个新需求,希望在开发状态打印方法参数值,方法执行顺序,发布状态不打印并清理无用输出。
这里需要一个全局拦截器,重构,造轮子已然无望。
源码-->编译-->可执行文件
在编译时向源码中注入一段代码实现拦截器,或者删除无用代码。
以下面这段代码为例说一下需要做什么
function test(id, name) {
console.log('This is a demo ' + id + ' ' + name);
return;
}
function test111(id, name) {
console.log('This is a demo ' + id + ' ' + name);
return;
}
function doTest() {
test111(4, 5);
test(6, 7);
test111(4, 5);
}
doTest();
在每一个方法体内部增加日志打印语句打印方法名称,方法参数,方法调用编号(累加,标示方法运行顺序);
function test(id, name) {
//[TODO 这里需要增加输出代码]
console.log('This is a demo ' + id + ' ' + name);
return;
}
function test111(id, name) {
//[TODO 这里需要增加输出代码]
console.log('This is a demo ' + id + ' ' + name);
return;
}
function doTest() {
//[TODO 这里需要增加输出代码]
test111(4, 5);
test(6, 7);
test111(4, 5);
}
doTest();
找到输出语句删除;
function test(id, name) {
console.log('This is a demo ' + id + ' ' + name);// 删除
return;
}
function test111(id, name) {
console.log('This is a demo ' + id + ' ' + name);// 删除
return;
}
function doTest() {
test111(4, 5);
test(6, 7);
test111(4, 5);
}
doTest();
创建一个项目来实现这个需求
image.png
libs 资源库
dev 开发模式编译输出
dist 发布模式编译输出
src 源码目录
build.js 编译主调度文件
global.js 全局资源定义文件
对于语法遍历总是需要一定的规则的,如果真的有人和你说无脑遍历文件来个正则过滤这样的的方案请对他好一些。
需要理解一个概念抽象语法树
使用UglifyJS来对代码进行转换与遍历。部分如下:
global.js文件
global.U2=require('uglify-js');
global.gulp=require('gulp');
global.through=require('through2');
global.del=require('del');
global.util=require("./libs/util");
util.js
module.exports={
splice_string: function (str, begin, end, replacement) {
if (replacement.length > 0) {
return str.substr(0, begin) + replacement + str.substr(end);
} else {
return str.substr(0, begin) + replacement + str.substr(end + 1);
}
}
}
build_dev.js
//开发编译策略
module.exports={
doBuild: function (code) {
console.log("[Dev] Building Code ");
return through.obj(function (file, enc, cb) {
let addLog_nodes=[];
//获得文件内容
let code=file
.contents
.toString();
//将源码转换成语法树
let ast=U2.parse(code);
//遍历树
ast.walk(new U2.TreeWalker(function (node) {
if (node.body && node.name) {
//找到方法定义并放到等更改数组中
addLog_nodes.push(node);
}
}));
let runindexstr=`\n\tif (global.runIndex) { global.runIndex++; } else {global.runIndex=1; }\t`;
//遍历待修改数组注入代码
for (var j=addLog_nodes.length; --j >=0;) {
var conStr=`\n\tconsole.log('['+global.runIndex+']\t方法名称:${addLog_nodes[j].name.print_to_string()} 参数列表:'+`;
var node=addLog_nodes[j];
var start_pos=node.start.pos;
var end_pos=node.end.endpos;
if (node.argnames.length > 0) {
for (var k=node.argnames.length; --k >=0;) {
conStr +=`'${node.argnames[k].print_to_string()}='+${node.argnames[k].print_to_string()}+','+`;
}
} else {
conStr +=`'无',`;
}
conStr=runindexstr + conStr.substring(0, conStr.length - 1) + ");\n\t";
code=util.splice_string(code, start_pos + (node.print_to_string().indexOf("{")) + 3, start_pos + (node.print_to_string().indexOf("{")) + 3, conStr);
}
file.contents=new Buffer(code);
//写入转换后文件
this.push(file);
cb();
});
}
}
build_dist.js
//发布编译策略
module.exports={
doBuild: function () {
console.log("[Dist] Building Code ");
return through.obj(function (file, enc, cb) {
let console_nodes=[];
//读取文件内容
let code=file
.contents
.toString();
//将代码转换成语法树
let ast=U2.parse(code);
//语法树遍历
ast.walk(new U2.TreeWalker(function (node) {
//找到console语句放到待处理数组中
if (node &&node.expression) {
if (node.expression.print_to_string().indexOf('console') > -1) {
console_nodes.push(node);
}
}
}));
//遍历待处理数组进行代码删除
for (var i=console_nodes.length; --i >=0;) {
var node=console_nodes[i];
var start_pos=node.start.pos;
var end_pos=node.end.endpos;
var replacement="";
// var replacement="console.log('add code');\n\t" + node.print_to_string();
code=util.splice_string(code, start_pos, end_pos, replacement);
}
//压缩代码
code=U2
.minify(code)
.code;
file.contents=new Buffer(code);
//将文件装入实体
this.push(file);
cb();
});
}
}
build.js
require("./global");
const build_dev=require("./libs/build_dev");
const build_dist=require("./libs/build_dist");
var buildType="dev";
if (process.argv.length < 3) {
buildType="dev";
} else {
buildType=process.argv[2];
}
/**
* 清理输出任务
*/
gulp
.task("clean:dist", function (cb) {
if (buildType=="dev") {
del([process.cwd() + "/dev/*"], cb);
} else {
del([process.cwd() + "/dist/*"], cb);
}
cb();
});
/**
* 代码更改任务
*/
gulp.task("modifyCode", function (cb) {
if (buildType=="dev") {
gulp
.src('./src/*.js')
.pipe(build_dev.doBuild())
.pipe(gulp.dest(process.cwd() + "/dev"));
} else {
gulp
.src('./src/*.js')
.pipe(build_dist.doBuild())
.pipe(gulp.dest(process.cwd() + "/dist"));
}
cb();
});
/**
* 入口任务
*/
gulp.start("modifyCode", ["clean:dist"], function (error, msg) {
console.info("successfull");
if (error) {
console.info(error);
}
});
关于拦截器部分实现,只是实现了一个通用的拦截器,考虑升级实现类似spring 注解形式的定向注入模式,后续会发布相关解决方案。
端用JavaScript实现桑基图(Sankey图)
桑基图(Sankey图),是流图的一种,常用来展示事物的数量、发展方向、数据量大小等,在可视化分析中经常使用。
本文,演示如何在前端用JavaScript绘制桑基图。注:本例使用JShaman数据展示JS代码混淆加密流程。
先看效果:
因为已有成熟的库可用,比如,可以使用d3引擎,所以sankey的实现较为简单。
众所周知,JShaman是国内知名的JS代码混淆加密平台,我们将用JShaman英文版的混淆返回内容做为数据源,绘制一张JS代码混淆加密流程桑基图。
JShaman数据采集,直接复制即可:
用d3实现桑基图绘制,核心代码如下,文末会提供完整代码。
绘图成功:
桑基图效果说明:从图中,可以看到JShaman对JS代码的混淆加密流程:初始的JS代码,先转为AST(抽象语法树),再进行String reverse、Dead Code Insertion、Eval Encryption等数十种混淆加密操作,生成了新的AST,最后再根据AST重新生成JS代码,这便是JS代码混淆加密的完整流程,由图可以让人一目了然的知晓全过程。
最后,附上完整代码,如果您也需要绘制桑基图,可以参考此代码:
*请认真填写需求信息,我们会在24小时内与您取得联系。