The purpose of this article is to show you how to create an ESLint plugin and create an ESLint rule to help you understand how ESLint works and to create rules that perfectly meet your requirements if necessary.

Plug-in goals

Disallow the second parameter of setTimeout in a project to be a number.

PS: If it is a number, it is easy to become the devil’s number. No one knows why it is a number or what it means.

Initialize the project with a template:

1. Install the NPM package

The use of Yeoman templates (generator-ESLint) is provided by ESLint to make it easier for developers to develop plug-ins.

All we need to know about Yeoman is that it is a scaffolding tool that generates an engineered directory structure containing the specified frame structure.

npm install -g yo generator-eslint
Copy the code

2. Create a folder:

mkdir eslint-plugin-demo
cd eslint-plugin-demo
Copy the code

3. Initialize the ESLint plugin’s project structure from the command line:

yo eslint:plugin
Copy the code

Enter the command line interaction flow, which generates the ESLint plug-in project framework and files.

? What is your name? OBKoro1
? What is the plugin ID? korolint   // What is the ID of this plug-in
? Type a short description of thisPlugin: XX company's custom ESLint rule// Enter a description of the plug-in
? Does this plugin contain custom ESLint rules? Yes // Does this plugin contain custom ESLint rules?
? Does this plugin contain one or more processors? No Does this plug-in contain one or more processors
// The handler is used to process files other than JS, such as.vue files
   create package.json
   create lib/index.js
   create README.md
Copy the code

Now you can see that there are some folders and files inside the folder, but we also need to create a file with the details of the rule.

4. Create rules

The previous command line generated the project template for the ESLint plug-in. This command line is the file that generates the specific rules for the ESLint plug-in.

yo eslint:rule // Generate template files for esLint rules
Copy the code

Create rule command line interaction:

? What is your name? OBKoro1
? Where will this rule be published? (Use arrow keys) // Where will this rule be published?
❯ ESLint Core  // Official core rules (currently 200 + rules)
  ESLint Plugin  // Select the ESLint plugin
? What is the rule ID? settimeout-no-number  // Rule ID
? Type a short description of thisRule: setTimeout The second parameter cannot be a number// Enter a description of the rule
? Type a short example ofThe code that will fail: placeholder// Enter the code for a failed example
   create docs/rules/settimeout-no-number.md
   create lib/rules/settimeout-no-number.js
   create tests/lib/rules/settimeout-no-number.js
Copy the code

Project structure with concrete rule files added

.Bass Exercises ── Readme.md Bass Exercises ─ docs// Use the document│ └ ─ ─ rules// Document all rules│ └ ─ ─ settimeout - no - number. The md// Specific rules document├ ─ ─ lib// ESLint rule development├─ ├─ exercises - ├─ exercises - ├─ exercises - exercises// Multiple rules can be built under this directory│ └ ─ ─ settimeout - no - number. Js// Rule details├ ─ ─ package. Json └ ─ ─ tests// Unit tests└── garbage ├ ─ garbage ├ ─ garbage garbage// Test the file for the rule
Copy the code

4. Install project dependencies

npm install
Copy the code

ESLint rules are a rule that can be used as a plugin for the ESLint rule.

AST – Abstract syntax tree

AST is short for Abstract Syntax Tree.

The effect of AST

The code is abstracted into a tree data structure to facilitate subsequent analysis and detection of the code.

The code is parsed as an AST

Astexplorer.net is a tool site: it can see how the code is parsed into an AST.

As shown below: when a value is selected on the right, the corresponding area on the left becomes highlighted so that the corresponding code can be easily selected in the AST.

AST selector:

The circled sections in the figure below are called AST selectors.

What AST selectors do: Use code to select specific snippets of code through selectors, and then statically analyze the code.

There are many AST selectors, and ESLint officially has a repository that lists all of them: Estree

Developing ESLint rules will use selectors in the following sections. You’ll get the idea later, but you’ll get the idea now.


How ESLint works

Before developing rules, we need to understand how ESLint works and why plug-ins need to be written that way.

1. Parse the code into AST

ESLint parses JS code into an AST using the JavaScript parser Espree.

ES6, React, and VUE have all developed parsers so ESLint can detect them. ESLint is the dominant front-end Lint tool for this reason.

2. Deeply traverse the AST to monitor the matching process.

After taking the AST, ESLint iterates through each selector twice, “top to bottom” and then “bottom to top”.

3. Trigger the listener selectorruleThe callback

During deep traversal, each rule that takes effect listens on one or more of the selectors, and whenever a selector is matched, the rule that listens on that selector triggers the corresponding callback.

4. Specific detection rules and other details.


The development rule

Rule Default Template

Open the template file lib/rules/settimeout-no-number.js and clean up the file to remove unnecessary options:

module.exports = {
    meta: {
        docs: {
            description: "SetTimeout the second argument must not be a number",},fixable: null.// Fix the function
    },
   The core / / rule
    create: function(context) {
       // Public variables and functions should be defined here
        return {
            // Returns the event hook}; }};Copy the code

Some of the deleted configuration items are used by the official ESLint core rules, while others are not needed for the time being. You can refer to the ESLint documentation if you need to use them

Create method – listens for selectors

Lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint lint

Create returns an object whose properties are set to selectors that ESLint collects and all callbacks listening for during AST traversal will be executed.

The core / / rule
create: function(context) {
    // Public variables and functions should be defined here
    return {
        // Returns the event hook
        Identifier: (node) = > {
            // Node is the selected content, which is the part we are listening for, and its value refers to the AST}}; }Copy the code

Watch the AST:

Creating an ESLint rule requires watching code parse into an AST, selecting the code you want to test, and making some judgments.

The following code is parsed online via AstExplorer.net.

setTimeout((a)= >{
	console.log('settimeout')},1000)
Copy the code

Rule complete file

lib/rules/settimeout-no-number.js:

module.exports = {
    meta: {
        docs: {
            description: "SetTimeout the second argument must not be a number",},fixable: null.// Fix the function
    },
    The core / / rule
    create: function (context) {
        // Public variables and functions should be defined here
        return {
            // Returns the event hook
            'CallExpression': (node) = > {
                if(node.callee.name ! = ='setTimeout') return // Either timer or filter
                const timeNode = node.arguments && node.arguments[1] // Get the second argument
                if(! timeNode)return // There is no second argument
                The second parameter is a digital error
                if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
                    context.report({
                        node,
                        message: 'setTimeout the second argument is forbidden to be a number '}}}}; }};Copy the code

Context.report () : This method is used to tell ESLint that the code is warning or wrong, as used above. View the documentation for context and context.report() here.

After the rule is written, the principle is to do targeted detection according to the results of AST parsing, filter out the code we want to select, and then make logical judgment on the value of the code.

It might be a little confusing right now, but never mind, let’s write a test case and then use the Debugger to see how the code works.

Test cases:

Test file tests/lib/rules/settimeout – no – number. Js:

/** * @fileOverview setTimeout The second argument cannot be a number * @author OBKoro1 */
"use strict";
var rule = require(".. /.. /.. /lib/rules/settimeout-no-number"), / / into the rule
    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester({
    parserOptions: {
        ecmaVersion: 7.// Es5 is supported by default}});// Run the test case
ruleTester.run("settimeout-no-number", rule, {
    // Correct test cases
    valid: [
        {
            code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)'
        },
        {
            code: 'setTimeout(()=>{ console.log(11) },someNumber)'}].// Wrong test case
    invalid: [
        {
            code: 'setTimeout(()=>{ console.log(11) },1000)'.errors: [{
                message: "SetTimeout the second argument must not be a number".// Consistent with the error thrown by rule
                type: "CallExpression" // rule listens to the corresponding hook}}}]]);Copy the code

Let’s learn how to debug node files in VSCode to see how the rule works.

In fact, it is possible to debug the rule using the console, but it is a bit slow to debug the rule using the console. For nodes, the information is not complete, so I prefer to debug the rule using the debugger.

Debug the node file in VSCode

  1. Click the Settings button in the image below to open a filelaunch.json
  2. Fill the following content in the file to debug the Node file.
  3. inruleFile to playdebuggerOr put a little red dot on the number of lines of code.
  4. Click the start button in the picture to enterdebugger

{
    // Use IntelliSense to learn about related attributes.
    // Hover to view descriptions of existing properties.
    / / for more information, please visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0"."configurations": [{"type": "node"."request": "launch"."name": "Start program".// Name of the debug interface
            // Run this file under the project:
            "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js"."args": [] // Node file parameters
        },
        // The following command is used to debug package.json, but vscode has a bug that makes it unusable now
        {
            "name": "Launch via NPM"."type": "node"."request": "launch"."runtimeExecutable": "npm"."runtimeArgs": [
                "run-script"."dev"    // Dev corresponds to dev in scripts in package.json]."port": 9229    // This is a debug port, not a project startup port]}},Copy the code

Run the test case to enter the breakpoint

  1. inlib/rules/settimeout-no-number.jsPlay somedebugger
  2. Click the Start button to run the test file as debugtests/lib/rules/settimeout-no-number.js
  3. Start debuggingrule.

Release the plugin

Eslint plugins are referenced as NPM packages, so the plugin needs to be published:

  1. Sign up: If you haven’t already signed up for an NPM account, do so.

  2. Login NPM: NPM login

  3. NPM publish, ESLint already has package.json ready.

Integration into the project:

Install NPM package: NPM I eslint-plugin-korolint -d

  1. Conventional methods:Introduce plug-in write rules one by one
// .eslintrc.js
module.exports = {
  plugins: [ 'korolint'].rules: { 
    "korolint/settimeout-no-number": "error"}}Copy the code
  1. extendsInherited plug-in configuration:

When there are many rules, it would be too cumbersome for the user to write them one by one, so ESLint can inherit the plugin configuration:

Lib /rules/index.js:

'use strict';
var requireIndex = require('requireindex');
const output = {
  rules: requireIndex(__dirname + '/rules'), // Export all rules
  configs: {
    // Export custom rules to be referenced directly in the project
    koroRule: {
      plugins: ['korolint'].// Import plug-ins
      rules: {
        // Enable the rule
        'korolint/settimeout-no-number': 'error'}}}};module.exports = output;
Copy the code

Usage:

Use extends to inherit the plug-in configuration, extends beyond the inheritance way, even if you pass in a NPM package, a relative file path address, eslint can inherit the configuration.

// .eslintrc.js
module.exports = {
  extends: [ 'plugin:korolint/koroRule' ] // Inherits the configuration exported by the plug-in
}
Copy the code

NPM: eslint-plugin-xx-xx eslint-plugin-xx eslint-plugin-xx otherwise an error will be reported, and this is a headache o(╥﹏╥)o

Extension:

This is enough to develop a plug-in, but here are some ideas for extending it.

Traversal direction:

As mentioned above, after taking the AST, ESLint will iterate over each selector twice, “top down” and then “bottom up”.

The selector we are listening for fires by default during the “top up” process, and if it needs to be executed during the “bottom up” process it needs to add :exit. In the above context CallExpression becomes CallExpression:exit.

Note: a piece of code that parses may contain the same selector more than once, and the selector’s hook will fire more than once.

The fix function: automatically fixes rule errors

Repair effect:

/ / before repair
setTimeout((a)= >{},1000)
// After the fix, the variable name was written incorrectly so that the user could modify it
const countNumber1 = 1000
setTimeout((a)= > {

}, countNumber2)
Copy the code
  1. Enable fixes on rule’s meta object:
/ / rule file
module.exports = {
  meta: {
    docs: {
      description: 'setTimeout the second argument is forbidden to be a number '
    },
    fixable: 'code' // Enable the repair function}}Copy the code
  1. incontext.report()Provide afixFunction:

Modify context. Report to add a fix method. See the documentation for more details.

context.report({
    node,
    message: 'setTimeout the second argument is forbidden to be a number ',
    fix(fixer) {
        const numberValue = timeNode.value;
        const statementString = `const countNumber1 = ${numberValue}\n`
        return [
        The variable name is intentionally miswritten so that the user can change it
        fixer.replaceTextRange(node.arguments[1].range, 'countNumber2'),
        // Add a line of code to declare a variable before setTimeoutfixer.insertTextBeforeRange(node.range, statementString) ]; }});Copy the code

Project Address:

eslint-plugin-korolint


This blog has been on and off for weeks, but it’s finally finished!

“ESLint” is a version of ESLint, which is a version of ESLint. “ESLint” is a version of ESLint, which is a version of ESLint, which is a version of ESLint.

If you find my blog helpful, please follow/like it!

Front-end advanced accumulation, public account, GitHub I don’t want Star✨ (crazy hint), WX :OBkoro1, email: [email protected]

Gay friends fly me

The ESLint plugin was learned from my gay friend yeyan1996, who also pointed me out when I ran into problems, thank you.

References:

Discussion of how creating rules ESLint works