ESLint检测部分源码解读

写在前面

以下是我阅读eslint源码的过程 , 在这过程中 , 我首先会自己写一个eslint插件的demo , 然后自己定义一个规则 , 然后再进行检测 , 根据调用栈迅速的一步一步看下去 , 大致知道是怎么样的流程后 ; 接着再重新拆分每一步是怎么做的 , 分析规则和插件的运用 , 从而更加巩固自己对于eslint插件的开发 ; 基于这个想法 , 我们就开始吧

在大致流程中会交代eslint的修复过程 , 但是也是大致的说明一下 ; 详细拆分的过程是没有分析修复过程的

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

第一节 . 大致流程

1. 找到eslint命令入口文件

打开源码 , 我们通过package.json查看eslint的命令入口 , 在bin下的eslint.js

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

2. 进入./bin/eslint.js

"use strict";
require("v8-compile-cache");
// 读取命令中 --debug参数, 并输出代码检测的debug信息和每个插件的耗时
if (process.argv.includes("--debug")) {
    require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
}

// 这里省略了readStdin getErrorMessage onFatalError 三个方法

// 主要看下面IIFE , 而且这个是用了一个promise包裹 , 并且有捕捉函数的一个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才是执行eslint开始的地方
    process.exitCode = await require("../lib/cli").execute(
        process.argv,
        process.argv.includes("--stdin") ? await readStdin() : null
    );
}()).catch(onFatalError);

2.1 lib/cli执行脚本文件

// 其他代码
const cli = {
  // args 就是那些 --cache --debug等参数
    async execute(args, text) {
        /** @type {ParsedCLIOptions} */
      
      // 开始对参数格式化
        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": 指的是该规则识别的代码要么会导致错误,要么可能会导致令人困惑的行为。开发人员应该优先考虑解决这个问题。
    docs: {
      description: 'XXX 不能出现在代码中!',
      category: 'Possible Errors', // eslint规则首页的分类: Possible Errors、Best Practices、Strict Mode、Varibles、Stylistic Issues、ECMAScript 6、Deprecated、Removed
      recommended: false, // "extends": "eslint:recommended"属性是否启用该规则
      url: '', // 指定可以访问完整文档的URL
    },
    fixable: 'code', // 该规则是否可以修复
    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: {
              // 占位数据
              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字符串转换成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就是执行了插件文件里面create方法如下

create: function (context) {
  // 获取目标项目中.eslintrc.json文件下的rules的第二个参数
  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, // 记录修复次数, 这里会和最大修复次数10次比较, 大于10次或者有修复完成的标志即可停止修复
            currentText = text; // 修复前的源码字符串
        const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`;
        const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true;

        // 每个问题循环修复10次以上或者已经修复完毕fixedResult.fixed, 即可判定为修复完成
        do {
            passNumber++;

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

            debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`);
            // 执行修复代码
            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的结构为
        {
        ruleId: 'eslint-demo/eslint-demo', // 插件名称
        severity: 2,
        message: '错误的字符串XXX, 需要用MMM替换', // 提示语
        line: 17, // 行数
        column: 18, // 列数
        nodeType: 'Literal', // 当前节点在AST中是什么类型
        messageId: 'unexpected', 对应meta.messages.XXX,message可以直接用message替换
        endLine: 17, // 结尾的行数
        endColumn: 23, // 结尾的列数
        fix: { range: [ 377, 382 ], text: "'MMM'" } // 该字符串在整个文件字符串中的位置
      }
    */
    function attemptFix(problem) {
        const fix = problem.fix;
        const start = fix.range[0]; // 记录修复的起始位置
        const end = fix.range[1]; // 记录修复的结束位置

        // 如果重叠或为负范围,则将其视为问题
        if (lastPos >= start || start > end) {
            remainingMessages.push(problem);
            return false;
        }

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

        // 拼接修复后的结果, 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方法唯一失败的一次是与之前修复的发生冲突, 这里默认将已经修复好的标志设置为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. 项目代码准备

首先我们准备我们需要检测的工程结构如下

├── 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插件如何替换规则*/
  }
}

1.3 eslint自定义插件

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

2. 代码流程

当我们输入 npx eslint ./src/*.tsx的时候做了什么呢

2.1 第一层bin/eslint.js

入口文件 在 ./bin/eslint.js , 在这个文件中通过一个匿名自执行promise函数 , 引入了 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是否执行完毕 , 传入的参数是我们npx eslint --fix ./src 的fix这个参数 , 以及跟在--stdin后面的参数 ; 然后我们进入lib/cli , 因为上面直接调用了execute方法 , 那么我们就看看cli里面execute方法 , 首先定义了一个options参数 , 然后调用了CLIOptions.parse(args)方法 (查看代码块003), 这个方法是其他包optionator里面的方法 , 我们进入进去就可以看到parse的方法了 , 这个方法就是switch case将不同的参数处理装箱打包进行返回 , 这里面还用了一个.ls包进行map管理 , 且在保证用户输入的时候用了type-check这个包进行输入和测试进行管理 , 在非ts环境下 , 进行类似Haskell的类型语法检查 ; 好这时候我们拿到了经过装箱打包的options了 , 这个key名不是我起的 , 它源码就这样(好随便啊) ; 得到了如下结构, (看代码块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' ]
} 

得到这个结构后 , 就通过转换translateOptions函数进行配置的转换 , 这里我猜是因为一些人接手别人的代码 , 需要写的一个转换文件 ; 接着开始创建我们的一个eslint的编译器 const engine = new ESLint(translateOptions(options));

2.3 进入第三层lib/eslint/eslint.js

在lib/eslint/eslint.js里面的Eslint类 , Eslint这个类的构造函数首先会将所有的配置进行检验 , 在ESLint类里面会创建一个cli的编译器 ,

2.4 进入第四层lib/cli-engine/cli-engine.js

这个编译器在lib/cli-engine/cli-engine.js里面 , 这里主要是处理一下缓存以及eslint内部的默认规则 ; 然后回来lib/eslint/eslint.js里面的Eslint类 , 接下来就是获取从cli-engine.js的内部插槽 , 设置私有的内存快照 , 判断是否更新 ,如果更新就删除缓存 ;

2.5 进入第五层lib/linter/linter.js

这一层就比较简单了 , 就是用了map结构记录了cwd , lastConfigArray , lastSourceCode , parserMap , ruleMap , 分别是当前文件路径 , 最新的配置数据 , 最新的源码使用编译器espree解析出来的ast源码字符串 , 编译器(记录我们用的是什么编译器默认是espree) , 以及规则map

2.6 返回到第二层

接着返回到第二层继续走下去 , 因为不是使用--stdin , 所以直接看else , 执行了engine.lintFiles

// 代码块003
const cli = {
  // 其他方法
  async execute() {
    let options;
    try {
      options = CLIOptions.parse(args);
    } catch (error) {
      log.error(error.message);
      return 2;
    }
    // 其他验证参数的代码
    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类创建出来的所以我们去到第三层的lib/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构造函数的时候已经将cliEngine, set进去了, 所以这里直接获取即可
  const { cliEngine } = privateMembersMap.get(this);
    // processCLIEngineLintReport是返回linting指定的文件模式, 传入的参数是cliEngine, 并且第二个参数执行了executeOnFiles, 我们看看cliEngine这个类的executeOnFiles做了什么
  return processCLIEngineLintReport(
    cliEngine,
    cliEngine.executeOnFiles(patterns)
  );
}

2.7 再次进入第四层

再次进入第四层的CLIEngine类下的executeOnFiles方法, 从他接受的参数和方法名可以知道, 这个executeOnFiles主要是处理文件和文件组的问题 , 看代码块005

// 代码块005
executeOnFiles(patterns) {
  const results = [];
  // 这里是一个很迷的操作, 官方在这里手动把所有的最新配置都清除了, 这个是从外部传进来的, 但是它先手动清除然后下面再在迭代器里面每个都引用一遍, 
   lastConfigArrays.length = 0;
  //... 其他函数
  // 清除上次使用的配置数组。
  // 清除缓存文件, 使用fs.unlinkSync进行缓存文件的请求, 当不存在此类文件或文件系统为只读(且缓存文件不存在)时忽略错误
  // 迭代源文件并且放到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) {
              // 得到缓存结果
                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;
                    }
                }
            }

            // 这里开始进行lint操作, 这里去到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,
    // 这里计算并收集错误和警告数, 这里检测就不看了
        ...calculateStatsPerFile(messages)
    };
}

2.9 如何判断检测或者修复完成

const { fixed, messages, output } = linter.verifyAndFix()再次进入第五层lib/linter/linter.js, 这里的检测和修复都是先直接执行一变修复和检测流程do...while处理 , 具体处理如下 , 我们只是检测所以fixedResult.fixedshouldFix都是false , 这时候依然在代码检测中还没有使用espree进行ast转换 ; 看代码块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

// 根据第二个参数指定的规则验证文本。
// 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的数据在进程中进行预处理, 但是最后的ast转换还是在_verifyWithoutProcessors方法里面, 我们进入_verifyWithoutProcessors
  if (options.preprocess || options.postprocess) {
      return this._verifyWithProcessor(textOrSourceCode, config, options);
  }
  // 这里直接返回到代码块007, 记号0x007
  return this._verifyWithoutProcessors(textOrSourceCode, config, options);
}

3.0 代码转换成AST啦

这时候我们还是在第五层的lib/linter/linter.js文件中 , 继续看_verifyWithoutProcessors这个方法, 这个方法后就已经将fs读取出来的文件转换成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结构的. 如果一开始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结构, 如果没有就继续解析
        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规则
        const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);

  // 记录检测问题
        let lintingProblems;

  // 开始执行规则检测
        try {
          // 这个方法就是遍历我们.eslintrc.*的rules规则, 这里打一个记号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数组的问题并且返回, 到这里我们继续返回到代码块008
            problems: lintingProblems
                .concat(commentDirectives.problems)
                .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
            reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
        });
    }

3.1 eslint读取插件规则

我们这次进入eslint是如何遍历rules的 , 我们进入runRules方法 , 这会我们依然在第五层的lib/linter/linter.js , 看代码块010

// 代码块010

// 这个方法就是执行ast对象和给定的规则是否匹配, 返回值是一个问题数组
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) {
  // 这里创建一个没有this的事件监听器
    const emitter = createEmitter();
  // 用来记录"program"节点下的所有ast节点
    const nodeQueue = [];
    let currentNode = sourceCode.ast;

  // 开始迭代ast节点, 并且经过处理的节点, 那这里是怎么进行处理, 我们跳出去看看这里的代码, 所以这里再次手动打个记号0x002,这里查看代码块010_1
    Traverser.traverse(sourceCode.ast, {
      // 进入traverse递归ast的起始需要做的事情, isEntering是判断当前顶层节点是否为Program, 一个是否结束的标志
        enter(node, parent) {
            node.parent = parent;
            nodeQueue.push({ isEntering: true, node });
        },
      // 递归ast的完成需要做的事情
        leave(node) {
            nodeQueue.push({ isEntering: false, node });
        },
      // ast上需要遍历的key名
        visitorKeys: sourceCode.visitorKeys
    });

    // 公共的属性和方法进行冻结, 避免合并的时候有性能的不必要的消耗
    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
            }
        )
    );

    // 经过Traverser装箱的ast节点后, 开始进行验证
  // lintingProblems用来记录问题列表
    const lintingProblems = [];
  // configuredRules自定义和eslint的默认规则
    Object.keys(configuredRules).forEach(ruleId => {
      // 获取到每个规则后, 开始判断是否起效就是"off", "warn", "error"三个参数的设置, 这里手动打记号0x003, 我们看代码块010_2
        const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);

        // 如果当前规则是0(off关闭的话就不进行检测)
        if (severity === 0) {
            return;
        }
            // 这里的ruleMap是在 ./lib/rule下的以及文件下的所有js文件和第三方插件和自定义插件的文件, 就是我们一开始自定义的replaceXXX在代码块replaceXXX
      /*
      rule 结构就是上面的文件
      {
        meta: {
          // ...
        },
        create: [Function: create]
      }
      */
        const rule = ruleMapper(ruleId);

      // 没有就直接创建一个空的
        if (rule === null) {
            lintingProblems.push(createLintingProblem({ ruleId }));
            return;
        }

        const messageIds = rule.meta && rule.meta.messages;
        let reportTranslator = null;
      // 创建context上下文钩子, 这里怎么理解呢, 就是自定义eslint文件的下create方法的参数, 即上面代码块replaceXXX的create的context
        const ruleContext = Object.freeze(
            Object.assign(
                Object.create(sharedTraversalContext), // 这里获取一下公共钩子, 如果我们在自定义插件里面没有使用就不会设置进去, 以保证性能
                {
                    id: ruleId,
                    options: getRuleOptions(configuredRules[ruleId]), // 这里获取的是除了数组第一个元素到结尾即 ["error", "$1", "$2"]里面的 $1和$2
                    report(...args) {
                        // 在node 8.4以上才起效
                      // 创建一个报告器
                        if (reportTranslator === null) {
                          // 进入createReportTranslator文件这里打个记号0x004, 并且跳到下面代码块010_3
                            reportTranslator = createReportTranslator({
                                ruleId,
                                severity,
                                sourceCode,
                                messageIds,
                                disableFixes
                            });
                        }
                      // 根据上面记号0x004可以得到这个problem就是createReportTranslator的返回值, 其结构为
                      /*
                      {
                          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);

        // 这里将我们的所有规则通过事件发布系统发布出去
        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;
        }
    });
        // 好了看到这里的都是勇士了, 我写到这里的时候已经是第三天了, 满脑子都是eslint了, 我们开始返回到代码块009, 记号0x006
    return lintingProblems;
}

3.2 eslint对AST进行遍历并且转换成特定的结构

我们看看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和节点信息
        this._enter(node, parent);

        if (!this._skipped && !this._broken) {
          // 这里的keys是确认eslint的ast需要递归什么key值, 这里是eslint的第三方包eslint-visitor-keys, eslint会通过这里面的key名进行遍历打包成eslint本身需要的数据结构
            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规则配置, 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"是不区分大小写的
    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会根据"off", "warn", "error"得到(0|1|2) , 然后我们再返回代码块010中的记号0x003中

3. 以下的代码块作为附录说明 , 会跳来跳去 , 请根据下面提示读取下面的代码块 , 不然会很晕

查看代码块010_3
// 代码块010_3
// reportTranslator = createReportTranslator({}) 方法在/lib/linter/report-translator.js文件里面
function normalizeMultiArgReportCall(...args) {
   /*
   接收的参数因为是经过解构的所以就会变成
   [
      {
        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是可以接受一个数组或者对象的
   */
    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`在每个文件中为每个启用的规则调用一次。它需要非常有表现力。
*报表转换器本身(即`createReportTranslator`返回的函数)获取
*每次规则报告问题时调用,该问题发生的频率要低得多(通常是
*大多数规则不会报告给定文件的任何问题)。
     */
    return (...args) => {
        const descriptor = normalizeMultiArgReportCall(...args);
        const messages = metadata.messageIds;

      // 断言descriptor.node是否是一个合法的report节点, 合法的report节点看上面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参数
        validateSuggestions(descriptor.suggest, messages);

      // 接下来就是处理好所有的规则后, 开始创建最后的问题了, 看下面的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)
        });
    };
};
// 创建有关报告的信息
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
        };
    }
//...其他代码
};

我们继续返回代码块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. 总结

文件流程如下

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 入口文件, 主要是在这里进入具体的主要执行文件 , 并且读取命令行的参数
lib/cli.js 判断的传入的参数, 并且格式化所需要的参数, 创建eslint的编译器实例 , 在获取完所有的问题列表后会进行console打印到命令行 , 这里最后执行完后是返回对应rocess.exitCode的参数
lib/eslint/eslint.js eslint编译器实例文件 , 这里会简单的判断插件和写入的eslintrc文件是否合法 , 还会对文件的检测和文本的检测的结果进行报告 , 进入真正的脚本执行文件
lib/cli-engine/cli-engine.js 这个文件中会传进一个包含附加工具, 忽略文件, 缓存文件, 配置文件, 配置文件规则以及检测器的一个map插槽
lib/linter/linter.js 检测器文件 , 这里进行ast转换和rule检测 , 已经插件的读取 , 最后把检测后的问题返回

eslint中最主要的三个类 , 分别是ESLint和CLIEngine和Linter; 由这三个类分工合作对传入的代码已经插件进行匹配检测 , 当然在eslint还有一些分析和缓存方面 , 在这里也会带过一点 , 还有一个就是eslint写了一个无this的事件发布系统 , 因为eslint里面拆分出来太多类了 , 每个类的新建都有可能改变当前调用this , 所以这个eslint的事件发布系统是无this且freeze安全的 ; 在修复方面 , eslint会根据ast读取到的位置进行替换 ;

在使用插件方面 , 用eslint的生成器生成出来的 , 统一使用eslint-plugin-**这个格

4. 参考

  1. eslint官方文档
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容