By Xu Lun, F(X)Team, Ali Tao Department

Scanning front-end code using tools like ESLint and Stylelint is now almost standard for front-end students. However, with a business as complex as this, it is unrealistic to expect tools such as ESLint to completely solve code problems encountered in a business. Our front-line business students should also have their own ability to write rules.

Eslint is a rule scanner built on top of AST Parser and uses ESpree as the AST Parser by default. Rules writes the callbacks to AST events, and Linter calls the rules handler back and forth based on the event after processing the source code.


Also, before getting into the details, consider: Where are the boundaries of ESLint? What functionality is possible with ESLint writing rules, and what is not possible with ESLint?

Learn how to write rule tests first

No move, test first. Once the rules are written, how do you test them in real code?

Fortunately, it’s pretty simple, just write a JSON string and put the code in.

Let’s look at an example of no-console, a rule that doesn’t allow console.* statements 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();
Copy the code

We then call ruleTester’s run function directly. Is it easy to place valid samples under VALID and invalid samples under invalid?

Let’s take a look at what works:

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)"].Copy the code

It’s easier if it passes, so let’s just give the code and the options.

And then 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"}}}]]);Copy the code

Invalid to determine whether the error message is expected.

Let’s run the above test script using Mocha:

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

The running results are as follows:

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'); Log (foo) invalid ✓ console.log(foo) ✓ console.error(foo) ✓ console.info(foo) ✓ console.warn(foo) ✓ 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)Copy the code

If we put an invalid value in valid, we will get an error. For example, if we add:

ruleTester.run("no-console", rule, {
    valid: [
        "Console.info(foo)".// single array item
        { code: "console.log('Hello,World')".options: []},Copy the code

Will report the following error:

  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)
Copy the code

The console we just added will report an error with messageId unexpected and nodeType MemberExpression.

We should put it in invalid:

    invalid: [

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

Run it again and it will be successful:

Invalid ✓ console. The log (' Hello, World ')Copy the code

Introduction to the rules

After we know how to run the test, we can write our own rules.

Let’s take a look at the template for the rule, which provides a meta object and a create method:

module.exports = {
    meta: {
        type: "Rule type, such as suggestion".docs: {
            description: "Rule Description".category: "Rule classification: e.g. Possible Errors".recommended: true.url: "The rules of that document addresses, such as https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "Can it be fixed, as in code?".schema: [] / / options
    },
    create: function(context) {
        return {
            // Event callback}; }};Copy the code

In general, all an ESLint rule can do is write event callbacks that use information such as the AST retrieved from the context for analysis.

Context provides a succinct API:


In the code information class, we mainly use getScope to obtain the information of scope, getpiece to obtain the upper AST node, and getDeclaredVariables to obtain the change table. The last trick is to get the source code directly from getSourceCode and analyze it yourself.

MarkVariableAsUsed is used for cross-file analysis to analyze the use of variables.

The report function is used to output analysis results, such as error messages, modification suggestions, and auto-fixed code.

That’s too abstract. Let’s do an example.

Taking no-Console as an example, let’s first look at the Meta section, which doesn’t involve logical code and is all configuration:

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

The no-console callback handles only one Program:exit, which is the Program exit event:


        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); }}};Copy the code

Gets scope and AST information

We first getScope information through context.getscope (). The mapping between scope and AST is shown as follows:


In our previous console statement examples, we first get the global scope, as shown 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},...Copy the code

Take a look at 38 global variables and review Javascript basics:

    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]
    },
Copy the code

We see that all the variables are in a Map called set, so we can get all the variables in a traversal.

For the no-console rule, we’re basically looking for a variable name called console. So you could write:

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

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

            if (variable) {
                return variable;
            }

            scope = scope.upper;
        }

        return null;
    },
Copy the code

As you can see from the list of 38 variables, console is not defined, so

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

The result is null.

In scope. Through, we find 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
  }
]
Copy the code

Check that reference’s name is a function of console:

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

            return id && id.name === "console";
        }
Copy the code

Then use this function to filter all undefined variables in scope.though:

scope.through.filter(isConsole);
Copy the code

The last step is to output the report and report the filtered reference:

                    references
                        .filter(isMemberAccessExceptAllowed)
                        .forEach(report);
Copy the code

To report a problem, use the context’s report function:

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

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

The number of lines of code in question can be obtained from Node.

Handles a particular type of statement

No-console is not the easiest rule to write, and we use it as an example because it has the most problems. Let’s take a look at what we can do about other statements that shouldn’t appear.

ContinueStatement is the simplest of these statements, such as the no-continue rule, where ContinueStatement returns an error:

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

The no-Debugger rule disallows the use of debugger:

    create(context) {

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

Do not use the with statement:

    create(context) {

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

Variables, functions, and classes cannot be defined in case statements:

    create(context) {
        function isLexicalDeclaration(node) {
            switch (node.type) {
                case "FunctionDeclaration":
                case "ClassDeclaration":
                    return true;
                case "VariableDeclaration":
                    returnnode.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"}); }}}}; }Copy the code

Multiple type statements can share a single handler.

For example, don’t use constructors 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
        };
Copy the code

Do not assign 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
        };

    }
Copy the code

Function arguments cannot 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
        };

    }
Copy the code

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

In addition to dealing directly with statement types, you can add some additional judgment 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"}); }}}; }Copy the code

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

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 } }); }}}}; }Copy the code

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); }}}; }Copy the code

:exit – Statement end event

In addition to statement events, ESLint also provides :exit events.

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

Let’s look at an example where var is not allowed to define a variable:

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

If it’s hard to tell the difference between entry and exit, let’s look at an example where var is not allowed to define a variable in a non-function block:

            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,
Copy the code

EnterScope is called when entering a block and exitScope is called when exiting a block:


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

        function exitScope() {
            stack.pop();
        }
Copy the code

Use the Literal message directly – Literal

For example, floating point numbers like “-.7” that omit 0 are not allowed. Literal is used at this point 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")}); }}}}; }Copy the code

Octal digits 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"}); }}}; }Copy the code

Code path analysis

What we’ve discussed so far is basically a code snippet. Now let’s string together the code logic to form a code path.

Code paths have not only sequential structures, but branches and loops.


In addition to the above method of event handling, we can also handle the CodePath event:


The events onCodePathStart and onCodePathEnd are used to analyze the entire path, while onCodePathSegmentStart, onCodePathSegmentEnd is a fragment of CodePath, OnCodePathSegmentLoop is a loop fragment.

Let’s 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(a);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)) {
                    constloop = 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"})); }}; }Copy the code

Provides code to fix problems automatically

Finally, we’ll talk about how to give problems to auto-fix code.

We previously reported problems using context.report, and the auto-fix code is returned to the caller through this interface.

We will “and “! Replace =” with “=” and “! = = “as an example.

Fix () : = fix () : = fix ();

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

The final implementation calls Fixer’s replaceText function:

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

The full report code is as follows:

        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; }}); }Copy the code

Fixer supports 4 add apis, 2 delete apis, and 2 replace class apis:


Advanced topics

React JSX support

Facebook has encapsulated the framework for us, and it looks familiar to write about. MarkVariableAsUsed = markVariableAsUsed

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); }}; }};Copy the code

The special feature of JSX is to add JSXOpenElement, JSXClosingElement, JSXOpenFragment, JSXClosingFragment to handle JSX events.

The support of the TypeScript

With tsLint incorporated into ESLint, TypeScript’s Lint functionality is carried by typescript-ESLint.

Because Estree only supports javascript, typescript-ESLint provides parser compatible with estree format.

Lint is supported by TS, and with new tools and methods, the 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'}); }}}; }});Copy the code

Replace the AST parser for ESLint

ESLint supports the use of third-party AST parsers, and Babel also supports ESLint, so we could replace esPree with @babel/eslint-parser. After installing the plugin, modify.eslintrc.js:

module.exports = {
  parser: "@babel/eslint-parser"};Copy the code

Babel comes with TypeScript support.

StyleLint

Having said Eslint, let’s take a little longer look at StyleLint.

StyleLint and Eslint are both tools that handle AST event analysis.

The CSS uses a different AST Parser, such as Post CSS API, PostCSs-value-parser, postCSs-selector – Parser, etc.

Let’s take a look at an example of child body sense:

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

Is also the familiar report function returns, can also support autofix generation.

summary

Above, we’ve laid out the general framework of how ESLint rules are written.

Of course, the actual writing process also requires a deep understanding of the AST and language details. We’ll talk about it later.

Good luck writing more robust code by writing checkers that are appropriate for your business.



Tao department front – F-X-team opened a weibo! (Visible after microblog recording)
In addition to the article there is more team content to unlock 🔓