自己动手写符合自己业务需求的eslint规则
簡(jiǎn)介:eslint是構(gòu)建在AST Parser基礎(chǔ)上的規(guī)則掃描器,缺省情況下使用espree作為AST解析器。rules寫好對(duì)于AST事件的回調(diào),linter處理源代碼之后會(huì)根據(jù)相應(yīng)的事件來回調(diào)rules中的處理函數(shù)。另外,在進(jìn)入細(xì)節(jié)之前,請(qǐng)思考一下:eslint的邊界在哪里?哪些功能是通過eslint寫規(guī)則可以做到的,哪些是用eslint無法做到的?
作者 | 旭倫
來源 | 阿里技術(shù)公眾號(hào)
使用eslint和stylelint之類的工具掃描前端代碼現(xiàn)在已經(jīng)基本成為前端同學(xué)的標(biāo)配。但是,業(yè)務(wù)這么復(fù)雜,指望eslint等提供的工具完全解決業(yè)務(wù)中遇到的代碼問題還是不太現(xiàn)實(shí)的。我們一線業(yè)務(wù)同學(xué)也要有自己的寫規(guī)則的能力。
eslint是構(gòu)建在AST Parser基礎(chǔ)上的規(guī)則掃描器,缺省情況下使用espree作為AST解析器。rules寫好對(duì)于AST事件的回調(diào),linter處理源代碼之后會(huì)根據(jù)相應(yīng)的事件來回調(diào)rules中的處理函數(shù)。
另外,在進(jìn)入細(xì)節(jié)之前,請(qǐng)思考一下:eslint的邊界在哪里?哪些功能是通過eslint寫規(guī)則可以做到的,哪些是用eslint無法做到的?
一 先學(xué)會(huì)如何寫規(guī)則測(cè)試
兵馬未動(dòng),測(cè)試先行。規(guī)則寫出來,如何用實(shí)際代碼進(jìn)行測(cè)試呢?
所幸非常簡(jiǎn)單,直接寫個(gè)json串把代碼寫進(jìn)來就好了。
我們來看個(gè)no-console的例子,就是不允許代碼中出現(xiàn)console.*語句的規(guī)則。
首先把規(guī)則和測(cè)試運(yùn)行對(duì)象ruleTester引進(jìn)來:
//------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------const rule = require("../../../lib/rules/no-console"),{ RuleTester } = require("../../../lib/rule-tester");//------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------const ruleTester = new RuleTester();然后我們就直接調(diào)用ruleTester的run函數(shù)就好了。有效的樣例放在valid下面,無效的樣例放在invalid下面,是不是很簡(jiǎn)單。
我們先看下有效的:
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)"],能通過的情況比較容易,我們就直接給代碼和選項(xiàng)就好。
然后是無效的:
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" }] }] });無效的要判斷下出錯(cuò)信息是不是符合預(yù)期。
我們使用mocha運(yùn)行下上面的測(cè)試腳本:
./node_modules/.bin/mocha tests/lib/rules/no-console.js運(yùn)行結(jié)果如下:
no-consolevalid? 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里面放一個(gè)不能通過的,則會(huì)報(bào)錯(cuò),比如我們加一個(gè):
ruleTester.run("no-console", rule, {valid: ["Console.info(foo)",// single array item{ code: "console.log('Hello,World')", options: [] },就會(huì)報(bào)下面的錯(cuò):
1 failing1) 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+0at 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是會(huì)報(bào)一個(gè)messageId為unexpected,而nodeType為MemberExpression的錯(cuò)誤。
我們應(yīng)將其放入到invalid里面:
invalid: [// no options{ code: "console.log('Hello,World')", errors: [{ messageId: "unexpected", type: "MemberExpression" }] },再運(yùn)行,就可以成功了:
invalid? console.log('Hello,World')二 規(guī)則入門
會(huì)跑測(cè)試之后,我們就可以寫自己的規(guī)則啦。
我們先看下規(guī)則的模板,其實(shí)主要要提供meta對(duì)象和create方法:
module.exports = {meta: {type: "規(guī)則類型,如suggestion",docs: {description: "規(guī)則描述",category: "規(guī)則分類:如Possible Errors",recommended: true,url: "說明規(guī)則的文檔地址,如https://eslint.org/docs/rules/no-extra-semi"},fixable: "是否可以修復(fù),如code",schema: [] // 選項(xiàng)},create: function(context) {return {// 事件回調(diào)};} };總體來說,一個(gè)eslint規(guī)則所能做的事情,就是寫事件回調(diào)函數(shù),在回調(diào)函數(shù)中使用context中獲取的AST等信息進(jìn)行分析。
context提供的API是比較簡(jiǎn)潔的:
代碼信息類主要我們使用getScope獲取作用域的信息,getAncestors獲取上一級(jí)AST節(jié)點(diǎn),getDeclaredVariables獲取變量表。最后的絕招是直接獲取源代碼getSourceCode自己分析去。
markVariableAsUsed用于跨文件分析,用于分析變量的使用情況。
report函數(shù)用于輸出分析結(jié)果,比如報(bào)錯(cuò)信息、修改建議和自動(dòng)修復(fù)的代碼等。
這么說太抽象了,我們來看例子。
還以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."}},我們?cè)倏磏o-console的回調(diào)函數(shù),只處理一處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的對(duì)應(yīng)關(guān)系如下圖:
我們前面的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個(gè)全局變量,復(fù)習(xí)下Javascript基礎(chǔ)吧:
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]},我們看到,所有的變量,都以一個(gè)名為set的Map中,這樣我們就可以以遍歷獲取所有的變量。
針對(duì)no-console的規(guī)則,我們主要是要查找是否有叫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個(gè)變量中發(fā)現(xiàn),console是并沒有定義的變量,所以
const consoleVar = astUtils.getVariableByName(scope, "console");的結(jié)果是null.
于是我們要去查找未定義的變量,這部分是在scope.through中,果然找到了name是console的節(jié)點(diǎn):
[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} ]這樣我們就可以寫個(gè)檢查reference的名字是不是console的函數(shù)就好:
function isConsole(reference) {const id = reference.identifier;return id && id.name === "console";}然后用這個(gè)函數(shù)去filter scope.though中的所有未定義的變量:
scope.through.filter(isConsole);最后一步是輸出報(bào)告,針對(duì)過濾出的reference進(jìn)行報(bào)告:
references.filter(isMemberAccessExceptAllowed).forEach(report);報(bào)告問題使用context的report函數(shù):
function report(reference) {const node = reference.identifier.parent;context.report({node,loc: node.loc,messageId: "unexpected"});}發(fā)生問題的代碼行數(shù)可以從node中獲取到。
2 處理特定類型的語句
no-console從規(guī)則書寫上并不是最容易的,我們以其為例主要是這類問題最多。下面我們舉一反三,看看針對(duì)其它不應(yīng)該出現(xiàn)的語句該如何處理。
其中最簡(jiǎn)單的就是針對(duì)一類語句統(tǒng)統(tǒng)報(bào)錯(cuò),比如no-continue規(guī)則,就是遇到ContinueStatement就報(bào)錯(cuò):
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規(guī)則:
create(context) {return {DebuggerStatement(node) {context.report({node,messageId: "unexpected"});}};}不許使用with語句:
create(context) {return {WithStatement(node) {context.report({ node, messageId: "unexpectedWith" });}};}在case語句中不許定義變量、函數(shù)和類:
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"});}}}};}多個(gè)類型語句可以共用一個(gè)處理函數(shù)。
比如不許使用構(gòu)造方法生成數(shù)組:
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};}函數(shù)的參數(shù)不允許重名:
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};}如果事件太多的話,可以寫成一個(gè)數(shù)組,這被稱為選擇器數(shù)組:
const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"]; ...[loopSelector](node) {if (currentCodePath.currentSegments.some(segment => segment.reachable)) {loopsToReport.add(node);}},除了直接處理語句類型,還可以針對(duì)類型加上一些額外的判斷。
比如不允許使用delete運(yùn)算符:
create(context) {return {UnaryExpression(node) {if (node.operator === "delete" && node.argument.type === "Identifier") {context.report({ node, messageId: "unexpected" });}}};}不準(zhǔn)使用"=="和"!="運(yùn)算符:
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進(jìn)行比較:
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 }});}}}};}不準(zhǔn)給常量賦值:
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 - 語句結(jié)束事件
除了語句事件之外,eslint還提供了:exit事件。
比如上面的例子我們使用了VariableDeclaration語句事件,我們下面看看如何使用VariableDeclaration結(jié)束時(shí)調(diào)用的VariableDeclaration:exit事件。
我們看一個(gè)不允許使用var定義變量的例子:
return {"VariableDeclaration:exit"(node) {if (node.kind === "var") {report(node);}}};如果覺得進(jìn)入和退出不好區(qū)分的話,我們來看一個(gè)不允許在非函數(shù)的塊中使用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,這些邏輯的作用是,進(jìn)入語句塊的時(shí)候調(diào)用enterScope,退出語句塊的時(shí)候調(diào)用exitScope:
function enterScope(node) {stack.push(node.range);}function exitScope() {stack.pop();}4 直接使用文字信息 - Literal
比如不允許使用"-.7"這樣省略了0的浮點(diǎn)數(shù)。此時(shí)使用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")});}}}};}不準(zhǔn)使用八進(jìn)制數(shù)字:
create(context) {return {Literal(node) {if (typeof node.value === "number" && /^0[0-9]/u.test(node.raw)) {context.report({node,messageId: "noOcatal"});}}};}三 代碼路徑分析
前面我們討論的基本都是一個(gè)代碼片段,現(xiàn)在我們把代碼邏輯串起來,形成一條代碼路徑。
代碼路徑就不止只有順序結(jié)構(gòu),還有分支和循環(huán)。
除了采用上面的事件處理方法之外,我們還可以針對(duì)CodePath事件進(jìn)行處理:
事件onCodePathStart和onCodePathEnd用于整個(gè)路徑的分析,而onCodePathSegmentStart, onCodePathSegmentEnd是CodePath中的一個(gè)片段,onCodePathSegmentLoop是循環(huán)片段。
我們來看一個(gè)循環(huán)的例子:
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" }));}};}四 提供問題自動(dòng)修復(fù)的代碼
最后,我們講講如何給問題給供自動(dòng)修復(fù)代碼。
我們之前報(bào)告問題都是使用context.report函數(shù),自動(dòng)修復(fù)代碼也是通過這個(gè)接口返回給調(diào)用者。
我們以將"=="和"!="替換成"==="和"!=="為例。
這個(gè)fix沒有多少技術(shù)含量哈,就是給原來發(fā)現(xiàn)問題的運(yùn)算符多加一個(gè)"=":
report(node, `${node.operator}=`);最終實(shí)現(xiàn)時(shí)是調(diào)用了fixer的replaceText函數(shù):
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個(gè)添加API,2個(gè)刪除API,2個(gè)替換類的API:
五 高級(jí)話題
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承載。
因?yàn)閑stree只支持javascript,typescript-eslint提供兼容estree格式的parser.
既然是ts的lint,自然是擁有了ts的支持,擁有了新的工具方法,其基本架構(gòu)仍是和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,我們?cè)倩ㄒ恍↑c(diǎn)篇幅看下StyleLint。
StyleLint與Eslint的架構(gòu)思想一脈相承,都是對(duì)于AST的事件分析進(jìn)行處理的工具。
只不過css使用不同的AST Parser,比如Post CSS API, postcss-value-parser, postcss-selector-parser等。
我們來看個(gè)例子體感一下:
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函數(shù)回報(bào),也可以支持autofix的生成。
七 小結(jié)
以上,我們基本將eslint規(guī)則寫法的大致框架梳理清楚了。當(dāng)然,實(shí)際寫規(guī)剛的過程中還需要對(duì)于AST以及語言細(xì)節(jié)有比較深的了解。預(yù)祝大家通過寫出適合自己業(yè)務(wù)的檢查器,寫出更健壯的代碼。
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。?
總結(jié)
以上是生活随笔為你收集整理的自己动手写符合自己业务需求的eslint规则的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android项目架构设计深入浅出
- 下一篇: 大数据领域的专精特新“小巨人”中科闻歌