Front-end development requires the support of various Types of Lint. A common misconception about using Lint is that: In practice, the definition of a lint specification depends largely on the habits of the author of the open source project, or the habits of the corporate team coding, even if two front-end experts can produce different code specifications.

Today’s topic is ESLint, the most popular JavaScript lint tool, while JSHint has gradually faded out of the limelight and is used less often

Common ESLint extensions include Standard, Airbnb, etc

Dissect the ESLint extension

Extension does two things

  • Configure config (specific rule parameters, global variables, runtime environment, etc.) on top of esLint
  • Customize your own rules to meet your needs

The idea is to take advantage of ESLint’s inheritance model, which in theory allows you to inherit indefinitely and override upper-level rules

The first rule is not detailed, esLint’s official website says in detail, basically every rule supports a custom parameter, the coverage is very broad, almost all syntax has a rule

The second custom rule is the highlight of this article, as esLint can no longer configure specific business scenarios to meet business requirements, such as:

  • eslint-plugin-vue
  • eslint-plugin-react
  • eslint-plugin-jest

General custom rules for special scenarios are named eslint-plugin-*, which can be easily written

{
  plugins: [
    'vue'.'react'.'jest']}Copy the code

Of course, eslint-config-* works the same way, except that it needs to be written when configured

{
  extends: 'standard'
}
Copy the code

The following describes the development process

Create the ESLint Plugin project

The official recommendation is to use Yeoman to generate projects. I feel the generated projects are old-fashioned, so I recommend getting used to my project structure

eslint-plugin-skr
  |- __tests__
  |  |- rules
  |  |- utils
  |
  |- lib
  |  |- rules
  |  |- utils
  |  |- index.js
  |
  |- jest.config.js
  |
  |- package.json
  |
  |- README.md
Copy the code

Yes, the yeoman generated project uses Mocha as the test framework by default. Personally, I feel that debugging is troublesome and not as flexible as JEST. Vscode can easily handle debugging

Debugging -jest-tests is a link to debugging-jest-tests

The jest config file is also posted here, which are basic configurations and not needed for complex ones. The testing part will be introduced in detail below

module.exports = {
  testEnvironment: 'node'.roots: ['__tests__'].resetModules: true.clearMocks: true.verbose: true
}
Copy the code

Custom rules are all under lib/rules, and a single file for each rule is sufficient

Here is a simple example to get through the two arteries

Develop a rule

preparation

  • Official development documentation
  • AST Abstract syntax tree

This official document is dense with dozens of attributes, but it’s really just the tip of the iceberg, and there are many complex scenarios to consider

Some people wonder: must be proficient in AST?

My answer is of course not. The simple answer is, at the very least, what does the parse syntax tree look like

Then give yourself a proposition to write! Let me write a super simple one

module.exports = {
  meta: {
    docs: {
      description: 'Block level comments are disabled'.category: 'Stylistic Issues'.recommended: true
    }
  },

  create (context) {
    const sourceCode = context.getSourceCode()

    return {
      Program () {
        const comments = sourceCode.getAllComments()

        const blockComments = comments.filter(({ type }) = > type === 'Block')

        blockComments.length && context.report({
          message: 'No block comments'})}}}}Copy the code

This example is very simple: call the method in the context variable to get all the comments

A slightly more complicated scenario

If you need the order of attributes in a Lint bar object, assume a rule as follows

// good
const bar = {
  meta: {},
  double: num= > num * 2
}

// bed
const bar = {
  double: num= > num * 2.meta: {},}Copy the code

This first time some will be a little confused, the official website does not provide specific examples, the solution is very simple, recommend a sharp tool AstExplorer

Don’t be in a hurry to copy the code to see the AST results, first select ESpree (the syntactic parsing library used by ESLint), as shown below

These four short lines of code correspond to an abstract syntax tree, as shown below:

Because the full expansion is too long, the hierarchy is very deeply nested if you are interested in trying it yourself. To find the bar property, you need program.body [0].declarations[0].init.properties

The create method returns an object, which can be used to define a number of detection types.

function checkLastSegment (node) {
  // report problem for function if last code path segment is reachable
}

module.exports = {
  meta: {... },create: function(context) {
    // declare the state of the rule
    return {
      ReturnStatement: function(node) {
        // at a ReturnStatement node while going down
      },
      // at a function expression node while going up:
      "FunctionExpression:exit": checkLastSegment,
      "ArrowFunctionExpression:exit": checkLastSegment,
      onCodePathStart: function (codePath, node) {
        // at the start of analyzing a code path
      },
      onCodePathEnd: function(codePath, node) {
        // at the end of analyzing a code path}}}}Copy the code

Here you can use the VariableDeclarator type as the inspection target, and the filtering conditions can be analyzed from the parse tree below

Take the VariableDeclarator object as the current node

When the variable name is bar (node.id.name === ‘bar’) and the value is an object (node.init.type === ‘ObjectExpression’), the code is as follows:

module.exports = {
  meta: {... }, create (context) {return {
      VariableDeclarator (node) {
        const isBarObj = node.id.name === 'bar' &&
          node.init.type === 'ObjectExpression'

        if(! isBarObj)return

        // checker}}}}Copy the code

After successfully retrieving the bar object, you can detect the order of attributes, sorting algorithm a large number, pick a favorite use, here is not repetitive, directly on the result:

const ORDER = ['meta'.'double']

function getOrderMap () {
  const orderMap = new Map()

  ORDER.forEach((name, i) = > {
    orderMap.set(name, i)
  })

  return orderMap
}

module.exports = {
  create (context) {
    const orderMap = getOrderMap()

    function checkOrder (propertiesNodes) {
      const properties = propertiesNodes
        .filter(property= > property.type === 'Property')
        .map(property= > property.key)

      properties.forEach((property, i) = > {
        const propertiesAbove = properties.slice(0, i)
        const unorderedProperties = propertiesAbove
          .filter(p= > orderMap.get(p.name) > orderMap.get(property.name))
          .sort((p1, p2) = > orderMap.get(p1.name) > orderMap.get(p2.name))

        const firstUnorderedProperty = unorderedProperties[0]

        if (firstUnorderedProperty) {
          const line = firstUnorderedProperty.loc.start.line

          context.report({
            node: property,
            message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`.data: {
              name: property.name,
              firstUnorderedPropertyName: firstUnorderedProperty.name,
              line
            }
          })
        }
      })
    }

    return {
      VariableDeclarator (node) {
        const isBarObj = node.id.name === 'bar' &&
          node.init.type === 'ObjectExpression'

        if(! isBarObj)return

        checkOrder(node.init.properties)
      }
    }
  }
}
Copy the code

There’s a lot of code here, but it’s actually pretty easy to be patient with, so I’ll explain it

The getOrderMap method converts an array to a Map, and the aspect gets its index via get. We can also handle multilatitude arrays, for example, two keys that want to be at the same sort level, neck and neck. We can write:

const order = [
  'meta'
  ['double'.'treble']]function getOrderMap () {
  const orderMap = new Map()

  ORDER.forEach((name, i) = > {
    if (Array.isArray(property)) {
      property.forEach(p= > orderMap.set(p, i))
    } else {
      orderMap.set(property, i)
    }
  })

  return orderMap
}
Copy the code

Double and Treble have the same rank, which is easy to extend. If there is a sort rule for n attributes, you can easily extend this rule.

That’s it. You can easily reverse Lint logic using amway’s online parsing tool.

If the rule is complex, you need a lot of utils support, otherwise every rule will be a mess, testing the ability to extract common code

test

As mentioned earlier, it is recommended to use JEST for testing, where tests are not quite the same as normal unit tests, esLint is result-based testing, what does that mean?

Lint only has two cases, pass and fail. You just need to sort the pass and fail cases into two arrays and leave the rest to EsLint’s RuleTester

The above attribute order rule is tested as follows:

const RuleTester = require('eslint').RuleTester
const rule = require('.. /.. /lib/rules/test')

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaVersion: 6
  }
})

ruleTester.run('test rule', rule, {
  valid: [
    `const bar = { meta: {}, double: num => num * 2 }`].invalid: [{code: `const bar = { double: num => num * 2, meta: {}, }`.errors: [{
        message: 'The "meta" property should be above the "double" property on line 2.'}}}]])Copy the code

Valid is code that is expected to pass, invalid is code that is not expected to pass and error messages.

Packaging output

Finally, you need to send an NPM package for the rules to be used in the project. Here we don’t need to describe how to send the package, but we will briefly talk about how to export the rules elegantly.

Directly on the code:

const requireIndex = require('requireindex')

// import all rules in lib/rules
module.exports.rules = requireIndex(`${__dirname}/rules`)
Copy the code

Using the three-party dependency requireIndex, it’s much simpler to bulk export all files in a folder.

Make sure that all rules files are in the rules folder. Don’t put utils in the rules folder

conclusion

The purpose of this article is to share some experience on writing custom ESLint rules.

Do not waste time learning AST. Different libraries implement AST differently. Next time you write Babel, you will need to learn other AST rules. Then summarize the rule, the logic is actually very simple, to judge the AST results on the line.

From the team level, it is hoped that all teams have their own ESLint rule library, which can greatly reduce the cost of code review and ensure the consistency of code, once and for all.