前置知识
抽象语法树(Abstract Syntax Tree)
- 抽象语法树(Abstract Syntax Tree,简称AST)是源代码语法结构的一种抽象表示
- 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构
抽象语法树用途
- 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码的错误提示、代码的自动补全等等。
- 优化变更代码、改变代码结构使达到想要的结构
JavaScript Parser
JavaScript Parser
是把JavaScript源码转化为抽象语法树的解析器- 我们可以通过
https://astexplorer.net/
在线查看源代码对应的ast结构
代码转换
- 通过
esprima
将代码转换成ast语法树 - 通过
estraverse
深度优先遍历,遍历ast抽象语法树 - 通过
escodegen
进行代码生成
pnpm i esprima estraverse escodegen -S
const esprima = require("esprima");
const estraverse = require("estraverse");
const escodegen = require("escodegen");
const code = `function ast(){}`;
// 1.将代码转换成ast语法树
const ast = esprima.parseScript(code);
// 2.深度优先遍历,遍历ast抽象语法树
let level = 0;
const padding = () => " ".repeat(level);
// 遍历节点的时候,会有两个过程,进入和离开
estraverse.traverse(ast, {
enter(node) {
console.log(padding() + "enter:" + node.type);
level += 2;
},
leave(node) {
level -= 2;
console.log(padding() + "leave:" + node.type);
},
});
// 3.代码⽣成
const result = escodegen.generate(ast);
console.log(result);
estraverse.traverse(ast, {
enter(node) {
if (node.type === "FunctionDeclaration") {
node.id.name = "myFunc";
}
},
});
babel插件
转换箭头函数
- @babel/core Babel 的编译器,核⼼ API 都在这⾥⾯,⽐如常⻅的 transform、parse,并实现了插件功能
- @babel/types ⽤于 AST 节点的 Lodash 式⼯具库, 它包含了构造、验证以及变换 AST 节点的⽅法,对编写处 理 AST 逻辑⾮常有⽤
- babel-plugin-transform-es2015-arrow-functions 转换箭头函数插件
const babel = require("@babel/core");
const types = require("@babel/types");
const arrowFunctions = require("babel-plugin-transform-es2015-arrow-functions");
const code = `const sum = (a,b)=> a+b`;
// 转化代码,通过arrowFunctions插件
const result = babel.transform(code, {
plugins: [arrowFunctions],
});
console.log(result.code);
const arrowFunctions = {
visitor: {
// 访问者模式,遇到箭头函数表达式会命中此⽅法
ArrowFunctionExpression(path) {
let { node } = path;
node.type = "FunctionExpression"; // 将节点转换成函数表达式
let body = node.body;
// 如果不是代码块,则增加代码块及return语句
if (!types.isBlockStatement(body)) {
node.body = types.blockStatement([types.returnStatement(body)]);
}
}
},
};
@babel/types 主要就是对类型的判断和创建,接下来,我们继续处理箭头函数中的this问题!
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md
const sum = (a,b)=> console.log(this) // 源代码
----transform ----
var _this = this; // 转换后的代码
const sum = function (a, b) {
return console.log(_this);
};
需要找到上级作⽤域增加this的声明语句
function getThisPaths(path) {
const thisPaths = [];
path.traverse({
ThisExpression(path) {
thisPaths.push(path);
},
});
return thisPaths; // 获得所有⼦路径中的thisExpression
}
function hoistFunctionEnvironment(path) {
const thisEnv = path.findParent((parent) => {
return (
(parent.isFunction() && !path.isArrowFunctionExpression()) ||
parent.isProgram()
);
});
const thisBindings = "_this"; // 找到this表达式替换成_this
const thisPaths = getThisPaths(path); // 遍历⼦路径
if (thisPaths.length > 0) {
if (!thisEnv.scope.hasBinding(thisBindings)) {
// 在作⽤域下增加 var _this = this
thisEnv.scope.push({
id: types.identifier(thisBindings),
init: types.thisExpression(),
});
}
}
// 替换this表达式
thisPaths.forEach((thisPath) =>
thisPath.replaceWith(types.identifier(thisBindings))
);
}
类编译为 Function
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
setName(newName) {
this.name = newName;
}
}
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.setName = function () {
this.name = newName;
};
const arrowFunctions = {
visitor: {
ClassDeclaration(path) {
const { node } = path;
const id = node.id;
const methods = node.body.body; // 获取类中的⽅法
const nodes = [];
methods.forEach((method) => {
if (method.kind === "constructor") {
let constructorFunction = types.functionDeclaration(
id,
method.params,
method.body
);
nodes.push(constructorFunction);
} else {
// Person.prototype.getName
const memberExpression = types.memberExpression(
types.memberExpression(id, types.identifier("prototype")),
method.key
);
// function(name){return name}
const functionExpression = types.functionExpression(
null,
method.params,
method.body
);
// 赋值
const assignmentExpression = types.assignmentExpression(
"=",
memberExpression,
functionExpression
);
nodes.push(assignmentExpression);
}
});
// 替换节点
if (node.length === 1) {
path.replaceWith(nodes[0]);
} else {
path.replaceWithMultiple(nodes);
}
},
},
}
Eslint使⽤
- ESLint 是⼀个开源的⼯具cli,ESLint采⽤静态分析找到并修复 JavaScript 代码中的问题
- ESLint 使⽤Espree进⾏ JavaScript 解析。
- ESLint 使⽤ AST 来评估代码中的模式。
- ESLint 是完全可插拔的,每⼀条规则都是⼀个插件,你可以在运⾏时添加更多。
扯⽪⼀下这些解析器的关系~~~
- esprima - 经典的解析器
- acorn - 造轮⼦媲美Esprima
- @babel/parser (babylon)基于acorn的
- espree 最初从Esprima中fork出来的,现在基于acorn
pnpm init
pnpm i eslint -D # 安装eslint
pnpm create @eslint/config # 初始化eslint的配置⽂件
⽣成的配置⽂件是:
module.exports = {
env: { // 指定环境
browser: true, // 浏览器环境 document
es2021: true, // ECMAScript语法 Promise
node: true, // node环境 require
},
extends: "eslint:recommended",
parserOptions: { // ⽀持语⾔的选项, ⽀持最新js语法,同时⽀持jsx语法
ecmaVersion: "latest", // ⽀持的语法是
sourceType: "module", // ⽀持模块化
ecmaFeatures: {
"jsx": true
}
},
rules: { // eslint规则
"semi": ["error", "always"],
"quotes": ["error", "double"]
},
globals: { // 配置全局变量
custom: "readonly" // readonly 、 writable
}
};
parser:可以指定使⽤何种parser来将code转换成estree。举例:转化ts语法
pnpm install @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
typescript -D
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended" // 集成规则和插件
],
"parser": "@typescript-eslint/parser" // 解析typescript
Eslint运⾏流程
Eslint 代码检查的过程
-
配置文件:ESLint使用一个配置文件(如.eslintrc文件)来定义规则集。这个文件是一个JSON格式的文件,其中包含了各种规则、插件和其他配置选项。开发者可以根据项目的需求和规范,自定义ESLint配置,以满足不同的代码风格和规范要求
-
解析代码:ESLint首先会将代码解析为抽象语法树(AST,Abstract Syntax Tree),这是一种将源代码语1 2法结构转换为数据结构和节点对象的表示方式,以便后续的分析和处理
- 解析输入代码:ESLint将输入的代码解析成一系列的token。Token是代码的基本组成单元,例如关键字、变量名、字符串、数字等
- 逐个检查代码中的每个token:ESLint会将解析后的token逐个送到规则集中进行检查,根据规则集判断token是否符合规则,并报告不符合规则的token及其原因
-
加载规则:ESLint会加载预定义的一系列规则,这些规则定义了代码应该符合的规范。开发者也可以根据项目需求自定义规则
-
执行规则:ESLint会遍历抽象语法树,根据规则对代码进行检查。当发现代码不符合规范时,ESLint会将检查结果以可读性良好的格式输出对应的问题信息,供开发者查看和处理
流程图
ESLint 核⼼API
- lintFiles 检验⽂件
- lintText 校验⽂本
- loadFormatter 加载formatter
- calculateConfigForFile 通过⽂件获取配置
- isPathIgnored 此路径是否是被忽略的
- static outputFixes 输出修复的⽂件
- static getErrorResults 获得错误结果
CLIEngine 脚⼿架核⼼
- getRules 获取规则
- resolveFileGlobPatterns 解析⽂件成glob模式
- executeOnFiles 根据⽂件执⾏逻辑
- executeOnText 根据⽂本执⾏逻辑
- getConfigForFile 获取⽂件的配置
- isPathIgnored 此路径是否是被忽略的
- getFormatter 获取输出的格式
- static getErrorResults 获取错误结果
- static outputFixes 输出修复的结果
Linter 校验js⽂本
- verifyAndFix 校验和修复
- verify 校验⽅法
- _verifyWithConfigArray 通过配置⽂件校验
- _verifyWithoutProcessors 校验没有processors
- _verifyWithProcessor 校验有processors
Eslint插件开发
Eslint官⽅提供了可以使⽤Yeoman 脚⼿架⽣成插件模板
npm install yo generator-eslint -g
模板初始化
mkdir eslint-plugin-zlint
cd eslint-plugin-zlint
yo eslint:plugin # 插件模板初始化
yo eslint:rule # 规则模版初始化
实现no-var
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"plugins": ['zlint'],
"rules": {
'zlint/no-var': ['error', 'always']
}
}
module.exports = {
meta: {
docs: {
description: "代码中不能出现var",
recommended: false,
},
fixable: "code",
messages: {
unexpectedVar: 'Unexpected var'
}
},
create(context) {
const sourceCode = context.getSourceCode();
return {
"VariableDeclaration:exit"(node) { // 如果类型是var,拦截var声明
if (node.kind === 'var') {
context.report({ // 报警告
node,
messageId: 'unexpectedVar',
fix(fixer) { // --fix
const varToken = sourceCode.getFirstToken(node, {
filter: t => t.value
=== 'var'
});
// 将var 转换成 let
return fixer.replaceText(varToken, 'let')
}
})
}
}
};
},
};
单元测试
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 'latest',
},
});
ruleTester.run("no-var", rule, {
valid: [
{
code: "let a = 1"
}
],
invalid: [
{
code: "var a = 1",
errors: [{
messageId: "unexpectedVar",
}],
output: 'let a = 1'
},
],
});
实现eqeqeq
module.exports = {
env: { // 指定环境
browser: true, // 浏览器环境
es2021: true, // ECMAScript2021
node: true, // node环境
},
plugins: ['zlint'],
rules: {
"zlint/eqeqeq": ['error', "always", { null: 'never' }]
}
}
module.exports = {
meta: {
type: 'suggestion', // `problem`, `suggestion`, or `layout`
docs: {
description: "尽量使⽤三等号",
recommended: false,
},
fixable: 'code',
schema: {
type: "array",
items: [
{
enum: ["always"]
},
{
type: "object",
properties: {
null: {
enum: ["always", "never"]
}
}
}
],
},
messages: {
unexpected: "期望 '{{expectedOperator}}' ⽬前是 '{{actualOperator}}'."
}
},
create(context) {
// 处理null的情况
const config = context.options[0] || 'always';
const options = context.options[1] || {};
const nullOption = (config === 'always') ? options.null || 'always' : ''
const enforceRuleForNull = (nullOption === 'never')
function isNullCheck(node) {
let rightNode = node.right;
let leftNode = node.left;
return (rightNode.type === 'Literal' && rightNode.value === null) ||
(leftNode.type === 'Literal' && leftNode.value === null)
}
const sourceCode = context.getSourceCode();
function report(node, expectedOperator) {
const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, {
filter: t => t.value === node.operator
});
// 左右两边是不是相同类型
function areLiteralsAndSameType(node) {
return node.left.type === "Literal" && node.right.type === "Literal" &&
typeof node.left.value === typeof node.right.value;
}
context.report({
node,
loc: operatorToken.loc,
data: { expectedOperator, actualOperator: node.operator },
messageId: 'unexpected',
fix(fixer) {
if (areLiteralsAndSameType(node)) {
return fixer.replaceText(operatorToken, expectedOperator);
}
return null
}
})
}
return {
BinaryExpression(node) {
const isNull = isNullCheck(node);
// 如果是两等号的情况
if (node.operator !== '==' && node.operator !== '!=') {
if (enforceRuleForNull && isNull) { // 当遇到null的时候不进⾏转换
report(node, node.operator.slice(0, -1));
}
return
}
report(node, `${node.operator}=`)
}
};
},
};
单元测试
const ruleTester = new RuleTester();
ruleTester.run("eqeqeq", rule, {
valid: [
{
code: '1===1'
}
],
invalid: [
{
code: "1==1",
errors: [{
data: { expectedOperator: '===', actualOperator: '==' },
messageId: 'unexpected',
}],
output: "1===1"
},
{
code: "null === undefined",
options: ["always", { null: 'never' }],
errors: [{
data: { expectedOperator: '==', actualOperator: '===' },
messageId: 'unexpected',
}],
},
],
});
实现no-console
rules: {
"zlint/no-console": ['warn', { allow: ['log'] }],
"zlint/no-var": 'error'
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: "禁⽌使⽤console",
recommended: false,
},
schema: [
{
type: 'object',
properties: {
allow: {
type: 'array',
items: {
type: 'string'
}
}
}
}
]
},
create(context) {
const allowed = context.options[0]?.allow || [];
function isMemberAccessExceptAllowed(reference) {
const node = reference.identifier
const parent = node.parent;
if (parent.type === 'MemberExpression') { // 获得⽗节点,看下属性名字
const { name } = parent.property;
return !allowed.includes(name);
}
}
function report(reference) {
const node = reference.identifier.parent; // console 表达式
context.report({
node,
loc: node.loc,
message: 'Unexpected console statement.'
})
}
return {
"Program:exit"() {
// 1. 当前作⽤域
let scope = context.getScope();
// 2. 获得console变量
const variable = scope.set.get('console');
// 3. 获得references
const references = variable.references;
references.filter(isMemberAccessExceptAllowed).forEach(report)
}
};
},
};
单元测试
const ruleTester = new RuleTester({
env: {
browser: true
}
});
ruleTester.run("no-console", rule, {
valid: [
{
code: "console.log('hello')",
options: [{ allow: ['log'] }]
}
],
invalid: [
{
code: "console.info('hello')",
errors: [{ message: "Unexpected console statement." }],
},
],
});
extends 使⽤
module.exports = {
rules: requireIndex(__dirname + "/rules"),
configs: {
// 导出⾃定义规则 在项⽬中直接引⽤
recommended: {
plugins: ['zlint'], // 引⼊插件
rules: {
// 开启规则
'zlint/eqeqeq': ['error', "always", { null: 'never' }],
'zlint/no-console': ['error', { allow: ['log'] }],
'zlint/no-var': 'error'
}
}
},
processors: {
'.vue': {
preprocess(code) {
console.log(code)
return [code]
},
postprocess(messages) {
console.log(messages)
return []
}
}
}
};
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
extends: [
'plugin:zlint/recommended' // 直接集成即可
]
}