自己动手写符合自己业务需求的eslint规则

简介: 使用eslint和stylelint之类的工具扫描前端代码现在已经基本成为前端同学的标配。但是,业务这么复杂,指望eslint等提供的工具完全解决业务中遇到的代码问题还是不太现实的。我们一线业务同学也要有自己的写规则的能力。

# 自己动手写符合自己业务需求的eslint规则




使用eslint和stylelint之类的工具扫描前端代码现在已经基本成为前端同学的标配。但是,业务这么复杂,指望eslint等提供的工具完全解决业务中遇到的代码问题还是不太现实的。我们一线业务同学也要有自己的写规则的能力。




eslint是构建在AST Parser基础上的规则扫描器,缺省情况下使用espree作为AST解析器。rules写好对于AST事件的回调,linter处理源代码之后会根据相应的事件来回调rules中的处理函数。


![](https://gw.alicdn.com/imgextra/i3/O1CN01XXaN3429TCBHRCooE_!!6000000008068-55-tps-439-522.svg)




另外,在进入细节之前,请思考一下:eslint的边界在哪里?哪些功能是通过eslint写规则可以做到的,哪些是用eslint无法做到的?




## 先学会如何写规则测试


兵马未动,测试先行。规则写出来,如何用实际代码进行测试呢?


所幸非常简单,直接写个json串把代码写进来就好了。




我们来看个no-console的例子,就是不允许代码中出现console.*语句的规则。




首先把规则和测试运行对象ruleTester引进来:


```js

//------------------------------------------------------------------------------

// Requirements

//------------------------------------------------------------------------------


const rule = require("../../../lib/rules/no-console"),

   { RuleTester } = require("../../../lib/rule-tester");


//------------------------------------------------------------------------------

// Tests

//------------------------------------------------------------------------------


const ruleTester = new RuleTester();

```




然后我们就直接调用ruleTester的run函数就好了。有效的样例放在valid下面,无效的样例放在invalid下面,是不是很简单。


我们先看下有效的:


```js

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)"

   ],

```




能通过的情况比较容易,我们就直接给代码和选项就好。


然后是无效的:


```js

   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里面放一个不能通过的,则会报错,比如我们加一个:


```js

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里面:


```js

   invalid: [


       // no options

       { code: "console.log('Hello,World')", errors: [{ messageId: "unexpected", type: "MemberExpression" }] },

```


再运行,就可以成功了:


```

   invalid

     ✓ console.log('Hello,World')

```




## 规则入门




会跑测试之后,我们就可以写自己的规则啦。


我们先看下规则的模板,其实主要要提供meta对象和create方法:


```js

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是比较简洁的:


![](https://gw.alicdn.com/imgextra/i1/O1CN01XJsmyO1lqVYxA5gOO_!!6000000004870-2-tps-1528-1122.png)


代码信息类主要我们使用getScope获取作用域的信息,getAncestors获取上一级AST节点,getDeclaredVariables获取变量表。最后的绝招是直接获取源代码getSourceCode自己分析去。


markVariableAsUsed用于跨文件分析,用于分析变量的使用情况。


report函数用于输出分析结果,比如报错信息、修改建议和自动修复的代码等。




这么说太抽象了,我们来看例子。


还以no-console为例,我们先看meta部分,这部分不涉及逻辑代码,都是一些配置:


```js

   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, 这是程序退出的事件:


```js


       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);

               }

           }

       };

```




### 获取作用域和AST信息


我们首先通过context.getScope()获取作用域信息。作用域与AST的对应关系如下图:


![](https://gw.alicdn.com/imgextra/i2/O1CN01ZpBUFt1jwLNY2qJD7_!!6000000004612-2-tps-1242-1712.png)


我们前面的console语句的例子,首先拿到的都是全局作用域,举例如下:


```json

<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基础吧:


```js

   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的变量名。于是可以这么写:


```js

   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是并没有定义的变量,所以


```js

const consoleVar = astUtils.getVariableByName(scope, "console");

```


的结果是null.


于是我们要去查找未定义的变量,这部分是在scope.through中,果然找到了name是console的节点:


```js

[

 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的函数就好:


```js

       function isConsole(reference) {

           const id = reference.identifier;


           return id && id.name === "console";

       }

```


然后用这个函数去filter scope.though中的所有未定义的变量:


```js

scope.through.filter(isConsole);

```


最后一步是输出报告,针对过滤出的reference进行报告:


```js

                   references

                       .filter(isMemberAccessExceptAllowed)

                       .forEach(report);

```




报告问题使用context的report函数:


```js

       function report(reference) {

           const node = reference.identifier.parent;


           context.report({

               node,

               loc: node.loc,

               messageId: "unexpected"

           });

       }

```


发生问题的代码行数可以从node中获取到。




### 处理特定类型的语句




no-console从规则书写上并不是最容易的,我们以其为例主要是这类问题最多。下面我们举一反三,看看针对其它不应该出现的语句该如何处理。


其中最简单的就是针对一类语句统统报错,比如no-continue规则,就是遇到ContinueStatement就报错:


```js

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规则:


```js

   create(context) {


       return {

           DebuggerStatement(node) {

               context.report({

                   node,

                   messageId: "unexpected"

               });

           }

       };


   }

```




不许使用with语句:


```js

   create(context) {


       return {

           WithStatement(node) {

               context.report({ node, messageId: "unexpectedWith" });

           }

       };


   }

```




在case语句中不许定义变量、函数和类:


```js

   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"

                       });

                   }

               }

           }

       };


   }

```






多个类型语句可以共用一个处理函数。


比如不许使用构造方法生成数组:


```js

       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

       };

```




不许给类定义赋值:


```js

   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

       };


   }

```




函数的参数不允许重名:


```js

   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

       };


   }

```




如果事件太多的话,可以写成一个数组,这被称为选择器数组:


```js

const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];

...

     [loopSelector](node) {

               if (currentCodePath.currentSegments.some(segment => segment.reachable)) {

                   loopsToReport.add(node);

               }

           },

```






除了直接处理语句类型,还可以针对类型加上一些额外的判断。


比如不允许使用delete运算符:


```js

   create(context) {


       return {


           UnaryExpression(node) {

               if (node.operator === "delete" && node.argument.type === "Identifier") {

                   context.report({ node, messageId: "unexpected" });

               }

           }

       };


   }

```




不准使用"=="和"!="运算符:


```js

   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进行比较:


```js

   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 }

                       });

                   }

               }

           }

       };

   }

```




不准给常量赋值:


```js

   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);

               }

           }

       };

   }

```




### :exit - 语句结束事件




除了语句事件之外,eslint还提供了:exit事件。




比如上面的例子我们使用了VariableDeclaration语句事件,我们下面看看如何使用VariableDeclaration结束时调用的VariableDeclaration:exit事件。


我们看一个不允许使用var定义变量的例子:


```js

       return {

           "VariableDeclaration:exit"(node) {

               if (node.kind === "var") {

                   report(node);

               }

           }

       };

```




如果觉得进入和退出不好区分的话,我们来看一个不允许在非函数的块中使用var来定义变量的例子:


```js

           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:


```js


       function enterScope(node) {

           stack.push(node.range);

       }


       function exitScope() {

           stack.pop();

       }

```






### 直接使用文字信息 - Literal




比如不允许使用"-.7"这样省略了0的浮点数。此时使用Literal来处理纯文字信息。




```js

   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")

                       });

                   }

               }

           }

       };

   }

```




不准使用八进制数字:


```js

   create(context) {

       return {

           Literal(node) {

               if (typeof node.value === "number" && /^0[0-9]/u.test(node.raw)) {

                   context.report({

                       node,

                       messageId: "noOcatal"

                   });

               }

           }

       };

   }

```




## 代码路径分析




前面我们讨论的基本都是一个代码片段,现在我们把代码逻辑串起来,形成一条代码路径。


代码路径就不止只有顺序结构,还有分支和循环。




![](https://gw.alicdn.com/imgextra/i2/O1CN01KhEfv21dv4aQIuXIM_!!6000000003797-55-tps-392-496.svg)


除了采用上面的事件处理方法之外,我们还可以针对CodePath事件进行处理:


![](https://gw.alicdn.com/imgextra/i3/O1CN01ngELgt28nW8kWRQyJ_!!6000000007977-2-tps-1330-944.png)




事件onCodePathStart和onCodePathEnd用于整个路径的分析,而onCodePathSegmentStart, onCodePathSegmentEnd是CodePath中的一个片段,onCodePathSegmentLoop是循环片段。




我们来看一个循环的例子:


```js

   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没有多少技术含量哈,就是给原来发现问题的运算符多加一个"=":


```js

report(node, `${node.operator}=`);

```


最终实现时是调用了fixer的replaceText函数:


```js

               fix(fixer) {

                   if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {

                       return fixer.replaceText(operatorToken, expectedOperator);

                   }

                   return null;

               }

```




完整的report代码如下:


```js

       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:


![](https://gw.alicdn.com/imgextra/i3/O1CN01gqV0Qy1te9oCt1VLX_!!6000000005926-2-tps-1970-734.png)


## 高级话题




### React JSX的支持




Facebook给我们封装好了框架,写起来也是蛮眼熟的。刚好之前没有举markVariableAsUsed的例子,正好一起看了:




```js

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的事件。




### TypeScript的支持




随着tslint合并到eslint中,TypeScript的lint功能由typescript-eslint承载。


因为estree只支持javascript,typescript-eslint提供兼容estree格式的parser.


既然是ts的lint,自然是拥有了ts的支持,拥有了新的工具方法,其基本架构仍是和eslint一致的:


```typescript

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',

         });

       }

     },

   };

 },

});

```






### 更换ESLint的AST解析器


ESLint支持使用第三方AST解析器,刚好Babel也支持ESLint,于是我们就可以用@babel/eslint-parser来替换espree. 装好插件之后,修改.eslintrc.js即可:


```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等。




我们来看个例子体感一下:


```js

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以及语言细节有比较深的了解。我们会在后续做专题讲解。


预祝大家通过写出适合自己业务的检查器,写出更健壮的代码。


目录
相关文章
|
存储 SQL 缓存
【软件开发规范二】《禁止项开发规范》
文章详细阐述java开发中的禁止项内容
【软件开发规范二】《禁止项开发规范》
|
监控 前端开发 JavaScript
AST 代码扫描实战:如何保障代码质量
2020 年 618 大促已经过去,作为淘系每年重要的大促活动,淘系前端在其中扮演着什么样的角色,如何保证大促的平稳进行?又在其中应用了哪些新技术?淘系前端团队特此推出「618 系列|淘系前端技术分享」,为大家介绍 618 中的前端身影。 本篇来自于频道与D2C智能团队的菉竹,为大家介绍本次 618 大促中是如何用代码扫描做资损防控的。
3306 0
AST 代码扫描实战:如何保障代码质量
|
1月前
|
设计模式 JavaScript 安全
TypeScript性能优化及代码质量提升的重要性、方法与策略,包括合理使用类型注解、减少类型断言、优化模块导入导出、遵循编码规范、加强代码注释等
本文深入探讨了TypeScript性能优化及代码质量提升的重要性、方法与策略,包括合理使用类型注解、减少类型断言、优化模块导入导出、遵循编码规范、加强代码注释等,旨在帮助开发者在保证代码质量的同时,实现高效的性能优化,提升用户体验和项目稳定性。
42 6
|
1月前
|
JavaScript 测试技术 开发者
确定 Babel 插件的功能是否符合项目需求
【10月更文挑战第25天】细致的评估方法,可以较为准确地确定 Babel 插件的功能是否真正符合项目需求,从而为项目选择合适的插件,保障项目的顺利开发和高质量交付。
|
3月前
|
前端开发 JavaScript 开发工具
前端项目增加eslint全过程
如何在前端项目中安装并配置ESLint和Prettier,包括VSCode插件的安装、npm包的全局安装、.eslintrc.js配置文件的生成以及编辑器设置的调整。
61 6
|
2月前
|
JSON JavaScript 前端开发
深入解析ESLint配置:从入门到精通的全方位指南,精细调优你的代码质量保障工具
深入解析ESLint配置:从入门到精通的全方位指南,精细调优你的代码质量保障工具
103 0
|
5月前
|
机器学习/深度学习 监控 算法
量化交易系统开发步骤功能/规则玩法/案例项目/逻辑功能
量化交易策略系统开发是指利用编程和数学模型来设计、开发和实施自动化交易策略的过程。它涉及了将交易策略转化为可编程的算法,以便计算机可以根据预定规则和条件进行自动交易。
|
5月前
|
前端开发 JavaScript 运维
开发与运维开发问题之JSX规则和注意事项如何解决
开发与运维开发问题之JSX规则和注意事项如何解决
开发与运维开发问题之JSX规则和注意事项如何解决
|
5月前
|
测试技术
软件交付问题之在开发过程中,发现自己的代码风格与系统现有代码风格不匹配时,如何解决
软件交付问题之在开发过程中,发现自己的代码风格与系统现有代码风格不匹配时,如何解决
|
7月前
|
开发者
Eslint插件使用配置, 规范化开发, 详细配置流程
Eslint插件使用配置, 规范化开发, 详细配置流程

热门文章

最新文章