×
Community Blog Write eslint Rules That Meet Needs

Write eslint Rules That Meet Needs

This article discusses the general framework of eslint rule writing.

By Xulun, from F(x) Team

Using tools (such as eslint and stylelint) to scan frontend code has become standard for frontend developers. However, the business is so complex that it is not realistic to expect tools (such as eslint) to solve the code problems encountered in the business. Our first-line business developers should also have the ability to write rules.

Eslint is a rule scanner built on AST Parser. Espree is used as the AST parser by default. Rules write the callback for the AST event. After the linter processes the source code, it will adjust the processing function in rules according to the corresponding event.

1

Before getting into the details, think about the following questions: Where is the boundary of eslint? What functions can be achieved through eslint writing rules? What cannot be achieved?

Learn the Way to Write Rule Tests

Test before writing. How can we test with actual code when the rules are written?

Fortunately, it is simple. Write a json string and write the code.

Look at the example of no-console. It does not allow console.* statements to appear in code.

First, introduce the rule and test run object ruleTester:

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require("../../../lib/rules/no-console"),
    { RuleTester } = require("../../../lib/rule-tester");

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();

Then, we will call ruleTester's run function. Isn't it simple to place valid samples under valid and invalid samples under invalid?

Let's look at the valid ones first:

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

It is easier to pass, so we can give the code and options directly.

Then, there is the invalid:

    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" }] }
    ]
});

If it is invalid, it is necessary to judge whether the error information meets expectations.

Use mocha to run the preceding test script:

./node_modules/.bin/mocha tests/lib/rules/no-console.js

The running result is listed below:

  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)

If we put one in valid that fails to pass, an error will be reported. For example, add:

ruleTester.run("no-console", rule, {
    valid: [
        "Console.info(foo)",

        // single array item
        { code: "console.log('Hello,World')", options: [] },

The following errors will be reported:

  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)

It indicates that the console that we just added reports messageId as unexpected and nodeType as MemberExpression error.

We should put it in invalid:

    invalid: [

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

Run again, and it works:

    invalid
      ✓ console.log('Hello,World')

Getting Started with Rules

After running the test, we can write rules.

Let's look at the template of the rule first. We mainly need to provide meta objects and methods of create:

module.exports = {
    meta: {
        type: "规则类型,如suggestion",

        docs: {
            description: "规则描述",
            category: "规则分类:如Possible Errors",
            recommended: true,
            url: "说明规则的文档地址,如https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "是否可以修复,如code",
        schema: [] // Option
    },
    create: function(context) {
        return {
            // Event Callback
        };
    }
};

Generally speaking, an eslint rule can write an event callback function and use the AST and other information obtained in the context to analyze the callback function.

The API provided by context is relatively concise:

2

The code information class mainly uses getScope to get the information of the scope. getAncestors gets the AST node at the upper level, and getDeclaredVariables gets the variable table. The final trick is to get the source code getSourceCode directly and analyze it yourself.

markVariableAsUsed are used for cross-file analysis to analyze the use of variables.

The report function is used to output analysis results, such as error information, modification suggestions, and code for automatic repair.

This is too abstract. Look at the example.

Let's take no-console as an example. Look at the meta part first. This part does not involve logical code but is all configured:

    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."
        }
    },

Look at the callback function of no-console. Process one Program:exit only, which is the event of program exit:


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

Obtaining Scope and AST Information

First, get the scope information through context.getScope(). The following figure shows the correspondence between scope and AST:

3

The first thing we get in the previous example of the console statement is the global scope. Examples are listed below:

<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
    },
...

Let's take a look at 38 global variables and review the Javascript foundation:

    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]
    },

All variables are in a Map named set so we can get all variables.

We mainly need to find out whether there is a variable name called console for the rule of no-console. It can be written like this:

    getVariableByName(initScope, name) {
        let scope = initScope;

        while (scope) {
            const variable = scope.set.get(name);

            if (variable) {
                return variable;
            }

            scope = scope.upper;
        }

        return null;
    },

We can find out of the 38 variables listed, console is an undefined variable, so:

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

The result is null.

Then, we have to look for undefined variables. This part is in scope.through. We found the node whose name is console:

[
  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
  }
]

Then, we can write a function to check whether the name of reference is console:

        function isConsole(reference) {
            const id = reference.identifier;

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

Then, filter all undefined variables in scope.though with this function:

scope.through.filter(isConsole);

The last step is to output the report for the filtered reference reports:

                    references
                        .filter(isMemberAccessExceptAllowed)
                        .forEach(report);

Report problems using the report function of context:

        function report(reference) {
            const node = reference.identifier.parent;

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

The number of lines of code where the problem occurs can be obtained from a node.

Handle Statements of Specific Types

No-console is not the easiest way to write rules. We take it as an example, which has the most such problems. Let's take a look at how to deal with other statements that should not appear.

One of the simplest is to report errors for all types of statements, such as the no-continue rule, which reports errors when encountering ContinueStatement:

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

    }
};

no-debugger rules that do not allow debugger:

    create(context) {

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

    }

Do not use the the statement:

    create(context) {

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

    }

Variables, functions, and classes are not allowed to be defined in case statements:

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

    }

Multiple types of statements can share one processing function.

For example, it is not allowed to use construction methods to generate arrays:

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

Do not assign values to class definitions:

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

    }

The parameter of the function should not have the same name:

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

    }

If there are too many events, it can be written as an array, which is called a selector array:

const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
...
                        [loopSelector](node) {
                if (currentCodePath.currentSegments.some(segment => segment.reachable)) {
                    loopsToReport.add(node);
                }
            },

In addition to directly processing statement types, we can add some additional judgments to the type.

For example, the delete operator is not allowed:

    create(context) {

        return {

            UnaryExpression(node) {
                if (node.operator === "delete" && node.argument.type === "Identifier") {
                    context.report({ node, messageId: "unexpected" });
                }
            }
        };

    }

Do not use "==" and "! ="operator:

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

    }

Do not compare with -0:

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

Do not assign values to constants:

    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 -The End Event of the Statement

In addition to statement events, eslint provides exit events.

For example, we used the VariableDeclaration statement event in the example above. Let's look at how to use the VariableDeclaration called at the end of the VariableDeclaration: exit event.

Look at an example where var is not allowed to define variables:

        return {
            "VariableDeclaration:exit"(node) {
                if (node.kind === "var") {
                    report(node);
                }
            }
        };

If it is difficult to distinguish entry and exit. Look at an example where var is not allowed to define variables in non-functional blocks:

            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,

Call enterScope when entering the statement block. Call exitScope when exiting the statement block:

        function enterScope(node) {
            stack.push(node.range);
        }

        function exitScope() {
            stack.pop();
        }

Use Text Information - Literal

For example, it is not allowed to use the floating point number of "-. 7 " that omits 0. As such, Literal is used to process plain text information.

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

Octal numbers are not allowed:

    create(context) {
        return {
            Literal(node) {
                if (typeof node.value === "number" && /^0[0-9]/u.test(node.raw)) {
                    context.report({
                        node,
                        messageId: "noOcatal"
                    });
                }
            }
        };
    }

Analysis of Code Path

We discussed a code snippet earlier, and now, we string the code logic together to form a code path.

Code paths are sequential structures and also branches and loops.

4

In addition to the event handling methods above, we can handle CodePath events:

5

Event onCodePathStart and onCodePathEnd are used for the analysis of the entire path, while onCodePathSegmentStart, the onCodePathSegmentEnd is a fragment in CodePath. The onCodePathSegmentLoop is a loop fragment.

Look at an example of a loop:

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

Provide Code for Automatic Problem Repair

Finally, we will talk about how to fix the problem automatically.

We have previously reported problems using the context.report function. The automatic repair code is also returned to the caller through this interface.

We will replace "" and "!=" with "=" and "!==" as an example.

The only technique to fix this is adding an "=" to the operator containing problems:

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

In the final implementation, the replaceText function of the fixer is called:

                fix(fixer) {
                    if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
                        return fixer.replaceText(operatorToken, expectedOperator);
                    }
                    return null;
                }

The complete report code is listed below:

        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 supports four APIs for adding, two for deleting, and two for replacing classes:

6

Advanced Topics

Support of React JSX

Facebook has packaged the framework for us, and it looks familiar to write. No markVariableAsUsed example was provided before. Look at the following example:

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

The special feature of JSX is the addition of JSXOpenElement, JSXClosingElement, JSXOpenFragment, JSXClosingFragment, etc. to handle JSX events.

Support of TypeScript

As tslint is merged into eslint, the lint functionality of TypeScript is hosted by the typescript-eslint.

Since estree only supports javascript, typescript-eslint provides a parser compatible with the estree format.

Since it is a lint of ts, it naturally has the support of ts and a new tool method, and its basic architecture is still the same as eslint:

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',
          });
        }
      },
    };
  },
});

Replacing the AST Parser for ESLint

ESLint supports the use of a third-party AST parser, just as Babel supports ESLint. We can replace espree with @babel/eslint-parser. After installing the plug-in, edit the .eslintrc.js:

module.exports = {
  parser: "@babel/eslint-parser",
};

Babel supports TypeScript.

StyleLint

After discussing Eslint, take a little more time to look at StyleLint.

StyleLint and Eslint have the same architectural ideas, and both are tools for processing AST event analysis.

However, css uses different AST Parser, such as Post CSS API, postcss-value-parser, postcss-selector-parser, etc.

Look at an example:

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

It is a familiar report function that can support the generation of autofix.

Summary

We have sorted out the general framework of eslint rule writing.

In the actual process of writing rules, a deeper understanding of AST and language details is required. We will give a specific explanation in the follow-up.

I hope you can write more robust code by writing a checker suitable for your business.

0 0 0
Share on

Alibaba F(x) Team

66 posts | 3 followers

You may also like

Comments