天天看點

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

自己動手寫符合自己業務需求的eslint規則

使用eslint和stylelint之類的工具掃描前端代碼現在已經基本成為前端同學的标配。但是,業務這麼複雜,指望eslint等提供的工具完全解決業務中遇到的代碼問題還是不太現實的。我們一線業務同學也要有自己的寫規則的能力。

eslint是建構在AST Parser基礎上的規則掃描器,預設情況下使用espree作為AST解析器。rules寫好對于AST事件的回調,linter處理源代碼之後會根據相應的事件來回調rules中的處理函數。

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

另外,在進入細節之前,請思考一下:eslint的邊界在哪裡?哪些功能是通過eslint寫規則可以做到的,哪些是用eslint無法做到的?

先學會如何寫規則測試

兵馬未動,測試先行。規則寫出來,如何用實際代碼進行測試呢?

所幸非常簡單,直接寫個json串把代碼寫進來就好了。

我們來看個no-console的例子,就是不允許代碼中出現console.*語句的規則。

首先把規則和測試運作對象ruleTester引進來:

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

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

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

const ruleTester = new RuleTester();           

然後我們就直接調用ruleTester的run函數就好了。有效的樣例放在valid下面,無效的樣例放在invalid下面,是不是很簡單。

我們先看下有效的:

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

能通過的情況比較容易,我們就直接給代碼和選項就好。

然後是無效的:

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

無效的要判斷下出錯資訊是不是符合預期。

我們使用mocha運作下上面的測試腳本:

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

運作結果如下:

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)           

如果在valid裡面放一個不能通過的,則會報錯,比如我們加一個:

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

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

就會報下面的錯:

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)           

說明我們剛加的console是會報一個messageId為unexpected,而nodeType為MemberExpression的錯誤。

我們應将其放入到invalid裡面:

invalid: [

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

再運作,就可以成功了:

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

規則入門

會跑測試之後,我們就可以寫自己的規則啦。

我們先看下規則的模闆,其實主要要提供meta對象和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: [] // 選項
    },
    create: function(context) {
        return {
            // 事件回調
        };
    }
};           

總體來說,一個eslint規則所能做的事情,就是寫事件回調函數,在回調函數中使用context中擷取的AST等資訊進行分析。

context提供的API是比較簡潔的:

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

代碼資訊類主要我們使用getScope擷取作用域的資訊,getAncestors擷取上一級AST節點,getDeclaredVariables擷取變量表。最後的絕招是直接擷取源代碼getSourceCode自己分析去。

markVariableAsUsed用于跨檔案分析,用于分析變量的使用情況。

report函數用于輸出分析結果,比如報錯資訊、修改建議和自動修複的代碼等。

這麼說太抽象了,我們來看例子。

還以no-console為例,我們先看meta部分,這部分不涉及邏輯代碼,都是一些配置:

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

我們再看no-console的回調函數,隻處理一處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);
                }
            }
        };           

擷取作用域和AST資訊

我們首先通過context.getScope()擷取作用域資訊。作用域與AST的對應關系如下圖:

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

我們前面的console語句的例子,首先拿到的都是全局作用域,舉例如下:

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

具體看一下38個全局變量,複習下Javascript基礎吧:

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

我們看到,所有的變量,都以一個名為set的Map中,這樣我們就可以以周遊擷取所有的變量。

針對no-console的規則,我們主要是要查找是否有叫console的變量名。于是可以這麼寫:

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

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

            if (variable) {
                return variable;
            }

            scope = scope.upper;
        }

        return null;
    },           

我們可以在剛才列出的38個變量中發現,console是并沒有定義的變量,是以

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

的結果是null.

于是我們要去查找未定義的變量,這部分是在scope.through中,果然找到了name是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
  }
]           

這樣我們就可以寫個檢查reference的名字是不是console的函數就好:

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

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

然後用這個函數去filter scope.though中的所有未定義的變量:

scope.through.filter(isConsole);           

最後一步是輸出報告,針對過濾出的reference進行報告:

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

報告問題使用context的report函數:

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

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

發生問題的代碼行數可以從node中擷取到。

處理特定類型的語句

no-console從規則書寫上并不是最容易的,我們以其為例主要是這類問題最多。下面我們舉一反三,看看針對其它不應該出現的語句該如何處理。

其中最簡單的就是針對一類語句統統報錯,比如no-continue規則,就是遇到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" });
            }
        };

    }
};           

不允許使用debugger的no-debugger規則:

create(context) {

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

    }           

不許使用with語句:

create(context) {

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

    }           

在case語句中不許定義變量、函數和類:

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

    }           

多個類型語句可以共用一個處理函數。

比如不許使用構造方法生成數組:

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

不許給類定義指派:

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

    }           

函數的參數不允許重名:

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

    }           

如果事件太多的話,可以寫成一個數組,這被稱為選擇器數組:

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

除了直接處理語句類型,還可以針對類型加上一些額外的判斷。

比如不允許使用delete運算符:

create(context) {

        return {

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

    }           

不準使用"=="和"!="運算符:

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

    }           

不許和-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 }
                        });
                    }
                }
            }
        };
    }           

不準給常量指派:

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 - 語句結束事件

除了語句事件之外,eslint還提供了:exit事件。

比如上面的例子我們使用了VariableDeclaration語句事件,我們下面看看如何使用VariableDeclaration結束時調用的VariableDeclaration:exit事件。

我們看一個不允許使用var定義變量的例子:

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

如果覺得進入和退出不好區分的話,我們來看一個不允許在非函數的塊中使用var來定義變量的例子:

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,           

這些邏輯的作用是,進入語句塊的時候調用enterScope,退出語句塊的時候調用exitScope:

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

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

直接使用文字資訊 - Literal

比如不允許使用"-.7"這樣省略了0的浮點數。此時使用Literal來處理純文字資訊。

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

不準使用八進制數字:

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

代碼路徑分析

前面我們讨論的基本都是一個代碼片段,現在我們把代碼邏輯串起來,形成一條代碼路徑。

代碼路徑就不止隻有順序結構,還有分支和循環。

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

除了采用上面的事件處理方法之外,我們還可以針對CodePath事件進行處理:

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

事件onCodePathStart和onCodePathEnd用于整個路徑的分析,而onCodePathSegmentStart, onCodePathSegmentEnd是CodePath中的一個片段,onCodePathSegmentLoop是循環片段。

我們來看一個循環的例子:

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

提供問題自動修複的代碼

最後,我們講講如何給問題給供自動修複代碼。

我們之前報告問題都是使用context.report函數,自動修複代碼也是通過這個接口傳回給調用者。

我們以将"=="和"!="替換成"==="和"!=="為例。

這個fix沒有多少技術含量哈,就是給原來發現問題的運算符多加一個"=":

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

最終實作時是調用了fixer的replaceText函數:

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

完整的report代碼如下:

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支援4個添加API,2個删除API,2個替換類的API:

自己動手寫符合自己業務需求的eslint規則自己動手寫符合自己業務需求的eslint規則

進階話題

React JSX的支援

Facebook給我們封裝好了架構,寫起來也是蠻眼熟的。剛好之前沒有舉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);
      },
    };
  },
};
           

JSX的特殊之處是增加了JSXOpenElement, JSXClosingElement, JSXOpenFragment, JSXClosingFragment等處理JSX的事件。

TypeScript的支援

随着tslint合并到eslint中,TypeScript的lint功能由typescript-eslint承載。

因為estree隻支援javascript,typescript-eslint提供相容estree格式的parser.

既然是ts的lint,自然是擁有了ts的支援,擁有了新的工具方法,其基本架構仍是和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',
          });
        }
      },
    };
  },
});           

更換ESLint的AST解析器

ESLint支援使用第三方AST解析器,剛好Babel也支援ESLint,于是我們就可以用@babel/eslint-parser來替換espree. 裝好插件之後,修改.eslintrc.js即可:

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

Babel自帶支援TypeScript。

StyleLint

說完了Eslint,我們再花一小點篇幅看下StyleLint。

StyleLint與Eslint的架構思想一脈相承,都是對于AST的事件分析進行處理的工具。

隻不過css使用不同的AST Parser,比如Post CSS API, postcss-value-parser, postcss-selector-parser等。

我們來看個例子體感一下:

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

也是熟悉的report函數回報,也可以支援autofix的生成。

小結

以上,我們基本将eslint規則寫法的大緻架構梳理清楚了。

當然,實際寫規剛的過程中還需要對于AST以及語言細節有比較深的了解。我們會在後續做專題講解。

預祝大家通過寫出适合自己業務的檢查器,寫出更健壯的代碼。

繼續閱讀