获取到给定路径的所有父路径
例如给定一个路径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
直到directoryPath
是D:/
(将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
、.eslintrc
、package.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
命令时,涉及到获取配置文件的完整调用链条是:
- 执行
node_modules/eslint/.bin/eslint.js
文件 - 执行
node_modules/eslint/lib/cli.js
文件中的 execute 函数,在此函数中调用了const engine = new ESLint(translateOptions(options));
,接着调用engine.lintFiles(files)
- 执行
node_modules/eslint/lib/eslint/eslint.js
文件中的 lintFiles 函数, - 执行
node_modules/eslint/lib/cli-engine/cli-engine.js
文件中的 executeOnFiles 方法 - 执行
node_modules/eslint/lib/cli-engine/file-enumerator.js
文件中的iterateFiles
方法,然后依次调用_iterateFiles -> _iterateFilesWithFile -> getConfigArrayForFile
- 执行
node_modules/@eslint/eslintrc/lib/cascading-config-array-factory.js
文件的 getConfigArrayForFile 函数,在该函数内又调用了 _loadConfigInAncestors -> loadInDirectory - 执行
node_modules/@eslint/eslintrc/lib/config-array-factory.js
文件的 loadInDirectory函数,在该函数内依次(递归)调用:
配置文件内容转换
CLIEngine
接口接收的参数里面,有些和ESLint
配置文件(.eslintrc.js
、.eslintrc.cjs
、.eslintrc.yaml
、.eslintrc.yml
、.eslintrc.json
、.eslintrc
、package.json
)里的属性是不一样的,例如接收的参数里 globals 字段是个数组,而配置文件里的 globals 是个对象,于是数据的转换就成了必然的,ESLint
里是如何处理这一点的呢?
实际上通过阅读代码我们可以知道,从node_modules/eslint/lib/cli.js
的 engine.lintFiles(files) 根据一个被扫描的文件路径files
,lintFiles
函数就已经可以返回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: {
}
};
复制代码