flat config 的匹配规则
ESLint flat configs 是由多个 config 对象组成的数组。那么针对某个文件(filePath)去做代码校验和格式化的时候,应用了哪些 config 对象呢?
整体思路是:
- 先找出与 filePath 匹配的 config 对象;
- 然后将这些 config 对象按照一定的策略合并成一个对象(每个属性都有自己的合并策略)。
config 对象有以下属性:
// config 对象
{
files: [string, function, [string, function]] // files 类型是数组,支持的元素类型是 string、function, 或者 string, function 构成的数组
ignores,
language,
languageOptions: {
ecmaVersion,
sourceType,
globals,
parser,
parserOptions
},
plugins
processor: { `preprocess()` 和 `postprocess()` }
rules
}
如何找到与 filePath 匹配的 config 对象?
-
如果 config 对象 没有 files:
- 没有 ignores ,则该 config 对象是匹配的。
- 有 ignores,需要检查
filePath是否与ignores中的模式匹配。如果匹配,意味着filePath是属于被忽略的文件,此时该 config 对象是不匹配的。反之,如果filePath不匹配任何ignores模式,则该 config 对象是匹配的。
-
如果 config 对象 有 files
-
files 数组的任何一个元素是
pattern * or patterns ending in /** or /*,那么该 config 对象是匹配的。 -
如果 filePath 匹配了 files(files 数组中只要有一个匹配就算匹配,如果 files 的元素是数组,那么需要这个数组每个元素都匹配才算匹配, 参见 demo1)。 接下来还需要检查
ignores中的规则。如果filePath被ignores中的某个模式所匹配,意味着该文件路径是被忽略的,此时该 config 对象是不匹配的。反之,如果filePath不在ignores的范围内,则该 config 对象是匹配的(参见 demo2)。// demo1 { files: [ '**/*.js', [ '**/*.mjs', (filePath) => filePath.includes('app') ] ] } // 如果 filePath 是 test.js, 它匹配 `files[0]`,也就是 `'**/*.js'`,所以 filePath 匹配了 files // 如果 filePath 是 test.mjs, 它不匹配 `files[0]`,`files[1]` 中只匹配了`'**/*.mjs'`,不匹配 `filePath.includes('app')`,所以是不匹配 `files[1]` 的,因为 `files[0] & files[1]` 都不匹配,所以 filePath 不匹配 files// demo2 const config = { files: [ "src/**/*.js", // 匹配 src 目录下的所有 JS 文件 "src/utils/*.js" // 匹配 src/utils 目录下的所有 JS 文件 ], ignores: [ "src/utils/test.js", // 忽略 src/utils/test.js 文件 "src/**/*.spec.js" // 忽略 src 目录下所有以 .spec.js 结尾的文件 ] }; // 要检查的文件路径 const filePath = "src/utils/test.js"; // 尽管 `filePath` `src/utils/test.js` 匹配了 `files` 中的模式,但由于它也被 `ignores` 中的规则匹配,因此该条 config item 不会被添加
-
如何将匹配的 config 对象合并?
匹配的 config 对象是按照一定的策略合并的。eslint 内部对 config 对象的每个属性都定义了合并的策略(参见:/xxx-project/node_modules/eslint/lib/config/flat-config-schema.js -> flatConfigSchema)。
flatConfigSchema 中定义了 language, languageOptions, rules, plugins 等校验(validate)以及合并(merge)的方式。
合并逻辑伪代码:
合并多个配置对象(filteredConfigObjects),并根据每个属性的合并策略(configSchema)来决定如何合并这些对象的属性。
// filteredConfigObjects: 某个 filePath 匹配的 config 对象
filteredConfigObjects.reduce((result, obj) => {
return merge(result, obj)
}, {})
// configSchema: 每个属性的合并策略
merge(result, obj) {
for (const [key, strategy] of configSchema) {
if (key in result || key in obj) {
const mergeFunc = strategy.merge
const value = mergeFunc(result[key], obj[key])
if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}
languageOptions 的合并逻辑
1. languageOptions 有哪些配置?configure Language Options
languageOptions: {
ecmaVersion, // 默认:"latest",指示正在检查的代码的 ECMAScript 版本。不同的 ECMAScript 版本可能会引入不同的全局变量。例如,ES6 引入了 `Promise` 和 `Map` 等全局对象。设置 `ecmaVersion` 可以确保 ESLint 正确识别和处理这些全局变量。
sourceType, // 默认:script。其他可选:module, commonjs
globals, //
parser, // 默认是 espree, 可指定其他兼容 eslint 的 parser, 例如:@babel/eslint-parser, @typescript-eslint/parser
parserOptions: {}, // 如果是自定义 parser,parserOptions 的值会直接传给自定义 parser,用户可查看相关 parser 的文档看支持哪些 options.
// 如果使用默认 parser(espree), 那么支持以下配置(默认都是 false)
parserOptions: {
allowReserved,
ecmaFeatures: {
globalReturn,
impliedStrict,
jsx // 是否启用 jsx
}
},
}
2. languageOptions 如何合并?
parser 属性是后者替换前者,其他属性(ecmaVersion, sourceType, globals 等)是 deepMerge。
deepMerge:
const config1 = {
languageOptions: {
sourceType: 'commonjs',
globals: {
performance: true,
Storage: false
},
parser: {
mats: {name: 'typescript-eslint/parser', version: '8.17.0'},
parse: function(code, options) {},
parseForESLint: function(code, parserOptions) {}
},
parserOptions: {}
}
};
const config2 = {
languageOptions: {
sourceType: 'module',
globals: {
onhashchange: true,
performance: false
},
parser: {
meta: { name: 'vue-eslint-parser', version: '9.4.3' },
parse: function(code, options) {},
parseForESLint: function(code, parserOptions) {}
},
parserOptions: {
parser: {
js: 'espree',
jsx: 'espree',
ts: {
meta: {
name: 'typescript-eslint/parser',
version: '8.17.0'
}
},
tsx: {
meta: {
name: 'typescript-eslint/parser',
version: '8.17.0'
}
}
}
}
}
};
deepMerge(config1, config2) => {
languageOptions: {
sourceType: 'module',
globals: {
performance: false,
Storage: false,
onhashchange: true
},
parser: {
meta: { name: 'vue-eslint-parser', version: '9.4.3' },
parse: function(code, options) {},
parseForESLint: function(code, parserOptions) {}
},
parserOptions: {
parser: {
js: 'espree',
jsx: 'espree',
ts: {
meta: {
name: 'typescript-eslint/parser',
version: '8.17.0'
}
},
tsx: {
meta: {
name: 'typescript-eslint/parser',
version: '8.17.0'
}
}
}
}
}
}
- 如果属性是基础类型 or 数组类型,后者替换前者(
res.sourceType = 'module') - 如果属性是对象类型(例如 config1.globals 和 config2.globals),不同的属性合并,相同的属性后者替换前者(
languageOptions.globals = { performance: false, Storage: false, onhashchange: true })
plugins 的合并逻辑
- 如果
first和second同时存在相同的 key(例如:vue),但是值不相同就会报错。 - 优先使用
second中的值,如果不存在,则使用first中的值。
[
// config 1 ----> first
{
plugins: {
@: {},
vue: {},
},
},
// config 2 ----> second
{
plugins: {
@typescript-eslint: {},
}
}
]
// 合并之后的结果是
{
"plugins": {
@: {},
vue: {},
@typescript-eslint: {},
},
}
源代码:
/xxx-project/node_modules/@eslint/config-array/dist/cjs/index.cjs ConfigArray -> getConfigWithStatus
-
找到与 filePath 匹配的 config 对象
-
将匹配的 config 对象合并
language, languageOptions, parser, parserOptions
language, languageOptions 是顶层配置,parser, parserOptions 属于 languageOptions 中的配置,parserOptions 中还可以配置 parser
{
language,
languageOptions: {
parser,
parserOptions: {
parser: {
js ='espree'
jsx ='espree'
ts ={meta: {…}, parseForESLint: ƒ}
tsx ={meta: {…}, parseForESLint: ƒ}
}
}
}
}
languageOptions.parser:全局指定 ESLint 使用的解析器。languageOptions.parserOptions.parser:在特定情况下(如不同文件类型)指定使用的解析器,允许更细粒度的控制。