
使用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)
✓ varconsole = 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: [] },
就會報下面的錯:
1failing
no-console
valid
:
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
attestValidTemplate (lib/rule-tester/rule-tester.js:697:20)
atContext.<anonymous> (lib/rule-tester/rule-tester.js:972:29)
atprocessImmediate (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語句的例子,首先拿到的都是全域性作用域,舉例如下:
*1> GlobalScope {
type: 'global',
set: Map(38) {
> Variable { =
name: 'Array',
identifiers: [],
references: [],
defs: [],
tainted: false,
stack: true,
scope: [Circular *1],
eslintImplicitGlobalSetting: 'readonly',
eslintExplicitGlobal: false,
eslintExplicitGlobalComments: undefined,
writeable: false
},
> Variable { =
name: 'Boolean',
identifiers: [],
references: [],
defs: [],
tainted: false,
stack: true,
scope: [Circular *1],
eslintImplicitGlobalSetting: 'readonly',
eslintExplicitGlobal: false,
eslintExplicitGlobalComments: undefined,
writeable: false
},
> 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;
}
returnnull;
},
我們可以在剛才列出的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的函式就好:
functionisConsole(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函式:
functionreport(reference){
const node = reference.identifier.parent;
context.report({
node,
loc: node.loc,
messageId: "unexpected"
});
}
發生問題的程式碼行數可以從node中獲取到。
2 處理特定型別的語句
no-console從規則書寫上並不是最容易的,我們以其為例主要是這類問題最多。下面我們舉一反三,看看針對其它不應該出現的語句該如何處理。
其中最簡單的就是針對一類語句統統報錯,比如no-continue規則,就是遇到ContinueStatement就報錯:
{ =
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."
}
},
{
return{
{
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) {
functionisLexicalDeclaration(node) {
switch (node.type) {
case"FunctionDeclaration":
case"ClassDeclaration":
returntrue;
case"VariableDeclaration":
return node.kind !== "var";
default:
returnfalse;
}
}
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"
});
}
}
}
};
}
多個型別語句可以共用一個處理函式。
比如不許使用構造方法生成陣列:
functioncheck(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) {
functioncheckVariable(variable){
astUtils.getModifyingReferences(variable.references).forEach(reference => {
context.report({ node: reference.identifier, messageId: "class", data: { name: reference.identifier.name } });
});
}
functioncheckForClass(node){
context.getDeclaredVariables(node).forEach(checkVariable);
}
return {
ClassDeclaration: checkForClass,
ClassExpression: checkForClass
};
}
函式的引數不允許重名:
create(context) {
functionisParameter(def) {
return def.type === "Parameter";
}
functioncheckParams(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) {
functioncheckVariable(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:
functionenterScope(node) {
stack.push(node.range);
}
functionexitScope() {
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 = newMap(),
loopsToReport = newSet();
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);
}
returnnull;
}
完整的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);
}
returnnull;
}
});
}

五 高階話題
1 React JSX的支援
Facebook給我們封裝好了框架,寫起來也是蠻眼熟的。剛好之前沒有舉markVariableAsUsed的例子,正好一起看了:
{ =
meta: {
docs: {
description: 'Prevent React to be marked as unused',
category: 'Best Practices',
recommended: true,
url: docsUrl('jsx-uses-react'),
},
schema: [],
},
{
constpragma = pragmaUtil.getFromContext(context);
constfragment = pragmaUtil.getFragmentFromContext(context);
functionhandleOpeningElement() {
context.markVariableAsUsed(pragma);
}
return{
JSXOpeningElement: handleOpeningElement,
JSXOpeningFragment: handleOpeningElement,
{
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';
exportdefault 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);
consttype = 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)) returnfalse;
if (!isHexColor(node)) return;
report({
message: messages.rejected(node.value),
node: decl,
index: declarationValueIndex(decl) + node.sourceIndex,
result,
ruleName,
});
});
});
};
};
也是熟悉的report函式回報,也可以支援autofix的生成。
七 小結
以上,我們基本將eslint規則寫法的大致框架梳理清楚了。當然,實際寫規剛的過程中還需要對於AST以及語言細節有比較深的瞭解。預祝大家透過寫出適合自己業務的檢查器,寫出更健壯的程式碼。
網站架構師(CUED)培訓課程
網站架構師CUED(Cloud User Experience Design),集專案經理、產品經理、原型設計師等多重身份於一身,幫助客戶整理需求、內容及框架搭建工作,把客戶的需求完整地在網站系統中實現 。需要網站架構師具備完整的邏輯能力,對行業有較深理解,協助使用者完成網站的原型設計。點選閱讀原文了解詳情。
關鍵詞
型別
程式碼
函式
作用域
變數