ESLint檢測部分源碼解讀

寫在前面

以下是我閱讀eslint源碼的過程 , 在這過程中 , 我首先會自己寫一個eslint插件的demo , 然后自己定義一個規(guī)則 , 然后再進行檢測 , 根據(jù)調(diào)用棧迅速的一步一步看下去 , 大致知道是怎么樣的流程后 ; 接著再重新拆分每一步是怎么做的 , 分析規(guī)則和插件的運用 , 從而更加鞏固自己對于eslint插件的開發(fā) ; 基于這個想法 , 我們就開始吧

在大致流程中會交代eslint的修復過程 , 但是也是大致的說明一下 ; 詳細拆分的過程是沒有分析修復過程的

先上github上面把eslint源碼clone下來eslint , git clone https://github.com/eslint/eslint.git

第一節(jié) . 大致流程

1. 找到eslint命令入口文件

打開源碼 , 我們通過package.json查看eslint的命令入口 , 在bin下的eslint.js

{
   "bin": {
    "eslint": "./bin/eslint.js"
  }
}

2. 進入./bin/eslint.js

"use strict";
require("v8-compile-cache");
// 讀取命令中 --debug參數(shù), 并輸出代碼檢測的debug信息和每個插件的耗時
if (process.argv.includes("--debug")) {
    require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
}

// 這里省略了readStdin getErrorMessage onFatalError 三個方法

// 主要看下面IIFE , 而且這個是用了一個promise包裹 , 并且有捕捉函數(shù)的一個IIFE
(async function main() {
    process.on("uncaughtException", onFatalError);
    process.on("unhandledRejection", onFatalError);

    // Call the config initializer if `--init` is present.
    if (process.argv.includes("--init")) {
        await require("../lib/init/config-initializer").initializeConfig();
        return;
    }

    // 最終這里讀取了 lib/cli, lib/cli才是執(zhí)行eslint開始的地方
    process.exitCode = await require("../lib/cli").execute(
        process.argv,
        process.argv.includes("--stdin") ? await readStdin() : null
    );
}()).catch(onFatalError);

2.1 lib/cli執(zhí)行腳本文件

// 其他代碼
const cli = {
  // args 就是那些 --cache --debug等參數(shù)
    async execute(args, text) {
        /** @type {ParsedCLIOptions} */
      
      // 開始對參數(shù)格式化
        let options;
        try {
            options = CLIOptions.parse(args);
        } catch (error) {
            log.error(error.message);
            return 2;
        }
      
      // 獲取eslint編譯器實例
        const engine = new ESLint(translateOptions(options));
      
      // results作為接收收集問題列表的變量
        let results;
        if (useStdin) {
            results = await engine.lintText(text, {
                filePath: options.stdinFilename,
                warnIgnored: true
            });
        } else {
          // 進入主流程
            results = await engine.lintFiles(files);
        }

      
      // printResults進行命令行輸出
        let resultsToPrint = results;
        if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
            const { errorCount, fatalErrorCount, warningCount } = countErrors(results);
          // 判斷是否有出錯的退出碼
          // ...
        }
        return 2;
    }
};

module.exports = cli;

3. 如何fix

以字符串為例

比如我們寫了一個自定義的eslint插件如下

replaceXXX.js 看代碼塊replaceXXX

// 代碼塊replaceXXX
module.exports = {
  meta: {
    type: 'problem', // "problem": 指的是該規(guī)則識別的代碼要么會導致錯誤斤彼,要么可能會導致令人困惑的行為。開發(fā)人員應該優(yōu)先考慮解決這個問題踩麦。
    docs: {
      description: 'XXX 不能出現(xiàn)在代碼中!',
      category: 'Possible Errors', // eslint規(guī)則首頁的分類: Possible Errors卿堂、Best Practices盅安、Strict Mode嚣潜、Varibles冬骚、Stylistic Issues、ECMAScript 6、Deprecated只冻、Removed
      recommended: false, // "extends": "eslint:recommended"屬性是否啟用該規(guī)則
      url: '', // 指定可以訪問完整文檔的URL
    },
    fixable: 'code', // 該規(guī)則是否可以修復
    schema: [
      {
        type: 'string',
      },
    ],
    messages: {
      unexpected: '錯誤的字符串XXX, 需要用{{argv}}替換',
    },
  },
  create: function (context) {
    const str = context.options[0];
    function checkLiteral(node) {
      if (node.raw && typeof node.raw === 'string') {
        if (node.raw.indexOf('XXX') !== -1) {
          context.report({
            node,
            messageId: 'unexpected',
            data: {
              // 占位數(shù)據(jù)
              argv: str,
            },
            fix: fixer => {
              // 這里獲取到字符串中的XXX就會直接替換掉
              return fixer.replaceText(node, str);
            },
          });
        }
      }
    }
    return {
      Literal: checkLiteral,
    };
  },
};

4. 插件使用說明

因為在本地中使用 , 所以插件使用的是用的是npm link模式

my-project下的.eslintrc.json

{
  //..其他配置
  "plugins": [
    // ...
    "eslint-demo"
  ],
  "rules": {
    "eslint-demo/eslint-demo": ["error", "LRX"], // 將項目中所有的XXX字符串轉(zhuǎn)換成MMM
  }
}

my-project/app.js

// app.js
function foo() {
  const bar = 'XXX';
  console.log(name);
}

在my-project中使用 , 即可修復完成

npx eslint --fix ./*.js

4.1 那源碼中是如何fix的呢?

eslint的fix就是執(zhí)行了插件文件里面create方法如下

create: function (context) {
  // 獲取目標項目中.eslintrc.json文件下的rules的第二個參數(shù)
  const str = context.options[0];
    context.report({
    // ...
    fix: fixer => {
      return fixer.replaceText(node, `'${str}'`);
    },
  })
}

在eslint源碼中fix過程的代碼在lib/linter/source-code-fixer.js和lib/linter/linter.js , 而lib/linter/linter.js文件是驗證我們的修復代碼的文件是否合法以及接收修復后的文件 ;

4.2 lib/linter/linter.js , fix方面的源碼

verifyAndFix(text, config, options) {
        let messages = [],
            fixedResult,
            fixed = false,
            passNumber = 0, // 記錄修復次數(shù), 這里會和最大修復次數(shù)10次比較, 大于10次或者有修復完成的標志即可停止修復
            currentText = text; // 修復前的源碼字符串
        const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
        const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;

        // 每個問題循環(huán)修復10次以上或者已經(jīng)修復完畢f(xié)ixedResult.fixed, 即可判定為修復完成
        do {
            passNumber++;

            debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
            messages = this.verify(currentText, config, options);

            debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
            // 執(zhí)行修復代碼
            fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);

            /*
             * stop if there are any syntax errors.
             * 'fixedResult.output' is a empty string.
             */
            if (messages.length === 1 && messages[0].fatal) {
                break;
            }

            // keep track if any fixes were ever applied - important for return value
            fixed = fixed || fixedResult.fixed;

            // update to use the fixed output instead of the original text
            currentText = fixedResult.output;

        } while (
            fixedResult.fixed &&
            passNumber < MAX_AUTOFIX_PASSES
        );

        /*
         * If the last result had fixes, we need to lint again to be sure we have
         * the most up-to-date information.
         */
        if (fixedResult.fixed) {
            fixedResult.messages = this.verify(currentText, config, options);
        }

        // ensure the last result properly reflects if fixes were done
        fixedResult.fixed = fixed;
        fixedResult.output = currentText;

        return fixedResult;
    }
4.2.1 lib/linter/source-code-fixer.js , 修復代碼的主文件
/*
這里會進行一些簡單的修復, 如果是一些空格換行, 替換等問題, 這里會直接通過字符串拼接并且輸出一個完整的字符串
*/
SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
    debug("Applying fixes");
    if (shouldFix === false) {
        debug("shouldFix parameter was false, not attempting fixes");
        return {
            fixed: false,
            messages,
            output: sourceText
        };
    }

    // clone the array
    const remainingMessages = [],
        fixes = [],
        bom = sourceText.startsWith(BOM) ? BOM : "",
        text = bom ? sourceText.slice(1) : sourceText;
    let lastPos = Number.NEGATIVE_INFINITY,
        output = bom;

    // 命中并修復問題
    /*
        problem的結(jié)構(gòu)為
        {
        ruleId: 'eslint-demo/eslint-demo', // 插件名稱
        severity: 2,
        message: '錯誤的字符串XXX, 需要用MMM替換', // 提示語
        line: 17, // 行數(shù)
        column: 18, // 列數(shù)
        nodeType: 'Literal', // 當前節(jié)點在AST中是什么類型
        messageId: 'unexpected', 對應meta.messages.XXX,message可以直接用message替換
        endLine: 17, // 結(jié)尾的行數(shù)
        endColumn: 23, // 結(jié)尾的列數(shù)
        fix: { range: [ 377, 382 ], text: "'MMM'" } // 該字符串在整個文件字符串中的位置
      }
    */
    function attemptFix(problem) {
        const fix = problem.fix;
        const start = fix.range[0]; // 記錄修復的起始位置
        const end = fix.range[1]; // 記錄修復的結(jié)束位置

        // 如果重疊或為負范圍庇麦,則將其視為問題
        if (lastPos >= start || start > end) {
            remainingMessages.push(problem);
            return false;
        }

        // 移除非法結(jié)束符.
        if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
            output = "";
        }

        // 拼接修復后的結(jié)果, output是一個全局變量
        output += text.slice(Math.max(0, lastPos), Math.max(0, start));
        output += fix.text;
        lastPos = end;
        return true;
    }
        /*
        傳進來的messages每一項
            {
        ruleId: 'eslint-demo/eslint-demo',
        severity: 2,
        message: '錯誤的字符串XXX, 需要用MMM替換',
        line: 17,
        column: 18,
        nodeType: 'Literal',
        messageId: 'unexpected',
        endLine: 17,
        endColumn: 23,
        fix: { range: [Array], text: "'MMM'" }
      },
        */
    messages.forEach(problem => {
        if (Object.prototype.hasOwnProperty.call(problem, "fix")) {
            fixes.push(problem);
        } else {
            remainingMessages.push(problem);
        }
    });
        // 當fixes有需要修復的方法則進行修復
    if (fixes.length) {
        debug("Found fixes to apply");
        let fixesWereApplied = false;

        for (const problem of fixes.sort(compareMessagesByFixRange)) {
            if (typeof shouldFix !== "function" || shouldFix(problem)) {
                attemptFix(problem);

                // attemptFix方法唯一失敗的一次是與之前修復的發(fā)生沖突, 這里默認將已經(jīng)修復好的標志設置為true
                fixesWereApplied = true;
            } else {
                remainingMessages.push(problem);
            }
        }
        output += text.slice(Math.max(0, lastPos));

        return {
            fixed: fixesWereApplied,
            messages: remainingMessages.sort(compareMessagesByLocation),
            output
        };
    }

    debug("No fixes to apply");
    return {
        fixed: false,
        messages,
        output: bom + text
    };

};

二 . 詳細流程

1. 項目代碼準備

首先我們準備我們需要檢測的工程結(jié)構(gòu)如下

├── src
│   ├── App.tsx
│   ├── index.tsx
│   └── typings.d.ts
├── .eslintignore
├── .eslintrc.json
├── ....其他文件
└── package.json

1.1 App.tsx

import React from 'react';

function say() {
  const name = 'XXX';
  console.log(name);
}

const App = () => {
  say();
  return <div>app</div>;
};

export default App;

1.2 .eslintrc.json

{
  "root": true,
  "extends": [
    "airbnb",
    "airbnb/hooks",
    "airbnb-typescript",
    "plugin:react/recommended",
  ],
  "parserOptions": {
    "project": "./tsconfig.eslint.json"
  },
  "plugins": [
    "eslint-demo" /*這里是我們自定義的eslint插件*/
  ],
  "rules": {
    "react/function-component-definition": ["error", {
      "namedComponents": "arrow-function"
    }],
    "strict": ["error", "global"],
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "error",
    "eslint-demo/eslint-demo":  ["error", "LRX"], /*這里是我們自定義的eslint插件如何替換規(guī)則*/
  }
}

1.3 eslint自定義插件

使用上面大致流程的eslint的自定義插件

2. 代碼流程

當我們輸入 npx eslint ./src/*.tsx的時候做了什么呢

2.1 第一層bin/eslint.js

入口文件 在 ./bin/eslint.js , 在這個文件中通過一個匿名自執(zhí)行promise函數(shù) , 引入了 lib/cli文件并且通過一下代碼塊001

// 代碼塊001
process.exitCode = await require("../lib/cli").execute(
        process.argv,
        process.argv.includes("--stdin") ? await readStdin() : null
 );

2.2 進入第二層lib/cli

lib/cli文件的execute方法(查看代碼塊003) , 主要是返回一個退出碼 , 用作判斷eslint是否執(zhí)行完畢 , 傳入的參數(shù)是我們npx eslint --fix ./src 的fix這個參數(shù) , 以及跟在--stdin后面的參數(shù) ; 然后我們進入lib/cli , 因為上面直接調(diào)用了execute方法 , 那么我們就看看cli里面execute方法 , 首先定義了一個options參數(shù) , 然后調(diào)用了CLIOptions.parse(args)方法 (查看代碼塊003), 這個方法是其他包optionator里面的方法 , 我們進入進去就可以看到parse的方法了 , 這個方法就是switch case將不同的參數(shù)處理裝箱打包進行返回 , 這里面還用了一個.ls包進行map管理 , 且在保證用戶輸入的時候用了type-check這個包進行輸入和測試進行管理 , 在非ts環(huán)境下 , 進行類似Haskell的類型語法檢查 ; 好這時候我們拿到了經(jīng)過裝箱打包的options了 , 這個key名不是我起的 , 它源碼就這樣(好隨便啊) ; 得到了如下結(jié)構(gòu), (看代碼塊002)

// 代碼塊002
{
  eslintrc: true,
  ignore: true,
  stdin: false,
  quiet: false,
  maxWarnings: -1,
  format: 'stylish',
  inlineConfig: true,
  cache: false,
  cacheFile: '.eslintcache',
  cacheStrategy: 'metadata',
  init: false,
  envInfo: false,
  errorOnUnmatchedPattern: true,
  exitOnFatalError: false,
  _: [ './src/App.tsx', './src/index.tsx' ]
} 

得到這個結(jié)構(gòu)后 , 就通過轉(zhuǎn)換translateOptions函數(shù)進行配置的轉(zhuǎn)換 , 這里我猜是因為一些人接手別人的代碼 , 需要寫的一個轉(zhuǎn)換文件 ; 接著開始創(chuàng)建我們的一個eslint的編譯器 const engine = new ESLint(translateOptions(options));

2.3 進入第三層lib/eslint/eslint.js

在lib/eslint/eslint.js里面的Eslint類 , Eslint這個類的構(gòu)造函數(shù)首先會將所有的配置進行檢驗 , 在ESLint類里面會創(chuàng)建一個cli的編譯器 ,

2.4 進入第四層lib/cli-engine/cli-engine.js

這個編譯器在lib/cli-engine/cli-engine.js里面 , 這里主要是處理一下緩存以及eslint內(nèi)部的默認規(guī)則 ; 然后回來lib/eslint/eslint.js里面的Eslint類 , 接下來就是獲取從cli-engine.js的內(nèi)部插槽 , 設置私有的內(nèi)存快照 , 判斷是否更新 ,如果更新就刪除緩存 ;

2.5 進入第五層lib/linter/linter.js

這一層就比較簡單了 , 就是用了map結(jié)構(gòu)記錄了cwd , lastConfigArray , lastSourceCode , parserMap , ruleMap , 分別是當前文件路徑 , 最新的配置數(shù)據(jù) , 最新的源碼使用編譯器espree解析出來的ast源碼字符串 , 編譯器(記錄我們用的是什么編譯器默認是espree) , 以及規(guī)則map

2.6 返回到第二層

接著返回到第二層繼續(xù)走下去 , 因為不是使用--stdin , 所以直接看else , 執(zhí)行了engine.lintFiles

// 代碼塊003
const cli = {
  // 其他方法
  async execute() {
    let options;
    try {
      options = CLIOptions.parse(args);
    } catch (error) {
      log.error(error.message);
      return 2;
    }
    // 其他驗證參數(shù)的代碼
    const engine = new ESLint(translateOptions(options));
    let results;
    if (useStdin) {
      results = await engine.lintText(text, {
        filePath: options.stdinFilename,
        warnIgnored: true
      });
    } else {
      results = await engine.lintFiles(files);
    }
    let resultsToPrint = results;

        if (options.quiet) {
            debug("Quiet mode enabled - filtering out warnings");
            resultsToPrint = ESLint.getErrorResults(resultsToPrint);
        }
                // 最后會來到這里printResults方法, 我們看代碼塊012
        if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {

            // Errors and warnings from the original unfiltered results should determine the exit code
            const { errorCount, fatalErrorCount, warningCount } = countErrors(results);

            const tooManyWarnings =
                options.maxWarnings >= 0 && warningCount > options.maxWarnings;
            const shouldExitForFatalErrors =
                options.exitOnFatalError && fatalErrorCount > 0;

            if (!errorCount && tooManyWarnings) {
                log.error(
                    "ESLint found too many warnings (maximum: %s).",
                    options.maxWarnings
                );
            }

            if (shouldExitForFatalErrors) {
                return 2;
            }

            return (errorCount || tooManyWarnings) ? 1 : 0;
        }

        return 2;
  }
}

從第二層中可以看到 , engine是通過ESLint類創(chuàng)建出來的所以我們?nèi)サ降谌龑拥膌ib/eslint/eslint.js的lintFiles方法 , (看代碼塊004)

// 代碼塊004
async lintFiles(patterns) {
  if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
    throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
  }
  // privateMembersMap在new ESLint構(gòu)造函數(shù)的時候已經(jīng)將cliEngine, set進去了, 所以這里直接獲取即可
  const { cliEngine } = privateMembersMap.get(this);
    // processCLIEngineLintReport是返回linting指定的文件模式, 傳入的參數(shù)是cliEngine, 并且第二個參數(shù)執(zhí)行了executeOnFiles, 我們看看cliEngine這個類的executeOnFiles做了什么
  return processCLIEngineLintReport(
    cliEngine,
    cliEngine.executeOnFiles(patterns)
  );
}

2.7 再次進入第四層

再次進入第四層的CLIEngine類下的executeOnFiles方法, 從他接受的參數(shù)和方法名可以知道, 這個executeOnFiles主要是處理文件和文件組的問題 , 看代碼塊005

// 代碼塊005
executeOnFiles(patterns) {
  const results = [];
  // 這里是一個很迷的操作, 官方在這里手動把所有的最新配置都清除了, 這個是從外部傳進來的, 但是它先手動清除然后下面再在迭代器里面每個都引用一遍, 
   lastConfigArrays.length = 0;
  //... 其他函數(shù)
  // 清除上次使用的配置數(shù)組。
  // 清除緩存文件, 使用fs.unlinkSync進行緩存文件的請求, 當不存在此類文件或文件系統(tǒng)為只讀(且緩存文件不存在)時忽略錯誤
  // 迭代源文件并且放到results中
  // fileEnumerator.iterateFiles(patterns), 這里的patterns還是一個需要eslint文件的絕對地址, 這時候還沒有進行ast分析, fileEnumerator.iterateFiles這個方法是一個迭代器, 為了防止讀寫文件的時候有延遲, 這里需要使用迭代器
  for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) {
            if (ignored) {
                results.push(createIgnoreResult(filePath, cwd));
                continue;
            }

            // 收集已使用的廢棄的方法, 這里就是很迷, 上面明明清除了lastConfigArrays, 所以這里肯定都是true
            if (!lastConfigArrays.includes(config)) {
                lastConfigArrays.push(config);
            }

            // 下面是清除緩存的過程
            if (lintResultCache) {
              // 得到緩存結(jié)果
                const cachedResult = lintResultCache.getCachedLintResults(filePath, config);

                if (cachedResult) {
                    const hadMessages =
                        cachedResult.messages &&
                        cachedResult.messages.length > 0;

                    if (hadMessages && fix) {
                        debug(`Reprocessing cached file to allow autofix: ${filePath}`);
                    } else {
                        debug(`Skipping file since it hasn't changed: ${filePath}`);
                        results.push(cachedResult);
                        continue;
                    }
                }
            }

            // 這里開始進行l(wèi)int操作, 這里去到verifyText里面, 打個記號0x010
            const result = verifyText({
                text: fs.readFileSync(filePath, "utf8"),
                filePath,
                config,
                cwd,
                fix,
                allowInlineConfig,
                reportUnusedDisableDirectives,
                fileEnumerator,
                linter
            });

            results.push(result);

            // 存儲緩存到lintResultCache對象中
            if (lintResultCache) {
                lintResultCache.setCachedLintResults(filePath, config, result);
            }
        }

        // 這個通過file-entry-cache這個包將緩存持久化到磁盤喜德。
        if (lintResultCache) {
            lintResultCache.reconcile();
        }

        debug(`Linting complete in: ${Date.now() - startTime}ms`);
        let usedDeprecatedRules;
                // 這里也是直接返回到代碼塊004
        return {
            results,
            ...calculateStatsPerRun(results),

            // 
            get usedDeprecatedRules() {
                if (!usedDeprecatedRules) {
                    usedDeprecatedRules = Array.from(
                        iterateRuleDeprecationWarnings(lastConfigArrays)
                    );
                }
                return usedDeprecatedRules;
            }
        };
    }

2.8 開始進入linter類的檢測和修復主流程

我們進入verifyText方法中, 就在第四層lib/cli-engine/cli-engine.js文件中 , 看代碼塊006

// 代碼塊006
function verifyText({
    text,
    cwd,
    filePath: providedFilePath,
    config,
    fix,
    allowInlineConfig,
    reportUnusedDisableDirectives,
    fileEnumerator,
    linter
}){
  // ...其他配置
  
  // 這里再次進入第五層lib/linter/linter.js, 打個記號0x009
  const { fixed, messages, output } = linter.verifyAndFix(
    text,
    config,
    {
        allowInlineConfig,
        filename: filePathToVerify,
        fix,
        reportUnusedDisableDirectives,
  
        /**
         * Check if the linter should adopt a given code block or not.
         * @param {string} blockFilename The virtual filename of a code block.
         * @returns {boolean} `true` if the linter should adopt the code block.
         */
        filterCodeBlock(blockFilename) {
            return fileEnumerator.isTargetPath(blockFilename);
        }
    }
  );
  // 這里返回代碼塊5, 記號0x010
  const result = {
        filePath,
        messages,
    // 這里計算并收集錯誤和警告數(shù), 這里檢測就不看了
        ...calculateStatsPerFile(messages)
    };
}

2.9 如何判斷檢測或者修復完成

const { fixed, messages, output } = linter.verifyAndFix()再次進入第五層lib/linter/linter.js, 這里的檢測和修復都是先直接執(zhí)行一變修復和檢測流程do...while處理 , 具體處理如下 , 我們只是檢測所以fixedResult.fixedshouldFix都是false , 這時候依然在代碼檢測中還沒有使用espree進行ast轉(zhuǎn)換 ; 看代碼塊007

// 代碼塊007
do {
  passNumber++;
  debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`);
  // 開始檢測并且拋出錯誤, 我們看看下面是如何檢測的, 打上記號0x007
  messages = this.verify(currentText, config, options);
  debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
  // 這里是修復+處理信息的代碼, 這里打個記號0x008,并且去到代碼塊011
  fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix);
  // 如果有任何語法錯誤都會停止山橄。
  if (messages.length === 1 && messages[0].fatal) {
      break;
  }
  fixed = fixed || fixedResult.fixed;
  currentText = fixedResult.output;
} while (
  fixedResult.fixed &&
  passNumber < MAX_AUTOFIX_PASSES
);
return fixedResult; // 來到這里我們就返回到代碼塊006, 記號0x009

verify方法如下 , 看代碼塊008

// 代碼塊008

// 根據(jù)第二個參數(shù)指定的規(guī)則驗證文本。
// textOrSourceCode要解析的文本或源代碼對象住诸。
// [config]配置一個ESLintConfig實例來配置一切驾胆。CLIEngine傳遞一個'ConfigArray'對象涣澡。
// [filenameOrptions]正在檢查的文件的可選文件名贱呐。
verify(textOrSourceCode, config, filenameOrOptions) {
  debug("Verify");
  const options = typeof filenameOrOptions === "string"
      ? { filename: filenameOrOptions }
      : filenameOrOptions || {};
  
  // 這里把配置提取出來
  if (config && typeof config.extractConfig === "function") {
      return this._verifyWithConfigArray(textOrSourceCode, config, options);
  }

 // 這里是將options的數(shù)據(jù)在進程中進行預處理, 但是最后的ast轉(zhuǎn)換還是在_verifyWithoutProcessors方法里面, 我們進入_verifyWithoutProcessors
  if (options.preprocess || options.postprocess) {
      return this._verifyWithProcessor(textOrSourceCode, config, options);
  }
  // 這里直接返回到代碼塊007, 記號0x007
  return this._verifyWithoutProcessors(textOrSourceCode, config, options);
}

3.0 代碼轉(zhuǎn)換成AST啦

這時候我們還是在第五層的lib/linter/linter.js文件中 , 繼續(xù)看_verifyWithoutProcessors這個方法, 這個方法后就已經(jīng)將fs讀取出來的文件轉(zhuǎn)換成ast了 , 看代碼塊009

// 代碼塊009
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
  // 獲取到自定義的配置和eslint的默認配置的插槽
        const slots = internalSlotsMap.get(this);
        const config = providedConfig || {};
        const options = normalizeVerifyOptions(providedOptions, config);
        let text;
//slots.lastSourceCode是記錄ast結(jié)構(gòu)的. 如果一開始textOrSourceCode是通過fs讀取處理的文件字符串則不進行處理
        if (typeof textOrSourceCode === "string") {
            slots.lastSourceCode = null;
            text = textOrSourceCode;
        } else {
            slots.lastSourceCode = textOrSourceCode;
            text = textOrSourceCode.text;
        }

        let parserName = DEFAULT_PARSER_NAME; // 這里默認解析器名字 espree
        let parser = espree; // 保存ast的espree編譯器

  // 這里是判斷是否我們的自定義配置是否有傳入解析器, 就是.eslintrc.*里面的parser選項, 如果有就進行替換
        if (typeof config.parser === "object" && config.parser !== null) {
            parserName = config.parser.filePath;
            parser = config.parser.definition;
        } else if (typeof config.parser === "string") {
            if (!slots.parserMap.has(config.parser)) {
                return [{
                    ruleId: null,
                    fatal: true,
                    severity: 2,
                    message: `Configured parser '${config.parser}' was not found.`,
                    line: 0,
                    column: 0
                }];
            }
            parserName = config.parser;
            parser = slots.parserMap.get(config.parser);
        }

        // 讀取文件中的eslint-env
        const envInFile = options.allowInlineConfig && !options.warnInlineConfig
            ? findEslintEnv(text)
            : {};
        const resolvedEnvConfig = Object.assign({ builtin: true }, config.env, envInFile);
        const enabledEnvs = Object.keys(resolvedEnvConfig)
            .filter(envName => resolvedEnvConfig[envName])
            .map(envName => getEnv(slots, envName))
            .filter(env => env);

        const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs);
        const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
        const settings = config.settings || {};

  // slots.lastSourceCode記錄ast結(jié)構(gòu), 如果沒有就繼續(xù)解析
        if (!slots.lastSourceCode) {
            const parseResult = parse(
                text,
                parser,
                parserOptions,
                options.filename
            );

            if (!parseResult.success) {
                return [parseResult.error];
            }

            slots.lastSourceCode = parseResult.sourceCode;
        } else {

            // 向后兼容處理
            if (!slots.lastSourceCode.scopeManager) {
                slots.lastSourceCode = new SourceCode({
                    text: slots.lastSourceCode.text,
                    ast: slots.lastSourceCode.ast,
                    parserServices: slots.lastSourceCode.parserServices,
                    visitorKeys: slots.lastSourceCode.visitorKeys,
                    scopeManager: analyzeScope(slots.lastSourceCode.ast, parserOptions)
                });
            }
        }

        const sourceCode = slots.lastSourceCode;
        const commentDirectives = options.allowInlineConfig
            ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
            : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };

        // augment global scope with declared global variables
        addDeclaredGlobals(
            sourceCode.scopeManager.scopes[0],
            configuredGlobals,
            { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
        );
    // 獲取所有的eslint規(guī)則
        const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);

  // 記錄檢測問題
        let lintingProblems;

  // 開始執(zhí)行規(guī)則檢測
        try {
          // 這個方法就是遍歷我們.eslintrc.*的rules規(guī)則, 這里打一個記號0x006
            lintingProblems = runRules(
                sourceCode,
                configuredRules,
                ruleId => getRule(slots, ruleId),
                parserOptions,
                parserName,
                settings,
                options.filename,
                options.disableFixes,
                slots.cwd,
                providedOptions.physicalFilename
            );
        } catch (err) {
            err.message += `\nOccurred while linting ${options.filename}`;
            debug("An error occurred while traversing");
            debug("Filename:", options.filename);
            if (err.currentNode) {
                const { line } = err.currentNode.loc.start;

                debug("Line:", line);
                err.message += `:${line}`;
            }
            debug("Parser Options:", parserOptions);
            debug("Parser Path:", parserName);
            debug("Settings:", settings);
            throw err;
        }

  // 最后返回檢測出來的所有問題
        return applyDisableDirectives({
            directives: commentDirectives.disableDirectives, // 這里是處理是否disable-line/disable-next-line了, 里面的邏輯也不具體看了, 就是處理problem數(shù)組的問題并且返回, 到這里我們繼續(xù)返回到代碼塊008
            problems: lintingProblems
                .concat(commentDirectives.problems)
                .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
            reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
        });
    }

3.1 eslint讀取插件規(guī)則

我們這次進入eslint是如何遍歷rules的 , 我們進入runRules方法 , 這會我們依然在第五層的lib/linter/linter.js , 看代碼塊010

// 代碼塊010

// 這個方法就是執(zhí)行ast對象和給定的規(guī)則是否匹配, 返回值是一個問題數(shù)組
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) {
  // 這里創(chuàng)建一個沒有this的事件監(jiān)聽器
    const emitter = createEmitter();
  // 用來記錄"program"節(jié)點下的所有ast節(jié)點
    const nodeQueue = [];
    let currentNode = sourceCode.ast;

  // 開始迭代ast節(jié)點, 并且經(jīng)過處理的節(jié)點, 那這里是怎么進行處理, 我們跳出去看看這里的代碼, 所以這里再次手動打個記號0x002,這里查看代碼塊010_1
    Traverser.traverse(sourceCode.ast, {
      // 進入traverse遞歸ast的起始需要做的事情, isEntering是判斷當前頂層節(jié)點是否為Program, 一個是否結(jié)束的標志
        enter(node, parent) {
            node.parent = parent;
            nodeQueue.push({ isEntering: true, node });
        },
      // 遞歸ast的完成需要做的事情
        leave(node) {
            nodeQueue.push({ isEntering: false, node });
        },
      // ast上需要遍歷的key名
        visitorKeys: sourceCode.visitorKeys
    });

    // 公共的屬性和方法進行凍結(jié), 避免合并的時候有性能的不必要的消耗
    const sharedTraversalContext = Object.freeze(
        Object.assign(
            Object.create(BASE_TRAVERSAL_CONTEXT),
            {
                getAncestors: () => getAncestors(currentNode),
                getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
                getCwd: () => cwd,
                getFilename: () => filename,
                getPhysicalFilename: () => physicalFilename || filename,
                getScope: () => getScope(sourceCode.scopeManager, currentNode),
                getSourceCode: () => sourceCode,
                markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
                parserOptions,
                parserPath: parserName,
                parserServices: sourceCode.parserServices,
                settings
            }
        )
    );

    // 經(jīng)過Traverser裝箱的ast節(jié)點后, 開始進行驗證
  // lintingProblems用來記錄問題列表
    const lintingProblems = [];
  // configuredRules自定義和eslint的默認規(guī)則
    Object.keys(configuredRules).forEach(ruleId => {
      // 獲取到每個規(guī)則后, 開始判斷是否起效就是"off", "warn", "error"三個參數(shù)的設置, 這里手動打記號0x003, 我們看代碼塊010_2
        const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);

        // 如果當前規(guī)則是0(off關閉的話就不進行檢測)
        if (severity === 0) {
            return;
        }
            // 這里的ruleMap是在 ./lib/rule下的以及文件下的所有js文件和第三方插件和自定義插件的文件, 就是我們一開始自定義的replaceXXX在代碼塊replaceXXX
      /*
      rule 結(jié)構(gòu)就是上面的文件
      {
        meta: {
          // ...
        },
        create: [Function: create]
      }
      */
        const rule = ruleMapper(ruleId);

      // 沒有就直接創(chuàng)建一個空的
        if (rule === null) {
            lintingProblems.push(createLintingProblem({ ruleId }));
            return;
        }

        const messageIds = rule.meta && rule.meta.messages;
        let reportTranslator = null;
      // 創(chuàng)建context上下文鉤子, 這里怎么理解呢, 就是自定義eslint文件的下create方法的參數(shù), 即上面代碼塊replaceXXX的create的context
        const ruleContext = Object.freeze(
            Object.assign(
                Object.create(sharedTraversalContext), // 這里獲取一下公共鉤子, 如果我們在自定義插件里面沒有使用就不會設置進去, 以保證性能
                {
                    id: ruleId,
                    options: getRuleOptions(configuredRules[ruleId]), // 這里獲取的是除了數(shù)組第一個元素到結(jié)尾即 ["error", "$1", "$2"]里面的 $1和$2
                    report(...args) {
                        // 在node 8.4以上才起效
                      // 創(chuàng)建一個報告器
                        if (reportTranslator === null) {
                          // 進入createReportTranslator文件這里打個記號0x004, 并且跳到下面代碼塊010_3
                            reportTranslator = createReportTranslator({
                                ruleId,
                                severity,
                                sourceCode,
                                messageIds,
                                disableFixes
                            });
                        }
                      // 根據(jù)上面記號0x004可以得到這個problem就是createReportTranslator的返回值, 其結(jié)構(gòu)為
                      /*
                      {
                          ruleId: options.ruleId,
                          severity: options.severity,
                          message: options.message,
                          line: options.loc.start.line,
                          column: options.loc.start.column + 1,
                          nodeType: options.node && options.node.type || null,
                          messageId?: options.messageId,
                          problem.endLine?: options.loc.end.line;
                                            problem.endColumn?: options.loc.end.column + 1;
                                            problem.fix?: options.fix;
                                            problem.suggestions?: options.suggestions;
                      }
                      */
                        const problem = reportTranslator(...args);

                        if (problem.fix && rule.meta && !rule.meta.fixable) {
                            throw new Error("Fixable rules should export a `meta.fixable` property.");
                        }
                      // 將處理好的問題存儲到lintingProblems中
                        lintingProblems.push(problem);
                    }
                }
            )
        );

      // 這里打個記號0x005,并一起來查看代碼塊010_4, 這里ruleListeners拿到的每個插件create返回值
        const ruleListeners = createRuleListeners(rule, ruleContext);

        // 這里將我們的所有規(guī)則通過事件發(fā)布系統(tǒng)發(fā)布出去
        Object.keys(ruleListeners).forEach(selector => {
            emitter.on(
                selector,
                timing.enabled
                    ? timing.time(ruleId, ruleListeners[selector])
                    : ruleListeners[selector]
            );
        });
    });

    // 我認為這段代碼是分析用的, 具體還有什么功能這里就不深究了, 不在eslint檢測的過程中
    const eventGenerator = nodeQueue[0].node.type === "Program"
        ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
        : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });

    nodeQueue.forEach(traversalInfo => {
        currentNode = traversalInfo.node;

        try {
            if (traversalInfo.isEntering) {
                eventGenerator.enterNode(currentNode);
            } else {
                eventGenerator.leaveNode(currentNode);
            }
        } catch (err) {
            err.currentNode = currentNode;
            throw err;
        }
    });
        // 好了看到這里的都是勇士了, 我寫到這里的時候已經(jīng)是第三天了, 滿腦子都是eslint了, 我們開始返回到代碼塊009, 記號0x006
    return lintingProblems;
}

3.2 eslint對AST進行遍歷并且轉(zhuǎn)換成特定的結(jié)構(gòu)

我們看看Traverser的做了什么, 該文件在lib/shared/traverser.js , 看代碼塊010_1

// 代碼塊010_1
// Traverser是一個遍歷AST樹的遍歷器類。使用遞歸的方式進行遍歷ast樹的
// 這里主要看它是如何遞歸的
class Traverser {
    _traverse(node, parent) {
        if (!isNode(node)) {
            return;
        }
        this._current = node;
    // 重置是否跳過就是那些需要disable的文件
        this._skipped = false;
    // 這里會是傳入的cb, 一般都是處理_skipped和節(jié)點信息
        this._enter(node, parent);

        if (!this._skipped && !this._broken) {
          // 這里的keys是確認eslint的ast需要遞歸什么key值, 這里是eslint的第三方包eslint-visitor-keys, eslint會通過這里面的key名進行遍歷打包成eslint本身需要的數(shù)據(jù)結(jié)構(gòu)
            const keys = getVisitorKeys(this._visitorKeys, node);

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

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

        if (!this._broken) {
          // 當遍歷完成, 會給出一個鉤子進行一些還原的操作
            this._leave(node, parent);
        }

        this._current = parent;
    }
}

看完eslint是如何遞歸處理espree解析出來的ast后 , 我們再滑看會上面的記號0x002, 在代碼塊010中

查看代碼塊010_2
// 代碼塊010_2
// 我們來看看這一句代碼做了什么const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// 首先這段代碼是用來匹配0, 1, 2, "off", "warn", "error"這留個變量的
// configuredRules 自定義+eslint的rules規(guī)則配置, ruleId是對應的key名
// ConfigOps是重點, 這里進入ConfigOps的文件里面在@eslint/eslintrc包里面的lib/shared/config-ops.js, 并不是在eslint包里面哦
const RULE_SEVERITY_STRINGS = ["off", "warn", "error"],
    RULE_SEVERITY = RULE_SEVERITY_STRINGS.reduce((map, value, index) => {
        map[value] = index;
        return map;
    }, {}),
    VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"];
// RULE_SEVERITY定義為{ off: 0, warn: 1, error: 2 }

// 主要是以下方法處理我們rules的0, 1, 2, "off", "warn", "error", 這里如果傳入非法值, 就默認返回0, 而"off", "warn", "error"是不區(qū)分大小寫的
    getRuleSeverity(ruleConfig) {
        const severityValue = Array.isArray(ruleConfig) ? ruleConfig[0] : ruleConfig;

        if (severityValue === 0 || severityValue === 1 || severityValue === 2) {
            return severityValue;
        }

        if (typeof severityValue === "string") {
            return RULE_SEVERITY[severityValue.toLowerCase()] || 0;
        }

        return 0;
    }

至此severity會根據(jù)"off", "warn", "error"得到(0|1|2) , 然后我們再返回代碼塊010中的記號0x003中

3. 以下的代碼塊作為附錄說明 , 會跳來跳去 , 請根據(jù)下面提示讀取下面的代碼塊 , 不然會很暈

查看代碼塊010_3
// 代碼塊010_3
// reportTranslator = createReportTranslator({}) 方法在/lib/linter/report-translator.js文件里面
function normalizeMultiArgReportCall(...args) {
   /*
   接收的參數(shù)因為是經(jīng)過解構(gòu)的所以就會變成
   [
      {
        abc: true,
        node: {
          type: 'Literal',
          value: 'XXX',
          raw: "'XXX'",
          range: [Array],
          loc: [Object],
          parent: [Object]
        },
        messageId: 'unexpected',
        data: { argv: 'Candice1' },
        fix: [Function: fix]
      }
    ] 
    意味著context.report是可以接受一個數(shù)組或者對象的
   */
    if (args.length === 1) {
        return Object.assign({}, args[0]);
    }

    
    if (typeof args[1] === "string") {
        return {
            node: args[0],
            message: args[1],
            data: args[2],
            fix: args[3]
        };
    }

    // Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
    return {
        node: args[0],
        loc: args[1],
        message: args[2],
        data: args[3],
        fix: args[4]
    };
}
module.exports = function createReportTranslator(metadata) {

    /*
     createReportTranslator`在每個文件中為每個啟用的規(guī)則調(diào)用一次入桂。它需要非常有表現(xiàn)力奄薇。
*報表轉(zhuǎn)換器本身(即`createReportTranslator`返回的函數(shù))獲取
*每次規(guī)則報告問題時調(diào)用,該問題發(fā)生的頻率要低得多(通常是
*大多數(shù)規(guī)則不會報告給定文件的任何問題)抗愁。
     */
    return (...args) => {
        const descriptor = normalizeMultiArgReportCall(...args);
        const messages = metadata.messageIds;

      // 斷言descriptor.node是否是一個合法的report節(jié)點, 合法的report節(jié)點看上面normalizeMultiArgReportCall方法
        assertValidNodeInfo(descriptor);

        let computedMessage;

        if (descriptor.messageId) {
            if (!messages) {
                throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata.");
            }
            const id = descriptor.messageId;

            if (descriptor.message) {
                throw new TypeError("context.report() called with a message and a messageId. Please only pass one.");
            }
          // 這里要注意creat下context.report({messageId: 'unexpected', // 對應meta.messages.XXX,message可以直接用message替換})和meta.messages = {unexpected: '錯誤的字符串XXX, 需要用{{argv}}替換'}, 里面的key名要對應
            if (!messages || !Object.prototype.hasOwnProperty.call(messages, id)) {
                throw new TypeError(`context.report() called with a messageId of '${id}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`);
            }
            computedMessage = messages[id];
        } else if (descriptor.message) {
            computedMessage = descriptor.message;
        } else {
            throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem.");
        }
             // 斷言desc和fix參數(shù)
        validateSuggestions(descriptor.suggest, messages);

      // 接下來就是處理好所有的規(guī)則后, 開始創(chuàng)建最后的問題了, 看下面的createProblem
        return createProblem({
            ruleId: metadata.ruleId,
            severity: metadata.severity,
            node: descriptor.node,
            message: interpolate(computedMessage, descriptor.data),
            messageId: descriptor.messageId,
            loc: normalizeReportLoc(descriptor),
            fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode), // 跟修復相關代碼
            suggestions: metadata.disableFixes ? [] : mapSuggestions(descriptor, metadata.sourceCode, messages)
        });
    };
};
// 創(chuàng)建有關報告的信息
function createProblem(options) {
    const problem = {
        ruleId: options.ruleId,
        severity: options.severity,
        message: options.message,
        line: options.loc.start.line,
        column: options.loc.start.column + 1,
        nodeType: options.node && options.node.type || null
    };

     // 如果這不在條件中馁蒂,則某些測試將失敗因為問題對象中存在“messageId”
    if (options.messageId) {
        problem.messageId = options.messageId;
    }

    if (options.loc.end) {
        problem.endLine = options.loc.end.line;
        problem.endColumn = options.loc.end.column + 1;
    }
// 跟修復相關
    if (options.fix) {
        problem.fix = options.fix;
    }

    if (options.suggestions && options.suggestions.length > 0) {
        problem.suggestions = options.suggestions;
    }

    return problem;
}

接下來我們返回記號0x004

代碼塊010_4, 記錄了eslint如何運行rules中插件的create方法的
function createRuleListeners(rule, ruleContext) {
    try {
      // 每次最后還是直接返回我們自定義返回的對象, 比如我們代碼塊replaceXXX的return, 具體看上面代碼塊replaceXXX
        return rule.create(ruleContext);
    } catch (ex) {
        ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`;
        throw ex;
    }
}

接下來我們返回記號0x005

代碼塊011, 因為本次是檢測不涉及fix過程
SourceCodeFixer.applyFixes = function(sourceText, messages, shouldFix) {
    debug("Applying fixes");
        // 所以這里就知道返回了
    if (shouldFix === false) {
        debug("shouldFix parameter was false, not attempting fixes");
        return {
            fixed: false,
            messages,
            output: sourceText
        };
    }
//...其他代碼
};

我們繼續(xù)返回代碼塊007

代碼塊012

async function printResults(engine, results, format, outputFile) {
    let formatter;

    try {
        formatter = await engine.loadFormatter(format);
    } catch (e) {
        log.error(e.message);
        return false;
    }
// 格式化輸出
    const output = formatter.format(results);

    if (output) {
        if (outputFile) {
            const filePath = path.resolve(process.cwd(), outputFile);

            if (await isDirectory(filePath)) {
                log.error("Cannot write to output file path, it is a directory: %s", outputFile);
                return false;
            }

            try {
                await mkdir(path.dirname(filePath), { recursive: true });
                await writeFile(filePath, output);
            } catch (ex) {
                log.error("There was a problem writing the output file:\n%s", ex);
                return false;
            }
        } else {
          // 這里里面就是用最樸素的打印方式 console.log()進行打印輸出
            log.info(output);
        }
    }

    return true;
}

3. 總結(jié)

文件流程如下

bin/eslint.js -> lib/cli.js -> lib/eslint/eslint.js>lintText() -> lib/cli-engine/cli-engine.js>executeOnFiles() -> lib/cli-engine.js/cli-engine.js>verifyText() -> lib/linter/linter.js>verify()>_verifyWithoutProcessors()>runRules()

文件路徑 文件說明
bin/eslint.js 入口文件, 主要是在這里進入具體的主要執(zhí)行文件 , 并且讀取命令行的參數(shù)
lib/cli.js 判斷的傳入的參數(shù), 并且格式化所需要的參數(shù), 創(chuàng)建eslint的編譯器實例 , 在獲取完所有的問題列表后會進行console打印到命令行 , 這里最后執(zhí)行完后是返回對應rocess.exitCode的參數(shù)
lib/eslint/eslint.js eslint編譯器實例文件 , 這里會簡單的判斷插件和寫入的eslintrc文件是否合法 , 還會對文件的檢測和文本的檢測的結(jié)果進行報告 , 進入真正的腳本執(zhí)行文件
lib/cli-engine/cli-engine.js 這個文件中會傳進一個包含附加工具, 忽略文件, 緩存文件, 配置文件, 配置文件規(guī)則以及檢測器的一個map插槽
lib/linter/linter.js 檢測器文件 , 這里進行ast轉(zhuǎn)換和rule檢測 , 已經(jīng)插件的讀取 , 最后把檢測后的問題返回

eslint中最主要的三個類 , 分別是ESLint和CLIEngine和Linter; 由這三個類分工合作對傳入的代碼已經(jīng)插件進行匹配檢測 , 當然在eslint還有一些分析和緩存方面 , 在這里也會帶過一點 , 還有一個就是eslint寫了一個無this的事件發(fā)布系統(tǒng) , 因為eslint里面拆分出來太多類了 , 每個類的新建都有可能改變當前調(diào)用this , 所以這個eslint的事件發(fā)布系統(tǒng)是無this且freeze安全的 ; 在修復方面 , eslint會根據(jù)ast讀取到的位置進行替換 ;

在使用插件方面 , 用eslint的生成器生成出來的 , 統(tǒng)一使用eslint-plugin-**這個格

4. 參考

  1. eslint官方文檔
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蜘腌,隨后出現(xiàn)的幾起案子沫屡,更是在濱河造成了極大的恐慌,老刑警劉巖撮珠,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沮脖,死亡現(xiàn)場離奇詭異,居然都是意外死亡芯急,警方通過查閱死者的電腦和手機勺届,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娶耍,“玉大人免姿,你說我怎么就攤上這事¢啪疲” “怎么了胚膊?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長想鹰。 經(jīng)常有香客問我紊婉,道長,這世上最難降的妖魔是什么杖挣? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任肩榕,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘株汉。我一直安慰自己筐乳,他們只是感情好,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布乔妈。 她就那樣靜靜地躺著蝙云,像睡著了一般。 火紅的嫁衣襯著肌膚如雪路召。 梳的紋絲不亂的頭發(fā)上勃刨,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音股淡,去河邊找鬼身隐。 笑死,一個胖子當著我的面吹牛唯灵,可吹牛的內(nèi)容都是我干的贾铝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼埠帕,長吁一口氣:“原來是場噩夢啊……” “哼垢揩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起敛瓷,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叁巨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呐籽,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锋勺,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年绝淡,在試婚紗的時候發(fā)現(xiàn)自己被綠了宙刘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡牢酵,死狀恐怖悬包,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情馍乙,我是刑警寧澤布近,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站丝格,受9級特大地震影響撑瞧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜显蝌,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一预伺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦酬诀、人聲如沸脏嚷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽父叙。三九已至,卻和暖如春肴裙,著一層夾襖步出監(jiān)牢的瞬間趾唱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工蜻懦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留甜癞,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓阻肩,卻偏偏與公主長得像带欢,于是被迫代替她去往敵國和親运授。 傳聞我的和親對象是個殘疾皇子烤惊,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容