说明:ESlint 是 JavaScript 相关开发的静态代码分析工具。本文通过源码层级介绍 ESLint 核心类/方法简要介绍ESLint工作流程。官方文档内容详实,建议先通读官方文档再阅读本文。
一、架构设计
(一) 核心类
ESLint 项目结构划分清晰,基本一个文件对应一个核心类,主要有:
-
ESLint : 用于启动ESLint,封装处理/传递命令行参数的类。
-
CLIEngine :提供代码检查(executeOnFiles、executeOnText)、获取配置信息(getConfigForFile)和自定义代码检验Rule (getRules)等接口。
-
*Linter:具体执行AST解析代码(parse)、遍历处理规则(runRules)、代码检验的类
-
SourceCode:保存原文件内容(text, lines, lineStartIndices)、AST数据结构(ast)的辅助类。
(二)扩展定义
扩展定义即用户可以自定义添加的内容,包括Rule(检验规则)、Plugin(规则集合)、extends(默认配置项)。这里重点说明官方文档中没有的内容。
1. Rule
其中create 返回监听函数对象,其中key为监听事件 或 节点:
- 事件:onCodePathStart, onCodePathEnd, onCodePathSegmentStart, onCodePathSegmentEnd, onCodePathSegmentLoop;
- 节点为 estree 中的节点类型,详情可查看 eslint-visitor-keys(或 estree定义),其中node对应词法分析中具体token
- 遍历顺序为:Code Path Analysis Details,结合事件举例请看下文Code Path
module.exports = {
meta: {
type: "suggestion", // 规则的类型:"problem", "layout"
messages: { // string or {}, 展示messageId
avoidName: "Avoid using variables named '{{ name }}'"
},
doc: {
description: "",
categrpy: "",
recommended: true, // 配置中 "extends": "eslint:recommended"启用
url: ""
},
schema: [],
fixable: "code", // 是否应用 fix 函数
// context 为 linter.js 中 runRules 创建的上下文,下文详述
create: function (context) {
// 返回遍历 AST 节点时触发的 节点类型 或 事件 定义的函数
return {
"onCodePathSegmentStart": function(segment, node){},
"ExpressionStatement:exit": function(node) {}
}
}
}
}
2. Plugin
命名格式为 "eslint-plugin-" 的npm模块,在实际引用时可将"eslint-plugin-" 省略,具体内容可参考 eslint-plugin-vue
module.exports = {
// 开发者定义的所有rule
rules: {
'rule-name': require('./rules/test')
},
// 模块开发者定义的规则集
config: {
base: require('./config/base'),
recommended: require('')
},
// 为不同文件定义处理函数,包括 preprocess, postprocess两部分
// 具体在 linter.js 的 _verifyWithProcessor 函数中执行
processors: {
'.vue': require('./processor')
},
enviroments: {
'setup-compiler-macros': {
globals: {
defineProps: 'readonly'
}
}
}
}
3. Extends
配置应用的plugin对象的定义集,如 eslint:recommend, eslint:all
4. 全局配置文件,如 .eslintrc.js
// config-airbnb 中 './rules/react'
module.exports = {
plugins: [
'react',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
rules: {
'jsx-quotes': ['error', 'prefer-double'],
...
},
// 下文中 plugins 的共享配置
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.json']
}
},
...
}
}
二、运行流程
(一)整体流程
发现参考文章的图画的简单明了,此处直接引用。为方便个人记忆,增加了较详细的函数版本(各个类实例之间竟然是通过 WeakMap 映射的...)
(二)runRules
ESLint 核心步骤就是执行规则检查,下文通过 简化代码+注释 说明函数执行含义,主要步骤为:
-
按遍历顺序收集 node
-
遍历rule执行create函数,在emitter中绑定相应的监听事件
-
按收集node顺序遍历队列,并触发所有的监听事件
function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) {
// 自定义事件监听/触发器 { on, emit, eventNames }
const emitter = createEmitter();
// 保存 node 遍历队列, 遍历顺序为上文的 Code Path Analysis Details
const nodeQueue = [];
let currentNode = sourceCode.ast;
// 保存 node 的 enter 和 leave 位置
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
// 触发的监听key
visitorKeys: sourceCode.visitorKeys
});
// 定义ESLint 共享的Context
const sharedTraversalContext = Object.freeze(
Object.assign(
Object.create(BASE_TRAVERSAL_CONTEXT),
{
getAncestors: () => getAncestors(currentNode),
getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
getCwd: () => cwd,
getFilename: () => filename,
getPhysicalFilename: () => physicalFilename || filename,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
// *重点
getSourceCode: () => sourceCode,
markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name),
parserOptions: {
...languageOptions.parserOptions
},
parserPath: parserName,
languageOptions,
parserServices: sourceCode.parserServices,
settings
}
)
);
// 保存规则检查的 problems
const lintingProblems = [];
// 遍历所有rule规则
Object.keys(configuredRules).forEach(ruleId => {
// ...
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
// 绑定单个 rule 上下文,包括id, option, report函数
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
report(...args) {
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes
});
}
const problem = reportTranslator(...args);
// ...
lintingProblems.push(problem);
}
}
)
);
// 执行 rule 的 create函数
const ruleListeners = createRuleListeners(rule, ruleContext);
function addRuleErrorHandler(ruleListener) {
return function ruleErrorHandler(...listenerArgs) {
try {
return ruleListener(...listenerArgs);
} catch (e) {
e.ruleId = ruleId;
throw e;
}
};
}
// 设置单个 rule 所有的监听事件
Object.keys(ruleListeners).forEach(selector => {
const ruleListener = timing.enabled
? timing.time(ruleId, ruleListeners[selector])
: ruleListeners[selector];
emitter.on(selector, addRuleErrorHandler(ruleListener));
});
});
const eventGenerator = nodeQueue[0].node.type === "Program"
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
// * 按 AST 遍历顺序触发相应的监听事件:
// 其中 CodePathAnalyzer 类的 enterNode 执行preprocess 函数(非processors定义的process), 处理codePath相关内容,
// 并触发 onCodePathStart/onCodePathSegmentStart, 而后触发NodeEventGenerator 的 enterNode函数;
// leaveNode 的postprocess触发 onCodePathStart/onCodePathSegmentStart 事件
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
try {
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
eventGenerator.leaveNode(currentNode);
}
} catch (err) {
err.currentNode = currentNode;
throw err;
}
});
return lintingProblems;
}
(三)Code Path
代码展示事件顺序:
// onCodePathStart-------
// onCodePathSegmentStart=======
let i = 0;
// onCodePathSegmentEnd=======
// onCodePathSegmentStart=======
while(
// test: 判断语句
i < 3
// onCodePathSegmentEnd=======
// onCodePathSegmentStart=======
) {
i++;
// onCodePathSegmentLoop************
// onCodePathSegmentEnd=======
}
结合node遍历:
code-path:创建(更新)分支逻辑:preprocess, processCodePathToExit
// preprocess: 根据父节点类型更新路径(创建分支)
// processCodePathToExit: 根据节点类型更新路径
// 即构造成上图的路径图(多种情况,需要细化)
三、自定义rule
自定义消除代码中 console.time 的规则
//-----------------------------------------------------------------------
// Rule Definition
// 命名约定:
// 如果你的规则是禁止什么,加前缀 no-,比如 no-eval 禁用 eval(),no-debugger禁用debugger
// 如果你规则是强制包含什么,使用一个简短的名称,不带特殊的前缀。
// 在单词之间使用连字符
//-----------------------------------------------------------------------
/**
* @type {import('eslint').Rule.RuleModule}
* working with rules: https://cn.eslint.org/docs/developer-guide/working-with-rules
*/
module.exports = {
meta: {
type: "suggestion", // `problem`, `suggestion`, or `layout`
docs: {
description: "no console.time()",
category: "Fill me in",
recommended: false,
url: null, // URL to the documentation page for this rule
},
// 如果没有 fixable 属性,即使规则实现了 fix 功能,ESLint 也不会进行修复
// fixable: null, // Or `code` or `whitespace`
fixable: "code",
// 为options规则, 避免无效规则配置
// 示例: 接受一个参数
schema: [
{
type: "array", // 参数类型为数组,
items: {
type: "string", // 数组的每一项为字符串
},
},
], // Add a schema if the rule has options,
// 报错信息描述
messages: {
avoidMethod: "conosole method '{{name}}' is forbidden.'",
},
},
// context: https://cn.eslint.org/docs/user-guide/configuring#specifying-parser-options
create(context) {
// .parserOptions // 对应 .eslintrc.* 的 parserOptions 选项
// .id // rule 的 ID
// .options // 对应上文rule定义的 options
// { "quotes": ["error", "double"] } => context.options[0] === "double"
// .settings // 个屏幕共享的其阿米Setting
// .parserPath // 定义 parser 的名称
// .getAncestors() // 获取AST的父node数组
// .getCwd() // 获取当前路径
// .getDeclaredVariables(node) // 获取变量声明
// .getFilename() // returns the filename associated with the source
// .getPhysicalFilename() // 返回链接文件的完整路径
// 返回一个SourceCode 对象: https://cn.eslint.org/docs/developer-guide/working-with-rules#contextgetsourcecode
// .getSourceCode()
// .markVariableAsUsed(name) // 在当前作用域内用给定的名称标记一个变量
// 获取非 AST 的 comment部分
// sourceCode.getAllComments()
// sourceCode.getCommentsBefore(), .getCommentsAfter(), .getCommentsInside()
// .getScope() // 返回当前遍历节点的 scope
// ASTNode Type: https://cn.eslint.org/docs/developer-guide/working-with-rules#contextgetscope
// ASTNode定义: https://github.com/estree/estree, 规范: https://hexdocs.pm/estree/ESTree.ClassDeclaration.html#content
// .report(descriptor)
// node: 节点
// message: 传递消息
/**
* fix: function(fixer) {} // 修复函数: 文件内容会被解析为字符串text
* insertTextAfter(nodeOrToken, text)
* insertTextAfterRange(range, text)
* insertTextBefore(nodeOrToken, text)
* insertTextBeforeRange(range, text)
* remove(nodeOrToken)
* removeRange(range)
* replaceText(nodeOrToken, text)
* replaceTextRange(range, text)
*
* 返回一个fixing对象, 包含fixing对象的数组
* */
return {
// visitor functions for different types of nodes
// 1. 遍历节点(进入或退出时)
// 2. 触发事件: onCodePathStart, onCodePathEnd, onCodePathSegmentStart, onCodePathSegmentEnd, onCodePathSegmentLoop
// 以ast中类名为键名的选择函数
"CallExpression MemberExpression": (node) => {
// console.log("node", node);
// console.log("context", context);
// 如果在ast中满足一下条件,就用 context.report() 进行对外警告
if (node.property.name === "time" && node.object.name === "console") {
context.report({
node,
messageId: "avoidMethod",
data: {
name: "time",
},
// 修复: 返回更新后的 Array<string>
// fix(fixer) {
// const fixes = []
// const domainNode = (node.type === "Literal" && node) ||
// (node.type === "TemplateLiteral" && node.quasis[0]);
// fixes.push( fixer.replaceTextRange(
// [domainNode.range[0] + 1, domainNode.range[1] - 1],
// "replace string"
// ))
// return fixes
// }
});
}
},
// 文件开始的 Program 节点的 start
onCodePathStart: function (codePath, node) {
console.log('onCodePathStart-------');
},
onCodePathEnd: function (codePath, node) {
console.log('onCodePathEnd-------')
},
onCodePathSegmentStart: function (segment, node) {
console.log('onCodePathSegmentStart=======')
},
onCodePathSegmentEnd: function (segment, node) {
console.log('onCodePathSegmentEnd=======')
},
onCodePathSegmentLoop: function (segment,toSegment, node) {
console.log('onCodePathSegmentLoop************')
},
};
},
};