自己动手写符合自己业务需求的eslint规则
简介: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以及语言细节有比较深的了解。预祝大家通过写出适合自己业务的检查器,写出更健壮的代码。
原文链接
本文为阿里云原创内容,未经允许不得转载。
自己动手写符合自己业务需求的eslint规则的更多相关文章
- 自己动手写插件底层篇—基于jquery移动插件实现
序言 本章作为自己动手写插件的第一篇文章,会尽可能的详细描述一些实现的方式和预备知识的讲解,随着知识点积累的一点点深入,可能到了后期讲解也会有所跳跃.所以,希望知识点不是很扎实的读者或者是初学者,不要 ...
- DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?
写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...
- 【原创】自己动手写工具----XSmartNote [Beta 2.0]
一.前面的话 在上一篇自己动手写工具----XSmartNote中,我简单介绍了这个小玩意儿的大致界面和要实现的功能,看了一下园子里的评论,评价褒贬不一,有人说“现在那么多云笔记的工具”,“极简版ev ...
- 【原创】自己动手写工具----签到器[Beta 2.0]
一.前面的话 上一篇中基本实现了简单的签到任务,但是不够灵活.在上一篇自己动手写工具----签到器的结尾中,我设想了几个新增功能来提高工具的灵活程度,下面把新增功能点列出来看看: (1)新增其他的进程 ...
- DDD 领域驱动设计-看我如何应对业务需求变化,领域模型调整?
写在前面 上一篇:DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对? "愚蠢的应对",这个标题是我后来补充上的,博文中除了描述需求变化.愚蠢应对和一些思考,确实没有实质性 ...
- 我眼中BA(业务需求分析师)的技能广度和深度
BA,或者称业务分析师,是企业数字能力和业务能力之间的沟通桥梁.随着企业数字转型的进一步深化,相信对BA这样的技能需求会越来越多,只是未必都用“BA/业务分析师”这样的Title. ThoughtWo ...
- 自己动手写PHP MVC框架
自己动手写PHP MVC框架 来自:yuansir-web.com / yuansir@live.cn 代码下载: https://github.com/yuansir/tiny-php-framew ...
- 自己动手写spring容器(3)
好久没有写博客了,今天闲下来将之前未完成的表达出来. 在之前的文章自己动手写spring容器(2)中完成了对spring的依赖注入的实现,这篇将会介绍spring基于注解的依赖注入的实现. 在一般的J ...
- 自己动手写把”锁”---LockSupport介绍
本篇是<自己动手写把"锁">系列技术铺垫的最后一个知识点.本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒. LockSupport是Java6引入 ...
- 自己动手写web框架----1
本文可作为<<自己动手写struts–构建基于MVC的Web开发框架>>一书的读书笔记. 一个符合Model 2规范的web框架的架构图应该如下: Controller层的Se ...
随机推荐
- 专访|3DCAT如何赋能Matterverse打造3A游戏画面的Sandbox
元宇宙概念自20世纪90年代创造,在21世纪经历20年快速塑形,终于在2021年进入元年,元宇宙概念爆发,受到政府.机构.企业以及网民的高度关注,资本市场一度高涨,相关投资赛道大热. 元宇宙第一股Ro ...
- Python实践:基于Matplotlib实现某产品全年销量数据可视化
本文分享自华为云社区<画图实战-Python实现某产品全年销量数据多种样式可视化>,作者:虫无涯. 学习心得 有时候我们需要对某些数据进行分析,得到一些可视化效果图,而这些效果图可以直观展 ...
- Linux文件查找、三剑客、正则表达式
Linux文件查找 1.find查找概述 为什么要有文件查找,因为很多时候我们可能会忘了某个文件所在的位置,此时就需要通过find来查找. find命令可以根据不同的条件来进行查找文件,例如:文件名称 ...
- View之invalidate,requestLayout,postInvalidate
目录介绍 01.invalidate,requestLayout,postInvalidate区别 02.invalidate深入分析 03.postInvalidate深入分析 04.request ...
- 记录一次报错,程序启动,MySql自动关闭
关于初级程序员,对于安装mysql,以及配置可能会报几次错 有时候虽然进行第二次安装成功,但是第一次的残留文件还在,可能引起报错 在这里记录一次我的报错 程序启动导致Mysql自动断开,需要手动打开 ...
- SSR解决了什么问题?有做过SSR吗?你是怎么做的?
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.是什么 Server-Side Rendering 我们称其为SSR,意为服务端渲染 指由服务侧完成页面的 HTML 结构拼接的页面处 ...
- rust使用lazy_static对全局变量多线程并发读写示例
首先需要在项目依赖Cargo.toml添加lazy_static依赖项 [dependencies] lazy_static = "1.4.0" 示例代码如下: use lazy_ ...
- 基于proteus的数字电路设计
基于proteus的数字电路设计 1.实验原理 proteus的数字电路仿真能力还是比较强大的.这里总结一下proteus的几个基本操作以备后用.大致包括74hc系列的使用.常用调试设备.仿真开关.器 ...
- KingbaseES索引坏块
错误信息产生: 下面的报错一般为有坏块的产生. test=# select max(create_time) from public.tbl_table where create_time>=' ...
- 前端 Typescript 入门
前端 Typescript 入门 Ant design vue4.x 基于 vue3,示例默认是 TypeScript.比如 table 组件管理. vue3 官网介绍也使用了 TypeScript, ...