自己動手寫符合自己業務需求的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)varconsole = require('myconsole'); console.log(foo) invalidconsole.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: [] },
就會報下面的錯:
1failing1)no-consolevalidconsole.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+0attestValidTemplate (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語句的例子,首先拿到的都是全域性作用域,舉例如下:
<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; }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就報錯:
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) {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; } }); }
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){constpragma = pragmaUtil.getFromContext(context);constfragment = pragmaUtil.getFragmentFromContext(context);functionhandleOpeningElement() {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';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),集專案經理、產品經理、原型設計師等多重身份於一身,幫助客戶整理需求、內容及框架搭建工作,把客戶的需求完整地在網站系統中實現 。需要網站架構師具備完整的邏輯能力,對行業有較深理解,協助使用者完成網站的原型設計。點選閱讀原文了解詳情。


相關文章