ESLint源码分析(1) options

获取到给定路径的所有父路径

例如给定一个路径D:/dir1/dir2/dir3/dir4/dir5/index.js,然后返回下列内容:

D:/dir1/dir2/dir3/dir4/dir5
D:/dir1/dir2/dir3/dir4
D:/dir1/dir2/dir3
D:/dir1/dir2
D:/dir1
D:/
复制代码

在ESLint中实现方法是:cascading-config-array-factory.js#L397

    _loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
        // 代码省略……
        const parentPath = path.dirname(directoryPath);
        const parentConfigArray = parentPath && parentPath !== directoryPath
            ? this._loadConfigInAncestors(
                parentPath,
                configsExistInSubdirs || configArray.length > 0
            )
            : baseConfigArray;
        // 代码省略……
    }
复制代码

利用path.dirname方法,递归调用_loadConfigInAncestors直到directoryPathD:/(将D:/传递给path.dirname返回的结果依然是D:/,此时parentPath等于directoryPath则递归终止)

获取配置文件

因为在eslint里,我们可以配置多个配置文件,且各个配置文件相互之间有覆盖的关系,例如我们执行eslint D:/dir1/dir2/dir3/dir4/dir5/index.js命令,eslint会针对D:/dir1/dir2/dir3/dir4/dir5/index.js路径逐层查询是否有配置文件(.eslintrc.js.eslintrc.cjs.eslintrc.yaml.eslintrc.yml.eslintrc.json.eslintrcpackage.json),而且配置文件还可以通过extends属性引入其它规则文件,那么eslint是如何根据被扫描文件获取到全部的配置文件以及内容的呢?

上一节我们介绍了如何获取到指定路径的所有父路径,所以我们只需要在每个父路径里判断配置文件是否存在即可获取到对被扫描文件产生影响的配置文件,eslint中在 loadInDirectory 实现了这个逻辑:

const configFilenames = [
    ".eslintrc.js",
    ".eslintrc.cjs",
    ".eslintrc.yaml",
    ".eslintrc.yml",
    ".eslintrc.json",
    ".eslintrc",
    "package.json"
];

{
    loadInDirectory(directoryPath, { basePath, name } = {}) {
        const slots = internalSlotsMap.get(this);
        for (const filename of configFilenames) {
            const ctx = createContext(
                slots,
                "config",
                name,
                path.join(directoryPath, filename),
                basePath
            );
            if (fs.existsSync(ctx.filePath)) {
                let configData;
                try {
                    configData = loadConfigFile(ctx.filePath);
                } catch (error) {
                    if (!error || error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND") {
                        throw error;
                    }
                }
                if (configData) {
                    debug(`Config file found: ${ctx.filePath}`);
                    return new ConfigArray(
                        ...this._normalizeConfigData(configData, ctx)
                    );
                }
            }
        }
        debug(`Config file not found on ${directoryPath}`);
        return new ConfigArray();
    }
}
复制代码

fs.existsSync(ctx.filePath)判断配置文件是否存在,存在的话调用 loadConfigFile 方法拿到配置文件的内容configData,然后调用_normalizeConfigData -> _normalizeObjectConfigData -> _normalizeObjectConfigDataBody -> _loadExtends -> _loadExtendedShareableConfig -> _loadConfigData -> _normalizeConfigData -> _normalizeObjectConfigData方法,可见这是一个很复杂的递归调用

所以,当执行eslint D:/dir1/dir2/dir3/dir4/dir5/index.js命令时,涉及到获取配置文件的完整调用链条是:

  1. 执行node_modules/eslint/.bin/eslint.js文件
  2. 执行node_modules/eslint/lib/cli.js文件中的 execute 函数,在此函数中调用了 const engine = new ESLint(translateOptions(options));,接着调用 engine.lintFiles(files)
  3. 执行node_modules/eslint/lib/eslint/eslint.js文件中的 lintFiles 函数,
  4. 执行node_modules/eslint/lib/cli-engine/cli-engine.js文件中的 executeOnFiles 方法
  5. 执行node_modules/eslint/lib/cli-engine/file-enumerator.js文件中的iterateFiles方法,然后依次调用_iterateFiles -> _iterateFilesWithFile -> getConfigArrayForFile
  6. 执行node_modules/@eslint/eslintrc/lib/cascading-config-array-factory.js文件的 getConfigArrayForFile 函数,在该函数内又调用了 _loadConfigInAncestors -> loadInDirectory
  7. 执行node_modules/@eslint/eslintrc/lib/config-array-factory.js文件的 loadInDirectory函数,在该函数内依次(递归)调用:
    1. _normalizeConfigData
    2. _normalizeObjectConfigData
    3. _normalizeObjectConfigDataBody
    4. _loadExtends
    5. _loadExtendedShareableConfig
    6. _loadConfigData
    7. _normalizeConfigData
    8. _normalizeObjectConfigData

配置文件内容转换

CLIEngine接口接收的参数里面,有些和ESLint配置文件(.eslintrc.js.eslintrc.cjs.eslintrc.yaml.eslintrc.yml.eslintrc.json.eslintrcpackage.json)里的属性是不一样的,例如接收的参数里 globals 字段是个数组,而配置文件里的 globals 是个对象,于是数据的转换就成了必然的,ESLint里是如何处理这一点的呢?

实际上通过阅读代码我们可以知道,从node_modules/eslint/lib/cli.jsengine.lintFiles(files) 根据一个被扫描的文件路径fileslintFiles函数就已经可以返回lint结果了,根据追进,我们可以发现在 调用verifyText函数 的地方config就是原始的配置文件内容(按照引用顺序存在一个数组里),对globals字段未做任何修改,继续深入 verifyText 发现调用了linter.verifyAndFix,追查 linter.verifyAndFix,发现其最终调用了 _verifyWithoutProcessors,并且在其中调用了resolveGlobals方法,得到了一个更加全面的全局变量对象

遗憾的是,我们从命令行的方向并没有找到利用命令行到底是在哪里将做了转换,似乎我们一开始的方向是有错误的,可能并不是命令行向API调用的方式转化,而是反过来

被扫描的文件:

// test22.js
console.log(wqwq);
复制代码

我们编写代码做测试:

// lint.js
const CLIEngine = require('eslint').CLIEngine;

const cli = new CLIEngine({
    globals: ['okok']
});
const result = cli.executeOnFiles('../test/test22.js');

console.log(result.results[0].messages);
复制代码

打印的结果是:

[
  {
    ruleId: 'no-console',
    severity: 2,
    message: 'Unexpected console statement.',
    line: 1,
    column: 1,
    nodeType: 'MemberExpression',
    messageId: 'unexpected',
    endLine: 1,
    endColumn: 12
  }
]
复制代码

如果我们在调用时不声明任何的globals

const cli = new CLIEngine({
-   globals: []
});
复制代码

则打印结果是:

[
  {
    ruleId: 'no-console',
    severity: 2,
    message: 'Unexpected console statement.',
    line: 1,
    column: 1,
    nodeType: 'MemberExpression',
    messageId: 'unexpected',
    endLine: 1,
    endColumn: 12
  },
  {
    ruleId: 'no-undef',
    severity: 2,
    message: "'wqwq' is not defined.",
    line: 1,
    column: 13,
    nodeType: 'Identifier',
    messageId: 'undef',
    endLine: 1,
    endColumn: 17
  }
]
复制代码

这说明globals的设置起到了作用,接下来我们分析源代码,看看里面是如何对globals做处理的

我们在 调用verifyText函数 处打印改函数参数里的config,发现globals已经转化成了globals: { wqwq: false },这说明我们的思路是对的,所以转化工作在调用verifyText函数就已经完成了

进过一系列的调试,我们最终发现在 createConfigDataFromOptions 发现了我们想要的转化 toBooleanMap

结论是,命令行配置会转化成配置文件的格式

如何用ESLint接口扫描文件

最简单的方式就是调用executeOnFiles接口,该方法会自动根据要扫描的文件路径自动加载ESLint配置文件,当然你也可以在new CLIEngine的时候传入配置,如果传入了则该传入的配置优先级最高,示例代码如下:

const CLIEngine = require('eslint').CLIEngine;

const cli = new CLIEngine({
    globals: ['wqwq']
});
const result = cli.executeOnFiles('../test/test22.js');
console.log(result.results[0].messages);
复制代码

如果ESLint配置文件引用的是@babel/eslint-parser,则需要用parserOptions指明babel配置文件的路径:

const path = require('path');

module.exports = {
    parser: '@babel/eslint-parser',
    parserOptions: {
        babelOptions: {
            configFile: path.join(__dirname, './.babelrc')
        }
    },
    env: {
    },
    extends: 'your_extends',
    globals: {
    },
    rules: {
    }
};

复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享