Abstract: Understand ESLint thoroughly.

  • Discussion of how ESLint works

  • Author: zhangwang

    FundebugReproduced with authorization, copyright belongs to the original author.

ESLint is a must-have tool for modern front-end development. Its usage is simple, but the effect is very great, I do not know how many times to help me reduce the possible bug in the process of use. ESLint is one of the must-have tools for front-end development, and it’s worth digging deeper to understand how it works.

Before we get into the mechanics, let’s talk about why ESLint is used.

Why use ESLint

ESLint was actually released back in July 2013, but I first used it one afternoon less than three years ago (I clearly remember that the main editor used at that time was Sublime Text3). I tried ESLint in a project, typed ESLint init, followed the prompts and finally chose the very famous Airbnb code style. As a result, almost all files in the whole project were marked red. I tried using –fix but couldn’t fix all of them, which made me very frustrated.

Now that I think about it, my perception of ESLint at that time was incomplete. At that time, I thought ESLint was a tool to help us keep the code style consistent, and Airbnb’s JS style was widely respected.

I knew then that keeping the code style consistent increases readability and teamwork. However, we do not think deeply about why people praise a particular style, there must be a special meaning behind it.

Being consistent means adding constraints to the code we write, and ESLint is a tool for adding constraints to our code through rules. JS, as a dynamic language, can be written as one wishes, full of bugs, but through appropriate rules to constrain, can make our code more robust, more reliable project.

In the esLint-Rules section of the official documentation, we can see that there are a large number of rules officially provided, including those that are recommended (” ESLint :recommended”), those that are not enabled by default, and some that are deprecated.

This is consistent with real life, where we unconsciously follow and construct different rules. New rules are built because we have more experience with something, and turning them into rules may be in the hope that we will step down less and share a set of best practices to improve our productivity. When we submit code, we turn the conventions we want everyone to follow into MR templates that we want everyone to follow.

It seems to me that the core of ESLint is probably the various rules it contains, most of which are the product of many developers’ experiences:

  • Some can help us avoid mistakes;
  • Some help us write code for best practices;
  • Some help us regulate how variables are used;
  • Some help us format our code;
  • It helps us to use the new grammar properly.

You’ve seen a diagram that nicely illustrates what ESLint does:

  • If you don’t use ESLint, your code will have to be checked manually, formatting will be messy, running buggy, and your collaborators/users will be furious 😡;
  • If you use ESLint, your code will have a reliable machine to check, formatting rules, and run with fewer problems and everyone will be happy.

In general, ESLint allows us to make our code more robust by freely extending and combining a set of rules that code should follow, not only to keep our code style consistent, but also to use community best practices and reduce errors.

ESLint is surprisingly important, so let’s take a look at the uses of ESLint and how they work.

From how to use it to how ESLint works

As you may be familiar with, the use of ESLint has two parts: configuring lint rules through configuration files; Run lint on the command line to find non-conformities (of course you can try to fix some non-conformities);

ESLint also works well with the editor plug-in, and in fact, many people may be more used to it.

Configuration ESLint

A common way to generate an ESLint configuration file is through ESLint –init followed by various selections, as follows:

$ eslint --init                                         zhangwang@zhangwangdeMacBook-Pro-2
? How would you like to configure ESLint? Use a popular style guide
? Which style guide do you want to follow? Airbnb (https://github.com/airbnb/javascript)
? Do you use React? No
? What format do you want your config file to be in? JavaScript
Copy the code

With the above selection, ESLint automatically generates a configuration file for us,.eslintrc.js, which reads as follows:

/** .eslintrc.js */
module.exports = {
    "extends": "airbnb"
};
Copy the code

Not to be taken lightly with this simple configuration, extends includes a sharing mechanism for ESLint that allows us to use best community practices at a very low cost. Every front-end developer has probably heard airbnb javascript Style (Github Star nearly 80000). So the question is, how does “extends”: “airbnb” allow us to use a code style that has a lot of rules?

Actually airbnb here is short for eslint-config-airbnb. If we look at the source code, we can find the following:

module.exports = {
  extends: [
    'eslint-config-airbnb-base'.'eslint-config-airbnb-base/rules/strict'.'./rules/react'.'./rules/react-a11y',
  ].map(require.resolve),
  rules: {}};Copy the code

The “extends”: “airbnb” in our own configuration tells ESLint to reference the eslint-config-Airbnb rules as extensions to our own projects.

If you want to know how the ESLint source code resolves the extends keyword in configuration files, you can refer to the source code: config-file.js-applyextends

Extends can be a string or an array. A string that starts with ESLint:, such as esLint :recommended. Writing this means using esLint’s recommended configuration, where you can see what rules it has; Plugin: a string that starts with a plugin, such as “plugin:react/recommended”, which means applying all the recommended rules of a third-party plugin, eslint-plugin-react, which we’ll discuss later; Package that starts with eslint-config-, which is actually a collection of third-party rules. We can also omit eslint-config- due to the extra processing added in ESLint, Eslint-config-airbnb-base above can also be written as airbnb-base; A local path to the local ESLint configuration, such as./rules/react;

Each item in extents ends up pointing to an object with the same configuration rules as ESLint itself.

If we search for esLint-config – in NPM, we will find a large number of ESLint extension configuration modules, which we can use directly in esLint to use popular styles, or we can package our configuration results into a module for later reuse.

Now that we know what extends is, we still don’t seem to know how ESLint works. What to do? The main file for eslint-config-airbnb-base used in the extends of eslint-config-airbnb is as follows:

module.exports = {
  extends: [
    './rules/best-practices'.'./rules/errors'.'./rules/node'.'./rules/style'.'./rules/variables'.'./rules/es6'.'./rules/imports',
  ].map(require.resolve),
  parserOptions: {
    ecmaVersion: 2018.sourceType: 'module',},rules: {
    strict: 'error',}};Copy the code

In addition to extends, parserOptions and rules appear in the configuration file. With parserOptions we can tell ESLint what version of the JS syntax we want to support (ecmaVersion), the sourceType sourceType, and whether to enable any other syntax-related features (such as JSX), To configure parserOptions, refer to the official documentation.

Rules we will focus on later in this article, but here we keep it in suspense.

Taking a look at the familiar extends, if you’re familiar with the rules section of the official document, you might notice that the extends items, with the exception of./rules/imports, correspond to the rules category of the official document. /rules/best-practices to help us understand rule.

rules

./rules/best-practices

module.exports = {
  rules: {
    // enforces getter/setter pairs in objects
    'accessor-pairs': 'off'.// enforces return statements in callbacks of array's methods
    // https://eslint.org/docs/rules/array-callback-return
    'array-callback-return': ['error', { allowImplicit: true}].// treat var statements as if they were block scoped
    'block-scoped-var': 'error'.// disallow the use of alert, confirm, and prompt
    'no-alert': 'warn'. }}Copy the code

./rules/best-practices Is actually an ESLint configuration file as well, but it’s a bit purer with only rules in it.

As we mentioned earlier, the core of ESLint is a collection of rules, and the expansion of this configuration file brings us much closer to the core of ESLint. When I first saw this configuration file, I had several questions: 1. In this file, we only added simple configurations for a single rule, error, WARN,off, and at most an option. How does ESLint work based on these configurations? // eslint-disable-next-line no-console or /*eslint no-console: [“error”, {allow: [“warn”]}] */ to block or enable a rule. How does this work? 3. How do multiple rules work together?

Lint is analysis based on static code, and for ESLint, the core of our input is the rule and its configuration and the source code to be analyzed by Lint. As a developer, we all understand the importance of abstraction. If we can abstract out the commonness of JS source code, then the source code analysis might be easier. The abstraction of code is called an AST (Abstract Syntax Tree).

Talk about the AST

In the years I’ve been learning about the front end, I’ve come across AST in many places.

  • Chapter 1 of JavaScript you Don’t Know, volume 1, covers AST;
  • View the browser how to parse Html, JS encountered AST;
  • AST is mentioned in other front-end tools such as Babel, Webpack, UglifyJS;

The AST itself is not a new topic, and it will probably come up anywhere compilation principles are involved.

ESLint uses espree to parse our JS statements to generate an abstract syntax tree. The source code is available here.

AST Explorer is a very cool AST tool site that allows you to easily see what a piece of code looks like after being parsed into an AST.

From the screenshot above, you may have noticed that if I mouse over a value on the right, the corresponding area on the left is also highlighted. In fact, the AST does make it easy to find specific things in your code. The item selected on the right is called AST selectors, which should be easy to understand for those familiar with CSS selectors.

Just like CSS selectors, AST selectors have a variety of rules that make it easier to select specific code snippets

  • Selectors – ESLint – Pluggable JavaScript linter
  • Github-estools/ESQuery: ECMAScript AST Query Library.

Using AST selectors, we can easily find content in static code, which provides a foundation for understanding how rule works. Let’s continue with the above question.

How does a single rule work?

As for how to write a rule, the section “Working with Rules” in the official document has been described in detail. Here is a brief description.

As we mentioned earlier, the core of ESLint is the rule. Each rule is independent and can be set to disable off🈲️, warn⚠️, or error❌.

We chose “no-debugger”: “error” to see how the rule works. The source code is as follows:

module.exports = {
    meta: {
        type: "problem".docs: {
            description: "disallow the use of `debugger`".category: "Possible Errors".recommended: true.url: "https://eslint.org/docs/rules/no-debugger"
        },

        fixable: null.schema: [].messages: {
            unexpected: "Unexpected 'debugger' statement."
        }
    },

    create(context) {

        return {
            DebuggerStatement(node) {
                context.report({
                    node,
                    messageId: "unexpected"}); }}; }};Copy the code

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

  • metaMetadata that represents this rule, such as its category, document, and accepted parametersschemaAnd so on,The official documentationIt is described in detail and will not be repeated here.
  • createIf meta expresses what we want to do, thencreateIs used to express how this rule will parse the code;

Create returns an object where the name of the most common key can be the selector we mentioned above, and in that selector we can get the selected content, and then we can make some judgments about the selected content to see if it meets our rules. If it doesn’t, We can throw a problem with context.report(), and ESLint will use our configuration to render the thrown content differently.

The above code actually shows that an “Unexpected ‘Debugger’ statement.” is thrown when a debugger statement is matched.

At this point, it seems that our understanding of rule is much deeper. Static match analysis like the one above does help us avoid a lot of problems, but ESLint also seems to help us find statements that will never be executed. This doesn’t seem to be enough just by matching above, which brings us to another point of rule matching, code Path Analysis.

code path analysis

We’re going to have all kinds of conditional statements, looping statements, so that the code in our program doesn’t have to be executed sequentially, and it doesn’t have to be executed once. Code path refers to the execution path of the program. A program can be expressed by several code paths, and a code path may include two types of objects, CodePath and CodePathSegment.

What is a code path? Let’s take an example:

if (a && b) {
    foo();
}
bar();
Copy the code

Let’s examine the possible execution paths of the above code.

  • If A is true – tests if B is true
    • If b is true – Executefoo()– performbar()
    • If b is not true – Executebar()
  • If a is not true, executebar()

Conversion to the AST expression may be clearer, as shown below:

Here, the whole can be regarded as a CodePath, and the so-called CodePathSegment is a part of the above branch. A code path consists of multiple codePathsegments. ESLint abstracts the code path to five events.

  • onCodePathStart:
  • onCodePathEnd
  • onCodePathSegmentStart
  • onCodePathSegmentEnd
  • onCodePathSegmentLoop

If you’re interested in how ESLint abstracts out these five events, refer to code- Path-Analyzer.js. The code path in JS is described in code Path Analysis Details. Can refer to.

With these events, we can effectively control the scope of detection during static code analysis. For example, rule no-fallthrough is used to avoid executing multiple case statements

const lodash = require("lodash");
const DEFAULT_FALLTHROUGH_COMMENT = /falls? \s? through/i;

function hasFallthroughComment(node, context, fallthroughCommentPattern) {
    const sourceCode = context.getSourceCode();
    const comment = lodash.last(sourceCode.getCommentsBefore(node));

    return Boolean(comment && fallthroughCommentPattern.test(comment.value));
}

function isReachable(segment) {
    return segment.reachable;
}

function hasBlankLinesBetween(node, token) {
    return token.loc.start.line > node.loc.end.line + 1;
}

module.exports = {
    meta: {... }, create(context) {const options = context.options[0) | | {};let currentCodePath = null;
        const sourceCode = context.getSourceCode();
        let fallthroughCase = null;
        let fallthroughCommentPattern = null;

        if (options.commentPattern) {
            fallthroughCommentPattern = new RegExp(options.commentPattern);
        } else {
            fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
        }

        return {
            onCodePathStart(codePath) {
                currentCodePath = codePath;
            },
            onCodePathEnd() {
                currentCodePath = currentCodePath.upper;
            },

            SwitchCase(node) {
                if(fallthroughCase && ! hasFallthroughComment(node, context, fallthroughCommentPattern)){ context.report({message: "Expected a 'break' statement before '{{type}}'.".data: { type: node.test ? "case" : "default" },
                        node
                    });
                }
                fallthroughCase = null;
            },

            "SwitchCase:exit"(node) {
                const nextToken = sourceCode.getTokenAfter(node);
                if (currentCodePath.currentSegments.some(isReachable) &&
                    (node.consequent.length > 0|| hasBlankLinesBetween(node, nextToken)) && lodash.last(node.parent.cases) ! == node) { fallthroughCase = node; }}}; }};Copy the code

This rule uses onCodePathStart and onCodePathEnd to control currentCodePath. The general thing to do is relatively simple, during the SwitchCase to determine whether there is a fallthrough, if there is no comment to declare that there is an intentional fallthrough, then throw an error.

What is SwitchCase:exit? To understand :exit we need to know how ESLint treats a piece of code. Let’s go back to the source code to find out.

We extract the eslint/lib/util/traverser. Parts of the js code

traverse(node, options) {
        this._current = null;
        this._parents = [];
        this._skipped = false;
        this._broken = false;
        this._visitorKeys = options.visitorKeys || vk.KEYS;
        this._enter = options.enter || noop;
        this._leave = options.leave || noop;
        this._traverse(node, null);
    }

    _traverse(node, parent) {
        if(! isNode(node)) {return;
        }

        this._current = node;
        this._skipped = false;
        this._enter(node, parent);

        if (!this._skipped && !this._broken) {
            const keys = getVisitorKeys(this._visitorKeys, node);

            if (keys.length >= 1) {
                this._parents.push(node);
                for (let i = 0; i < keys.length && !this._broken; ++i) {
                    const child = node[keys[i]];

                    if (Array.isArray(child)) {
                        for (let j = 0; j < child.length && !this._broken; ++j) {
                            this._traverse(child[j], node); }}else {
                        this._traverse(child, node); }}this._parents.pop(); }}if (!this._broken) {
            this._leave(node, parent);
        }

        this._current = parent;
    }

Copy the code

Looking at the code above, we can see that the AST traversal, using recursion, actually has an outside-in and outside-out process. : Exit actually adds an extra callback that gives us more control over static code.

Okay, so just to summarize, selector, selector:exit, code path event can actually be viewed as different stages of traversing the code AST, giving us a lot of control over how we’re going to analyze a piece of static code.

At this point, we have a good understanding of how to write a rule. Continuing with the other questions raised above,

  • How is Rule put together
  • How do multiple rules work

What does ESLint think of rules we pass in

We now know that we can pass in rules in a number of ways. Let’s look at how these rules are put together

There are two sources for the effective rule: 1. The rule involved in the configuration file is processed in lib/ rule-js in the source code. 2. The rule in comments, called directive rule, is handled by ESLint in the getDirectiveComments function;

This is a bit like the old school vernier calipers, where the rule in the configuration file is used for rough tuning and the rule inside the file is used for fine tuning.

The final rule is a combination of these two parts, objects called configuredRules, each of which has contents similar to ‘accessor-pairs’: ‘off’.

Once you have all the rules you need to apply to a file, the following application is a typical multi-traversal process. The source code is in runRules, and the excerpt is shown below:

function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename) {
    const emitter = createEmitter();
    const nodeQueue = [];
    let currentNode = sourceCode.ast;

    Traverser.traverse(sourceCode.ast, {
        enter(node, parent) {
            node.parent = parent;
            nodeQueue.push({ isEntering: true, node });
        },
        leave(node) {
            nodeQueue.push({ isEntering: false, node });
        },
        visitorKeys: sourceCode.visitorKeys
    });


    const lintingProblems = [];

    Object.keys(configuredRules).forEach(ruleId= > {
        const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);

        if (severity === 0) {
            return;
        }

        const rule = ruleMapper(ruleId);
        const messageIds = rule.meta && rule.meta.messages;
        let reportTranslator = null;
        const ruleContext = Object.freeze(
            Object.assign(
                Object.create(sharedTraversalContext),
                {
                    id: ruleId,
                    options: getRuleOptions(configuredRules[ruleId]), report(... args) {if (reportTranslator === null) {... }constproblem = reportTranslator(... args);if(problem.fix && rule.meta && ! rule.meta.fixable) {throw new Error("Fixable rules should export a `meta.fixable` property."); } lintingProblems.push(problem); }}));const ruleListeners = createRuleListeners(rule, ruleContext);

        // add all the selectors from the rule as listeners
        Object.keys(ruleListeners).forEach(selector= > {
            emitter.on();
        });
    });

    const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));

    nodeQueue.forEach(traversalInfo= > {
        currentNode = traversalInfo.node;
        if (traversalInfo.isEntering) {
            eventGenerator.enterNode(currentNode);
        } else{ eventGenerator.leaveNode(currentNode); }});return lintingProblems;
}

Copy the code
  1. The AST generated from the source code is traversed, passing each node to the nodeQueue twice.
  2. Iterate through all the rules to be applied, add listening events to all the selectors in the rule, execute when fired, push the problem tolintingProblems;
  3. The nodeQueue obtained in the first step is traversed, triggering the events contained therein
  4. Return lintingProblems

There are some details we haven’t covered here, but it’s basically the process. Node’s event-driven mechanism is said to be one of its features, and using rule in ESLint is a good example of event-driven.

So far we’ve understood the core of ESLint, but there are a few things we haven’t covered yet. We continue unpacking ‘./rules/react’ from eslint-config-airbnb

module.exports = {
  plugins: [
    'react',].parserOptions: {
    ecmaFeatures: {
      jsx: true,}},rules: {
    'jsx-quotes': ['error'.'prefer-double'],... },settings: {
    'import/resolver': {
      node: {
        extensions: ['.js'.'.jsx'.'.json']}},... }}Copy the code

There are two more configurations that we haven’t talked about yet, and two more plugins and Settings that we haven’t seen yet. Settings is used to transfer some shared configurations. It is relatively simple and can be used by referring to official documents. But plugin is also one of the main points of ESLint, which we’ll discuss a bit more.

plugin

Plugin has two concepts;

  1. One is the field in the ESLint configuration item, as shown aboveplugins: ['react',],;
  2. The community-wrapped ESLint plugin is searchable on NPMeslint-plugin-You can find a lot of them. Some of the famous ones areeslint-plugin-react ,eslint-plugin-import

Plugins can be seen as collections of third-party rules. ESLint rules only support the standard ECMAScript syntax. However, if we want to use ESLint in React, we need to define our own rules. So there’s eslint-plugin-react.

The plugin configuration is similar to that of the ESLint configuration file and will not be described here. If you are interested, you can refer to the official documentation – working-with-plugins.

However, we can mention two uses of plugin here:

  • inextendsPlugin has its own namespace and is available through"Extends" : [" plugin: myPlugin/myConfig "]Refer to some kind of rule in the plugin (it could be all or recommended);
  • inpluginFor example, add configurationplugins: ['react',],You can declare what you want to referenceeslint-plugin-reactProvided rules, but the specific use of rules, how to use, still need to configure;

Support for plugins makes ESLint more open, increasing the scope of its own use.

However, there is another problem, JS is evolving so fast, what if we want to add Lint to some non-standard JS syntax? ESLint also allows us to customize parser.

A custom parser

You just need to comply with ESLint, ESLint supports custom parser, and the community has actually done a lot of work on this. Such as

  • Babel-eslint, A wrapper for Babel’s parser used for ESLint
  • Typescript-eslint-parser allows us to lint TS code

The custom parser can be used as follows:

{
    "parser": "./path/to/awesome-custom-parser.js"
}

Copy the code

By customizing parser, the usage scenario of ESLint was greatly expanded.

Other Configuration Items

Let’s go to Configuring ESLint and see all the configurations ESLint supports and see what else is new to us.

  1. global.env: It is inevitable that we will use some global variables, and the use of global variables conflicts with the use of some rulesglobalConfiguration we can declare the global variables we want to use, and env is a double encapsulation of global variables, so we can directly use, so we can directly use some common libraries exposed global variables, you can vieweslint/environments.jsView the implementation details.
  2. Overrides, rule of the outer configuration generally takes effect globally. Through overrides, we can override some rules for some files.
  3. Settings, with which we pass in some custom configuration for each rule

Afterword.

There are also some ESLint related topics that are not covered in this article, such as Formatters. After all, ESLint is ultimately intended for viewing, good output is important, formatters are covered in many libraries, and it’s an interesting topic, it can be a big one, And then there’s ESLint’s cascading mechanism, ignore mechanism and so on.

In addition, each rule has its own reasons behind its use. I haven’t looked into the original purpose of adding many rules, but before I use them next time, MAYBE I will remind myself why I use this rule. It will be interesting for you to have a look.

So much for ESLint, it’s been using ESLint for a long time, but this attempt to clarify ESLint really took a long time, so there are a few other things I’d like to talk about.

When we learn a tool, what are we learning?

I think when we learn a tool we should first know the problem it is trying to solve, so that we can take the right medicine. It is not easy to confuse prettier from ESLint; Second, we should understand its basic usage; Again, if the tool is used often enough, maybe we should dive into some of the basics that its implementation uses, such as this time we know AST, and maybe next time it will be easier to understand how Babel works, rather than treating it as something completely new.

The second question I want to talk about is, what if I meet a problem that I don’t understand? I think, slowly, we have gradually developed the habit of reading official documents, and then, I hope to gradually develop the habit of reading source code, reading source code can solve a lot of their own look at the document can not understand the problem.

If you can see it here, give it a thumbs up. Ha, ha, ha

Recommended data

About using ESLint:

  • Better Code Quality with ESLint | Pluralsight Pluralsight is a subscription website, the price is not cheap, but the class is very good;
  • Take a look at the development history of Lint tools in front ends in the ESLint section

On the AST:

  • Parser API to learn about estree AST conventions
  • AST in Modern JavaScript – Zhihu, ESLint front-end application