Related background:

This article and the previous article “From 0 to 1 implementation of front-end code inspection tools” were originally a whole article, but due to the length and lack of coupling, it is split into two parts: the previous article mainly explores the integration of plugins and implementation of ESLint configuration process, biased towards configuration; The next article deals with the third argument to _.get() in ESLint custom rules, with a bias towards source code.

Problem status:

When using the _.get() method in lodash.js, there are some strange bugs, such as getting a property. Using _.get() on an undefined variable will result in null values:

Var name = {name: null} var name = _. Get (obj, 'name', 'zly'Copy the code

This does not protect the data, and it is very easy to throw errors when returning the location data in the background. This is because the third parameter of the _.get() method is invalid, so you have to change the format later to avoid this problem:

Var obj = {name: null} var name = _. Get (obj, 'name') | | 'zly' a value: 'zly'Copy the code

A AST.

1.1 AST Concepts:

Ast (Abstract syntax code) is a tree representation of the abstract syntax structure of the source code, and each node in the tree represents a structure in the source code. The so-called abstraction means that the JS code is transformed into a structured data structure. This data structure is essentially a big JSON object, and JSON is something we’re all familiar with, like a tree with a lot of foliage: roots, trunks, branches, leaves. No matter how small or big, it’s a whole tree.

Front-end JS from compile to run process:

Lexical Analysis (Lexical unit) -> Parsing -> AST

1.2 AST structure

Online astExplorer: blogz.gize. IO /ast/ (select Espree)

Var name = _.get(obj, ‘name’, ‘zly’);

{ "type": "Program", "start": 0, "end": 36, "range": [ 0, 36 ], "body": [ { "type": "VariableDeclaration", "start": Declarations: [{"type": "VariableDeclarator", "declarations": [{"type": "VariableDeclarator", "end": [0, 36], "declarations": [{"type": "VariableDeclarator", "declarations": [0, 36], "declarations": 36, "range": [ 4, 36 ], "id": { "type": "Identifier", "start": 4, "end": 8, "range": [ 4, 8 ], "name": "Name"}, "init" : {" type ":" CallExpression ", "start" / / function call: 11, "the end" : 36, "range" : [11, 36], "the callee" : {" type ": Get "start": 11, "end": 16, "range": [11, 16], "object": {"type": "Identifier", "start" / / Identifier: 11, "the end" : 12, "range" : [11, 12], "name" : "_"}, "property" : {" type ": "Identifier", "start" / / Identifier: 13, "the end" : 16, "range" : [13, 16], "name" : "get"}, "computed" : false}, "the arguments: [/ / {" type ":" Identifier ", "start" / / Identifier: 17, "end" : 20, "range" : [17, 20], "name" : "obj"}, {" type ": "Literal", / / text "start" : 22, "the end" : 28, "range" : [22 to 28], "value" : "name", "raw" : "' the name"}, {" type ": "Literal", "start": 30, "end": 35, "range": [ 30, 35 ], "value": "zly", "raw": "'zly'" } ] } } ], "kind": Var "// keyword}], "sourceType": "module"}Copy the code

The simplified tree structure is as follows:

1.3 AST Compilation Process

ESLint flowcharts for running rules

Eslint each rule is for a node module. After a user has configured the rule, ESLint loads the rule, executes the module, and checks it based on user-provided parameters (such as whether an automatic fix is required).

Eslint’s general flow chart is as follows:

3. Custom rules

Custom rules document: eslint.bootcss.com/docs/develo…

3.1 Debugging the local rule File

Use the –rulesdir configuration parameter to configure the local rule file to be debugged:

"script": {
  "lint": "eslint --rulesdir ./scripts/lodash/rules"
}
Copy the code

3.2 Rule file structure

Module. Exports = {meta: {type: "XXX", docs: {} / / prompts relevant document information, fixable: "code", / / could you repair the messages: {/ / in a unit test can use unexpectedThirParameter: 'Lodash. The get () the third parameter is not recommended', unexpectedParameterLength: 'lodash.get () wrong number of arguments ',}}, create: Function (context) {return {CallExpression(node) {CallExpression(node) {CallExpression(node) {CallExpression(node) {CallExpression(node) {CallExpression(node) {CallExpression(node); }};Copy the code

A rule is a Node module consisting of two parts: meta and Create:

3.2.1 meta

The meta contains metadata for the rule, with the following parameters:

  • Fixable: Whether the plug-in supports automatic repair
  • Messages: The corresponding messagId can be set in the message for plug-in to use (external error message)

3.2.2 the create

If meta says what we want to do, create says how the rule will parse the code:

  • The context: object contains methods that ESLint uses to access nodes while iterating through the abstract syntax tree of JavaScript code.
  • Return value: Needs to return an object, which can provide the correspondingAst Node type function.
  • Function CallExpression:Abstract the node types of the syntax tree. As each node type is traversed, the parser checks whether a corresponding function is provided externally, and if so, calls and passes the current node.

3.3 User rules run the general logic

3.4 Rules Check illegal codes

When the plugin runs the rule and detects code that does not conform to the rule, it can call context.report() inside the rule and pass an object with the following parameters: the corresponding node and error message:

create: function(context) {
  return {
    CallExpression(node) {
      if (node.arguments.length === 3) {  // var a = _.get(obj, 'a', 'zly')
       	context.report({
           node,
           message: 'unexpectedThirParameter'
         })
       }
     }
   };
}
Copy the code

3.5 FIX functions in ESLint

When it detects code that does not conform to the specification, the plug-in determines some raw data for the custom rule, such as fixable. If the metadata is provided and the report provides the fix function, the plug-in calls the fix function in the rule and passes an object containing several methods that can manipulate the AST. We need to: replaceText(replace the text within the given node or token) :

create: function(context) { return { CallExpression(node) { context.report({ node, message: 'unexpectedParameterLength, fix (fixer) {/ / fix/logic/final output newCode return fixer. ReplaceText (node, newCode)}})}}; }Copy the code

The main one is the fix function, which is called by the plug-in and which needs to return a fixing object, along with the fixer arguments it provides.

3.6 Obtaining Source Code

After obtaining the necessary information such as fix() and fixer objects, the next thing to consider is how to obtain the corresponding source code. Since all that is available here are nodes (ast), we need to import the AST parser library to convert the AST into source code:

const escodegen = require('escodegen') create(context) { return { CallExpression(node) { const code = Escodegen. Generate (node)}}Copy the code

Context provides a getSourceCode method to get the source code for the current node:

Create: function(context) {const source = context.getSourcecode () _.get(xx,xx,xx) return { CallExpression(node) { if (node.arguments.length === 3) { context.report({ node, message: '_.get() the third argument is not recommended when the default is'})}}}}Copy the code

In the process of obtaining the source code, there are also stepped on the pit, the following is a comparison of the scheme.

purpose My solution The document provided
Get the source code Introduce espree, manual reverse parsing context.getSourceCode()
fixable Set to true Official: “code”

3.7 Parsing Parameters

3.7.1 Cut codes according to special identifiers

The option of slicing code according to a special identifier works, but the code looks strange and becomes expensive to read:

Const codeArr = escodegen. Generate (node).split(',') [' _. Get (a ', '" b "', '0'] const defaultValue = codeArr [codeArr. Length - 1]. The trim (). The split (') ') [0] / / use parentheses to cut code, Get a default value const code = ` ${codeArr [0]}, ${codeArr [1]}) | | ${defaultValue} `Copy the code

3.7.2 Regular Matching

Using the regular matching scheme may seem simple, but it is more expensive to read and difficult to maintain:

const paramsReg = /(_\.get\([^,]+,[^,]+),([^,]+)([^)]+)? )/(\)Copy the code

3.7.3 AST interval

The interval provided by AST is used to calculate the corresponding parameter interval, and the code is segmented accurately according to the interval to obtain the code we need:

create(context) { CallExpression(node) { const [ object, path, defaultValue ] = getArgumentsByNode(context, node) } } function getArgumentsByNode(context, node) { if (! context || ! Node) {throw new Error(' arguments wrong ')} const nodeArguments = node.arguments if (! NodeArguments | | nodeArguments. Length = = = 0) {throw new Error (' incoming node, no parameters; Or not a function call ')} const sourceCode = context.getSourcecode () // an instance of a resource that provides many methods, Return nodearguments.map ((item) => {const originCode = sourcecode.gettext (node) const originCode = sourcecode.gettext (node) const originCode = sourcecode.gettext (node) const originCode = sourcecode.gettext (node argumentLength = item.end - item.start const sliceStartPosition = item.start - node.start const sliceEndPosition = sliceStartPosition + argumentLength return originCode.slice(sliceStartPosition, sliceEndPosition) }) }Copy the code

Each of the above three schemes has its own advantages and disadvantages, but the most reliable and easily extensible scheme is to split code based on intervals.

3.9 Rule Auto Fix

Now that we have the important information we need (source code, new code, etc.), we can assemble the rule’s core logic fix:

create: function(context) {
  return {
    CallExpression(node) {
       context.report({
         node,
         message: 'unexpectedParameterLength',
         fix(fixer) {
            const [ object, path, defaultValue ] = getArgumentsByNode(context, node)
            const newCode = `_.get(${object}, ${path}) || ${defaultValue}`
            
            return fixer.replaceText(node, newCode)
         }
       })
     }
   }
}
Copy the code

In the case of getArgumentsByNode, you simply pass in the context and the corresponding node to get the parameters of that node. Subsequent attempts at custom rules can be extended based on this function to encapsulate more of the parsing methods applicable to the business itself.

3.10 Compatible with some special situations

So far, the basic function of the rule is complete, but since this rule is not only the fix function, it also has a basic verification logic, for example, if no parameter is provided and more than three parameters are used to verify:

Var a = _. Get (object, 'a', 0, 0) var a = _. Get (object, 'a', 0, 0) 0) // Invalid, but will fix automaticallyCopy the code

For fix, it is also compatible with special cases such as binary expressions or operator precedence problems:

Var a = _. Get (object, 'a', 0) + 1 / / if auto fix, the following code / / since + priority than | | is high, the fix is var after a = _. Get (object, 'a') | | 0 + 1Copy the code

The full picture of the compatible code is as follows:

Create (context) {CallExpression (node) {/ / -- -- -- -- -- -- -- -- -- -- -- -- -- compatible parameters -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / no arguments or a parameter: _.get() / _.get(object) if (node.arguments.length === 0 || node.arguments.length === 1) { context.report({ node, MessageId: 'unexpectedParameterLength', / / return error Id})} / / parameter is greater than the more than three: If (node.arguments.length > 3) {context.report({node, messageId: 'unexpectedParameterLength', / / return error Id})} const [object, path, defaultValue] = getArgumentsByNode (context, node) const parentNode = node.parent let newCode = `_.get(${object}, ${path}) | | ${defaultValue} ` / / -- -- -- -- -- -- -- -- -- -- -- -- special operators a variety of compatible -- -- -- -- -- -- -- -- -- -- --, / var/binary expression key = _.get(object, "key", 0) + 1 if (parentNode.type === 'BinaryExpression') { newCode = `(${newCode})` } } }Copy the code

Unit testing

4.1 Jest unit test

Jest’s official website: www.jestjs.cn/docs/gettin…

4.1.1 Jest Test rules

It will be easier to simply test with JEST. Directly introduce this testing framework and write corresponding test cases:

const Linter = require('eslint').Linter const rules = require('.. /.. /lib/rules/lodash-get') describe(" unit test on the third parameter of lodash ",()=>{const linter = new linter () const config = {rules: { "lodash-get": "error" } } linter.defineRule(key, rules['lodash-get']) it("var key = _.get(object, 'key', '')", () => { const code = `var key = _.get(object, 'key', '')` const output = `var key = _.get(object, 'key') || ''` expect(linter.verifyAndFix(code, config).output).toBe(output); })... }Copy the code

4.1.2 Problems occurred in Jest test

Some strange test cases occur when using jEST. For example, if the test parameter is null, only an error is cast, and there is no automatic fix:

Get () => {const code = 'var get = _. Get ()' const message = linter.verifyandfix (code, Config).messages[0].message expect(message).tobe (' lodash.get () parameter is null '); })Copy the code

The problems in the process are summarized as follows:

  • If the parameter is null, it should not be usedlinter.verifyAndFixBecause the rules are not fixed
  • When you manually compare errors thrown, you rely heavily on error information, which can lead toTest cases are not robust(Because of the magic string)
  • Even if the use oflinter.verfify()To test against specific use cases, it is also necessary to automatically capture error messages for comparisonTest cases are not robust)
  • It’s hard to distinguish betweenNumber of parametersIllegal, orparameterillegal
  • Bad readability, redundant code, hard to tell yes from noNumber of parametersIllegal, orparameterillegal

4.2 Community Reference

Not only did JEST find problems when testing, but the final results didn’t match our expectations, so we had to refer to test cases used by community-related projects. The main reference here is the test case in the eslint-plugin-vue plugin.

Here are the custom block-spacing rules for eslint-plugin-vue, even for projects this large there are some unprofessional things like using the magic character brace-style:

4.3 Tests provided by ESLint

When ruleTester calls the Run method, the plugin will check the code. The plugin will throw the corresponding error message, and then compare the test case with the messageId thrown by the plugin. If they are consistent, the test passes:

const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester(); const rules = require('.. /.. /lib/rules/lodash-get') ruleTester.run('lodash-get', rules, { valid: [ { code: 'var key = _.get(object, "key") || {}', } ], invalid: [ { code: 'var get = _.get()', errors: [{ messageId: 'unexpectedParameterLength' }] }, { code: 'var key = _.get(object)', errors: [{ messageId: 'unexpectedParameterLength' }] }, { code: 'var key = _.get(object, "key", {}, {})', errors: [{ messageId: 'unexpectedParameterLength' }] }, { code: 'var key = _.get(object, "key", 0) - 1', output: 'var key = (_.get(object, "key") || 0) - 1', errors: [{ messageId: 'unexpectedThirParameter', }] }, ] )}Copy the code

4.4 Maintaining Subsequent Rules

The plug-in is currently compatible with a few boundary cases, and the test case coverage is not complete, because it is difficult to verify the integrity of this rule before business baptism. After using it for a period of time, some boundary cases are gradually found:

Var key = _. Get (object, "key", {}) && {} for in loop for (var key in _. Get (object, "key", {}) && {} for in loop for (var key in _. {} {})) typeOf operator typeOf _. Get (object, "key", {}) chain call / / fix _ become problematic code. After the get (object, 'key') | | [] map () _. Get (object, 'key', []).map()Copy the code

After analysis, the output code of the rule is improved for the above boundary conditions. For the code with risk, the result after fix is added with parentheses:

If (parentNode. Type = = = 'BinaryExpression' | | parentNode. / / binary expression type = = = 'LogicalExpression' | | / / logical operators ParentNode. Type = = = 'ForInStatement' | | / / for in parentNode. Operator = = = 'typeof' | | / / typeof operator parentNode. Property //_.get(object, 'key', []).map() ) { newCode = `(${newCode})` }Copy the code

When code for these boundaries is compatible, the effect of the fix is:

Logical operators var key = _. Get (object, "key", {}) && var key = {} after fix the code (_. Get (object, "key") | | []) && {}Copy the code

In live.

So far this has been the full length of part 1 + Part 2, almost giving ESLint a thorough understanding of how to use it. Thanks to the former team and team leader, some internal project names of the former company have been coded in the article.