使用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: [{ messa