共计 19355 个字符,预计需要花费 49 分钟才能阅读完成。
简介:eslint 是构建在 AST Parser 根底上的规定扫描器,缺省状况下应用 espree 作为 AST 解析器。rules 写好对于 AST 事件的回调,linter 解决源代码之后会依据相应的事件来回调 rules 中的处理函数。另外,在进入细节之前,请思考一下:eslint 的边界在哪里?哪些性能是通过 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);
}
}
};
1 获取作用域和 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 函数:
function report(reference) {
const node = reference.identifier.parent;
context.report({
node,
loc: node.loc,
messageId: "unexpected"
});
}
产生问题的代码行数能够从 node 中获取到。
2 解决特定类型的语句
no-console 从规定书写上并不是最容易的,咱们以其为例次要是这类问题最多。上面咱们触类旁通,看看针对其它不应该呈现的语句该如何解决。
其中最简略的就是针对一类语句通通报错,比方 no-continue 规定,就是遇到 ContinueStatement 就报错:
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow `continue` statements",
recommended: false,
url: "https://eslint.org/docs/rules/no-continue"
},
schema: [],
messages: {unexpected: "Unexpected use of continue statement."}
},
create(context) {
return {ContinueStatement(node) {context.report({ node, messageId: "unexpected"});
}
};
}
};
不容许应用 debugger 的 no-debugger 规定:
create(context) {
return {DebuggerStatement(node) {
context.report({
node,
messageId: "unexpected"
});
}
};
}
不许应用 with 语句:
create(context) {
return {WithStatement(node) {context.report({ node, messageId: "unexpectedWith"});
}
};
}
在 case 语句中不许定义变量、函数和类:
create(context) {function isLexicalDeclaration(node) {switch (node.type) {
case "FunctionDeclaration":
case "ClassDeclaration":
return true;
case "VariableDeclaration":
return node.kind !== "var";
default:
return false;
}
}
return {SwitchCase(node) {for (let i = 0; i < node.consequent.length; i++) {const statement = node.consequent[i];
if (isLexicalDeclaration(statement)) {
context.report({
node: statement,
messageId: "unexpected"
});
}
}
}
};
}
多个类型语句能够共用一个处理函数。
比方不许应用构造方法生成数组:
function check(node) {
if (
node.arguments.length !== 1 &&
node.callee.type === "Identifier" &&
node.callee.name === "Array"
) {context.report({ node, messageId: "preferLiteral"});
}
}
return {
CallExpression: check,
NewExpression: check
};
不许给类定义赋值:
create(context) {function checkVariable(variable) {astUtils.getModifyingReferences(variable.references).forEach(reference => {context.report({ node: reference.identifier, messageId: "class", data: { name: reference.identifier.name} });
});
}
function checkForClass(node) {context.getDeclaredVariables(node).forEach(checkVariable);
}
return {
ClassDeclaration: checkForClass,
ClassExpression: checkForClass
};
}
函数的参数不容许重名:
create(context) {function isParameter(def) {return def.type === "Parameter";}
function checkParams(node) {const variables = context.getDeclaredVariables(node);
for (let i = 0; i < variables.length; ++i) {const variable = variables[i];
const defs = variable.defs.filter(isParameter);
if (defs.length >= 2) {
context.report({
node,
messageId: "unexpected",
data: {name: variable.name}
});
}
}
}
return {
FunctionDeclaration: checkParams,
FunctionExpression: checkParams
};
}
如果事件太多的话,能够写成一个数组,这被称为选择器数组:
const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
...
[loopSelector](node) {if (currentCodePath.currentSegments.some(segment => segment.reachable)) {loopsToReport.add(node);
}
},
除了间接解决语句类型,还能够针对类型加上一些额定的判断。
比方不容许应用 delete 运算符:
create(context) {
return {UnaryExpression(node) {if (node.operator === "delete" && node.argument.type === "Identifier") {context.report({ node, messageId: "unexpected"});
}
}
};
}
不准应用 ”==” 和 ”!=” 运算符:
create(context) {
return {BinaryExpression(node) {
const badOperator = node.operator === "==" || node.operator === "!=";
if (node.right.type === "Literal" && node.right.raw === "null" && badOperator ||
node.left.type === "Literal" && node.left.raw === "null" && badOperator) {context.report({ node, messageId: "unexpected"});
}
}
};
}
不许和 - 0 进行比拟:
create(context) {function isNegZero(node) {return node.type === "UnaryExpression" && node.operator === "-" && node.argument.type === "Literal" && node.argument.value === 0;}
const OPERATORS_TO_CHECK = new Set([">", ">=", "<", "<=", "==", "===", "!=", "!=="]);
return {BinaryExpression(node) {if (OPERATORS_TO_CHECK.has(node.operator)) {if (isNegZero(node.left) || isNegZero(node.right)) {
context.report({
node,
messageId: "unexpected",
data: {operator: node.operator}
});
}
}
}
};
}
不准给常量赋值:
create(context) {function checkVariable(variable) {astUtils.getModifyingReferences(variable.references).forEach(reference => {context.report({ node: reference.identifier, messageId: "const", data: { name: reference.identifier.name} });
});
}
return {VariableDeclaration(node) {if (node.kind === "const") {context.getDeclaredVariables(node).forEach(checkVariable);
}
}
};
}
3 :exit – 语句完结事件
除了语句事件之外,eslint 还提供了:exit 事件。
比方下面的例子咱们应用了 VariableDeclaration 语句事件,咱们上面看看如何应用 VariableDeclaration 完结时调用的 VariableDeclaration:exit 事件。
咱们看一个不容许应用 var 定义变量的例子:
return {"VariableDeclaration:exit"(node) {if (node.kind === "var") {report(node);
}
}
};
如果感觉进入和退出不好辨别的话,咱们来看一个不容许在非函数的块中应用 var 来定义变量的例子:
BlockStatement: enterScope,
"BlockStatement:exit": exitScope,
ForStatement: enterScope,
"ForStatement:exit": exitScope,
ForInStatement: enterScope,
"ForInStatement:exit": exitScope,
ForOfStatement: enterScope,
"ForOfStatement:exit": exitScope,
SwitchStatement: enterScope,
"SwitchStatement:exit": exitScope,
CatchClause: enterScope,
"CatchClause:exit": exitScope,
StaticBlock: enterScope,
"StaticBlock:exit": exitScope,
这些逻辑的作用是,进入语句块的时候调用 enterScope,退出语句块的时候调用 exitScope:
function enterScope(node) {stack.push(node.range);
}
function exitScope() {stack.pop();
}
4 间接应用文字信息 – Literal
比方不容许应用 ”-.7″ 这样省略了 0 的浮点数。此时应用 Literal 来解决纯文字信息。
create(context) {const sourceCode = context.getSourceCode();
return {Literal(node) {if (typeof node.value === "number") {if (node.raw.startsWith(".")) {
context.report({
node,
messageId: "leading",
fix(fixer) {const tokenBefore = sourceCode.getTokenBefore(node);
const needsSpaceBefore = tokenBefore &&
tokenBefore.range[1] === node.range[0] &&
!astUtils.canTokensBeAdjacent(tokenBefore, `0${node.raw}`);
return fixer.insertTextBefore(node, needsSpaceBefore ? "0" : "0");
}
});
}
if (node.raw.indexOf(".") === node.raw.length - 1) {
context.report({
node,
messageId: "trailing",
fix: fixer => fixer.insertTextAfter(node, "0")
});
}
}
}
};
}
不准应用八进制数字:
create(context) {
return {Literal(node) {if (typeof node.value === "number" && /^0[0-9]/u.test(node.raw)) {
context.report({
node,
messageId: "noOcatal"
});
}
}
};
}
三 代码路径分析
后面咱们探讨的根本都是一个代码片段,当初咱们把代码逻辑串起来,造成一条代码门路。
代码门路就不止只有程序构造,还有分支和循环。
除了采纳下面的事件处理办法之外,咱们还能够针对 CodePath 事件进行解决:
事件 onCodePathStart 和 onCodePathEnd 用于整个门路的剖析,而 onCodePathSegmentStart, onCodePathSegmentEnd 是 CodePath 中的一个片段,onCodePathSegmentLoop 是循环片段。
咱们来看一个循环的例子:
create(context) {const ignoredLoopTypes = context.options[0] && context.options[0].ignore || [],
loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes),
loopSelector = loopTypesToCheck.join(","),
loopsByTargetSegments = new Map(),
loopsToReport = new Set();
let currentCodePath = null;
return {onCodePathStart(codePath) {currentCodePath = codePath;},
onCodePathEnd() {currentCodePath = currentCodePath.upper;},
[loopSelector](node) {if (currentCodePath.currentSegments.some(segment => segment.reachable)) {loopsToReport.add(node);
}
},
onCodePathSegmentStart(segment, node) {if (isLoopingTarget(node)) {
const loop = node.parent;
loopsByTargetSegments.set(segment, loop);
}
},
onCodePathSegmentLoop(_, toSegment, node) {const loop = loopsByTargetSegments.get(toSegment);
if (node === loop || node.type === "ContinueStatement") {loopsToReport.delete(loop);
}
},
"Program:exit"() {
loopsToReport.forEach(node => context.report({ node, messageId: "invalid"})
);
}
};
}
四 提供问题主动修复的代码
最初,咱们讲讲如何给问题给供主动修复代码。
咱们之前报告问题都是应用 context.report 函数,主动修复代码也是通过这个接口返回给调用者。
咱们以将 ”==” 和 ”!=” 替换成 ”===” 和 ”!==” 为例。
这个 fix 没有多少技术含量哈,就是给原来发现问题的运算符多加一个 ”=”:
report(node,
${node.operator}=);
最终实现时是调用了 fixer 的 replaceText 函数:
fix(fixer) {if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {return fixer.replaceText(operatorToken, expectedOperator);
}
return null;
}
残缺的 report 代码如下:
function report(node, expectedOperator) {
const operatorToken = sourceCode.getFirstTokenBetween(
node.left,
node.right,
token => token.value === node.operator
);
context.report({
node,
loc: operatorToken.loc,
messageId: "unexpected",
data: {expectedOperator, actualOperator: node.operator},
fix(fixer) {if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {return fixer.replaceText(operatorToken, expectedOperator);
}
return null;
}
});
}
Fixer 反对 4 个增加 API,2 个删除 API,2 个替换类的 API:
五 高级话题
1 React JSX 的反对
Facebook 给咱们封装好了框架,写起来也是蛮眼生的。刚好之前没有举 markVariableAsUsed 的例子,正好一起看了:
module.exports = {
meta: {
docs: {
description: 'Prevent React to be marked as unused',
category: 'Best Practices',
recommended: true,
url: docsUrl('jsx-uses-react'),
},
schema: [],},
create(context) {const pragma = pragmaUtil.getFromContext(context);
const fragment = pragmaUtil.getFragmentFromContext(context);
function handleOpeningElement() {context.markVariableAsUsed(pragma);
}
return {
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
JSXFragment() {context.markVariableAsUsed(fragment);
},
};
},
};
JSX 的非凡之处是减少了 JSXOpenElement, JSXClosingElement, JSXOpenFragment, JSXClosingFragment 等解决 JSX 的事件。
2 TypeScript 的反对
随着 tslint 合并到 eslint 中,TypeScript 的 lint 性能由 typescript-eslint 承载。
因为 estree 只反对 javascript,typescript-eslint 提供兼容 estree 格局的 parser.
既然是 ts 的 lint,天然是领有了 ts 的反对,领有了新的工具办法,其根本架构仍是和 eslint 统一的:
import * as ts from 'typescript';
import * as util from '../util';
export default util.createRule({
name: 'no-for-in-array',
meta: {
docs: {
description: 'Disallow iterating over an array with a for-in loop',
recommended: 'error',
requiresTypeChecking: true,
},
messages: {
forInViolation:
'For-in loops over arrays are forbidden. Use for-of or array.forEach instead.',
},
schema: [],
type: 'problem',
},
defaultOptions: [],
create(context) {
return {ForInStatement(node): void {const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const type = util.getConstrainedTypeAtLocation(
checker,
originalNode.expression,
);
if (util.isTypeArrayTypeOrUnionOfArrayTypes(type, checker) ||
(type.flags & ts.TypeFlags.StringLike) !== 0
) {
context.report({
node,
messageId: 'forInViolation',
});
}
},
};
},
});
3 更换 ESLint 的 AST 解析器
ESLint 反对应用第三方 AST 解析器,刚好 Babel 也反对 ESLint,于是咱们就能够用 @babel/eslint-parser 来替换 espree. 装好插件之后,批改.eslintrc.js 即可:
module.exports = {parser: "@babel/eslint-parser",};
Babel 自带反对 TypeScript。
六 StyleLint
说完了 Eslint,咱们再花一小点篇幅看下 StyleLint。
StyleLint 与 Eslint 的架构思维一脉相承,都是对于 AST 的事件剖析进行解决的工具。
只不过 css 应用不同的 AST Parser,比方 Post CSS API, postcss-value-parser, postcss-selector-parser 等。
咱们来看个例子体感一下:
const rule = (primary) => {return (root, result) => {const validOptions = validateOptions(result, ruleName, { actual: primary});
if (!validOptions) {return;}
root.walkDecls((decl) => {const parsedValue = valueParser(getDeclarationValue(decl));
parsedValue.walk((node) => {if (isIgnoredFunction(node)) return false;
if (!isHexColor(node)) return;
report({message: messages.rejected(node.value),
node: decl,
index: declarationValueIndex(decl) + node.sourceIndex,
result,
ruleName,
});
});
});
};
};
也是相熟的 report 函数回报,也能够反对 autofix 的生成。
七 小结
以上,咱们根本将 eslint 规定写法的大抵框架梳理分明了。当然,理论写规刚的过程中还须要对于 AST 以及语言细节有比拟深的理解。预祝大家通过写出适宜本人业务的查看器,写出更强壮的代码。
原文链接
本文为阿里云原创内容,未经容许不得转载。