自己动手写符合自己业务需求的eslint规则
Posted Jtag特工
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己动手写符合自己业务需求的eslint规则相关的知识,希望对你有一定的参考价值。
自己动手写符合自己业务需求的eslint规则
使用eslint和stylelint之类的工具扫描前端代码现在已经基本成为前端同学的标配。但是,业务这么复杂,指望eslint等提供的工具完全解决业务中遇到的代码问题还是不太现实的。我们一线业务同学也要有自己的写规则的能力。
eslint是构建在AST Parser基础上的规则扫描器,缺省情况下使用espree作为AST解析器。rules写好对于AST事件的回调,linter处理源代码之后会根据相应的事件来回调rules中的处理函数。
另外,在进入细节之前,请思考一下:eslint的边界在哪里?哪些功能是通过eslint写规则可以做到的,哪些是用eslint无法做到的?
先学会如何写规则测试
兵马未动,测试先行。规则写出来,如何用实际代码进行测试呢?
所幸非常简单,直接写个json串把代码写进来就好了。
我们来看个no-console的例子,就是不允许代码中出现console.*语句的规则。
首先把规则和测试运行对象ruleTester引进来:
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const rule = require("../../../lib/rules/no-console"),
RuleTester = require("../../../lib/rule-tester");
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester();
然后我们就直接调用ruleTester的run函数就好了。有效的样例放在valid下面,无效的样例放在invalid下面,是不是很简单。
我们先看下有效的:
ruleTester.run("no-console", rule,
valid: [
"Console.info(foo)",
// single array item
code: "console.info(foo)", options: [ allow: ["info"] ] ,
code: "console.warn(foo)", options: [ allow: ["warn"] ] ,
code: "console.error(foo)", options: [ allow: ["error"] ] ,
code: "console.log(foo)", options: [ allow: ["log"] ] ,
// multiple array items
code: "console.info(foo)", options: [ allow: ["warn", "info"] ] ,
code: "console.warn(foo)", options: [ allow: ["error", "warn"] ] ,
code: "console.error(foo)", options: [ allow: ["log", "error"] ] ,
code: "console.log(foo)", options: [ allow: ["info", "log", "warn"] ] ,
// https://github.com/eslint/eslint/issues/7010
"var console = require('myconsole'); console.log(foo)"
],
能通过的情况比较容易,我们就直接给代码和选项就好。
然后是无效的:
invalid: [
// no options
code: "console.log(foo)", errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.error(foo)", errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.info(foo)", errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.warn(foo)", errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
// one option
code: "console.log(foo)", options: [ allow: ["error"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.error(foo)", options: [ allow: ["warn"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.info(foo)", options: [ allow: ["log"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.warn(foo)", options: [ allow: ["error"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
// multiple options
code: "console.log(foo)", options: [ allow: ["warn", "info"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.error(foo)", options: [ allow: ["warn", "info", "log"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.info(foo)", options: [ allow: ["warn", "error", "log"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
code: "console.warn(foo)", options: [ allow: ["info", "log"] ], errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
// In case that implicit global variable of 'console' exists
code: "console.log(foo)", env: node: true , errors: [ messageId: "unexpected", type: "MemberExpression" ]
]
);
无效的要判断下出错信息是不是符合预期。
我们使用mocha运行下上面的测试脚本:
./node_modules/.bin/mocha tests/lib/rules/no-console.js
运行结果如下:
no-console
valid
✓ Console.info(foo)
✓ console.info(foo)
✓ console.warn(foo)
✓ console.error(foo)
✓ console.log(foo)
✓ console.info(foo)
✓ console.warn(foo)
✓ console.error(foo)
✓ console.log(foo)
✓ var console = require('myconsole'); console.log(foo)
invalid
✓ console.log(foo)
✓ console.error(foo)
✓ console.info(foo)
✓ console.warn(foo)
✓ console.log(foo)
✓ console.error(foo)
✓ console.info(foo)
✓ console.warn(foo)
✓ console.log(foo)
✓ console.error(foo)
✓ console.info(foo)
✓ console.warn(foo)
✓ console.log(foo)
23 passing (83ms)
如果在valid里面放一个不能通过的,则会报错,比如我们加一个:
ruleTester.run("no-console", rule,
valid: [
"Console.info(foo)",
// single array item
code: "console.log('Hello,World')", options: [] ,
就会报下面的错:
1 failing
1) no-console
valid
console.log('Hello,World'):
AssertionError [ERR_ASSERTION]: Should have no errors but had 1: [
ruleId: 'no-console',
severity: 1,
message: 'Unexpected console statement.',
line: 1,
column: 1,
nodeType: 'MemberExpression',
messageId: 'unexpected',
endLine: 1,
endColumn: 12
]
+ expected - actual
-1
+0
at testValidTemplate (lib/rule-tester/rule-tester.js:697:20)
at Context.<anonymous> (lib/rule-tester/rule-tester.js:972:29)
at processImmediate (node:internal/timers:464:21)
说明我们刚加的console是会报一个messageId为unexpected,而nodeType为MemberExpression的错误。
我们应将其放入到invalid里面:
invalid: [
// no options
code: "console.log('Hello,World')", errors: [ messageId: "unexpected", type: "MemberExpression" ] ,
再运行,就可以成功了:
invalid
✓ console.log('Hello,World')
规则入门
会跑测试之后,我们就可以写自己的规则啦。
我们先看下规则的模板,其实主要要提供meta对象和create方法:
module.exports =
meta:
type: "规则类型,如suggestion",
docs:
description: "规则描述",
category: "规则分类:如Possible Errors",
recommended: true,
url: "说明规则的文档地址,如https://eslint.org/docs/rules/no-extra-semi"
,
fixable: "是否可以修复,如code",
schema: [] // 选项
,
create: function(context)
return
// 事件回调
;
;
总体来说,一个eslint规则所能做的事情,就是写事件回调函数,在回调函数中使用context中获取的AST等信息进行分析。
context提供的API是比较简洁的:
代码信息类主要我们使用getScope获取作用域的信息,getAncestors获取上一级AST节点,getDeclaredVariables获取变量表。最后的绝招是直接获取源代码getSourceCode自己分析去。
markVariableAsUsed用于跨文件分析,用于分析变量的使用情况。
report函数用于输出分析结果,比如报错信息、修改建议和自动修复的代码等。
这么说太抽象了,我们来看例子。
还以no-console为例,我们先看meta部分,这部分不涉及逻辑代码,都是一些配置:
meta:
type: "suggestion",
docs:
description: "disallow the use of `console`",
recommended: false,
url: "https://eslint.org/docs/rules/no-console"
,
schema: [
type: "object",
properties:
allow:
type: "array",
items:
type: "string"
,
minItems: 1,
uniqueItems: true
,
additionalProperties: false
],
messages:
unexpected: "Unexpected console statement."
,
我们再看no-console的回调函数,只处理一处Program:exit, 这是程序退出的事件:
return
"Program:exit"()
const scope = context.getScope();
const consoleVar = astUtils.getVariableByName(scope, "console");
const shadowed = consoleVar && consoleVar.defs.length > 0;
/*
* 'scope.through' includes all references to undefined
* variables. If the variable 'console' is not defined, it uses
* 'scope.through'.
*/
const references = consoleVar
? consoleVar.references
: scope.through.filter(isConsole);
if (!shadowed)
references
.filter(isMemberAccessExceptAllowed)
.forEach(report);
;
获取作用域和AST信息
我们首先通过context.getScope()获取作用域信息。作用域与AST的对应关系如下图:
我们前面的console语句的例子,首先拿到的都是全局作用域,举例如下:
<ref *1> GlobalScope
type: 'global',
set: Map(38)
'Array' => Variable
name: 'Array',
identifiers: [],
references: [],
defs: [],
tainted: false,
stack: true,
scope: [Circular *1],
eslintImplicitGlobalSetting: 'readonly',
eslintExplicitGlobal: false,
eslintExplicitGlobalComments: undefined,
writeable: false
,
'Boolean' => Variable
name: 'Boolean',
identifiers: [],
references: [],
defs: [],
tainted: false,
stack: true,
scope: [Circular *1],
eslintImplicitGlobalSetting: 'readonly',
eslintExplicitGlobal: false,
eslintExplicitGlobalComments: undefined,
writeable: false
,
'constructor' => Variable
name: 'constructor',
identifiers: [],
references: [],
defs: [],
tainted: false,
stack: true,
scope: [Circular *1],
eslintImplicitGlobalSetting: 'readonly',
eslintExplicitGlobal: false,
eslintExplicitGlobalComments: undefined,
writeable: false
,
...
具体看一下38个全局变量,复习下javascript基础吧:
set: Map(38)
'Array' => [Variable],
'Boolean' => [Variable],
'constructor' => [Variable],
'Date' => [Variable],
'decodeURI' => [Variable],
'decodeURIComponent' => [Variable],
'encodeURI' => [Variable],
'encodeURIComponent' => [Variable],
'Error' => [Variable],
'escape' => [Variable],
'eval' => [Variable],
'EvalError' => [Variable],
'Function' => [Variable],
'hasOwnProperty' => [Variable],
'Infinity' => [Variable],
'isFinite' => [Variable],
'isNaN' => [Variable],
'isPrototypeOf' => [Variable],
'JSON' => [Variable],
'Math' => [Variable],
'NaN' => [Variable],
'Number' => [Variable],
'Object' => [Variable],
'parseFloat' => [Variable],
'parseInt' => [Variable],
'propertyIsEnumerable' => [Variable],
'RangeError' => [Variable],
'ReferenceError' => [Variable],
'RegExp' => [Variable],
'String' => [Variable],
'SyntaxError' => [Variable],
'toLocaleString' => [Variable],
'toString' => [Variable],
'TypeError' => [Variable],
'undefined' => [Variable],
'unescape' => [Variable],
'URIError' => [Variable],
'valueOf' => [Variable]
,
我们看到,所有的变量,都以一个名为set的Map中,这样我们就可以以遍历获取所有的变量。
针对no-console的规则,我们主要是要查找是否有叫console的变量名。于是可以这么写:
getVariableByName(initScope, name)
let scope = initScope;
while (scope)
const variable = scope.set.get(name);
if (variable)
return variable;
scope = scope.upper;
return null;
,
我们可以在刚才列出的38个变量中发现,console是并没有定义的变量,所以
const consoleVar = astUtils.getVariableByName(scope, "console");
的结果是null.
于是我们要去查找未定义的变量,这部分是在scope.through中,果然找到了name是console的节点:
[
Reference
identifier: Node
type: 'Identifier',
loc: [SourceLocation],
range: [Array],
name: 'console',
parent: [Node]
,
from: <ref *2> GlobalScope
type: 'global',
set: [Map],
taints: Map(0) ,
dynamic: true,
block: [Node],
through: [Circular *1],
variables: [Array],
references: [Array],
variableScope: [Circular *2],
functionExpressionScope: false,
directCallToEvalScope: false,
thisFound: false,
__left: null,
upper: null,
isStrict: false,
childScopes: [],
__declaredVariables: [WeakMap],
implicit: [Object]
,
tainted: false,
resolved: null,
flag: 1,
__maybeImplicitGlobal: undefined
]
这样我们就可以写个检查reference的名字是不是console的函数就好:
function isConsole(reference)
const id = reference.identifier;
return id && id.name === "console";
然后用这个函数去filter scope.though中的所有未定义的变量:
scope.through.filter(isConsole);
最后一步是输出报告,针对过滤出的reference进行报告:
references
.filter(isMemberAccessExceptAllowed)
.forEach(report);
报告问题使用context的report函数:
自己动手写符合自己业务需求的eslint规则