前言
ESLint
,众所周知,他的主要工作是校验我们写的代码,然后规范我们的代码,今天聊聊ESLint
是怎么工作的。
必要性?
一个项目一般情况下都是多人协同开发的(除了我自己做的那个门户)【手动狗头】,那就意味着大家的代码风格肯定多多少少都存在一定的差异,如果大家都随心而欲,没有约束的进行编码,后期维护的成本也就越来越大,如果再加上某些同事提桶,那就是事故,因此,ESLint
还是十分有必要的,在我们书写代码的时候,对基本写法进行一个约束,然后必要的时候弹出提示,而且一些小的问题还可以帮我们修复,何乐而不为?
打开官网,映入眼帘的便是:Find and fix Problems in your JavaScript Code
,光看这个就很nice。
再往下看:
- Find Problems
- Fix Automatically
- Customize
这也就是ESLint
的主要工作,找到问题
、自动修复
、客制化
。
工作模式
ESLint
通过遍历AST
,然后再遍历到不同的节点或者合适的时机的时候,触发响应的函数,抛出错误。
配置读取
ESLint
会从eslintrc
或者package.json.eslintConfig
中读取配置,前者的优先级会大于后者,如果同级目录下存在多个配置文件,那么这层目录只有一个配置文件会被读取,默认会进行逐层读取
配置文件,最终合并为一个。如果多个配置文件里都配置了重复的字段,那里给定目录最近的配置会生效,可以在配置文件中添加root: true
来阻止逐层读取
。
底层实现:
代码语言:javascript复制// 加载配置在目录中
try {
/** configArrayFactory.loadInDirectory 这个方法会依次加载配置里的extends, parser, plugin */
configArray = configArrayFactory.loadInDirectory(directoryPath);
} catch (error) {
throw error;
}
// 如果配置了root, 终端外层遍历
if(configArray.length > 0 && configArray.isRoot()) {
configArray.unshift(...baseConfigArray);
return this._cacheConfig(directoryPath, configArray)
}
// 向上查找配置文件 & 合并
const parentPath = path.dirname(directoryPath);
const parentConfigArray = parentPath && parentPath !== directoryPath ? this._loadConfigInAncestors() : baseConfigArray;
if(configArray.length > 0) {
configArray.unshift(...parentConfigArray)
} else {
configArray = parentConfigArray
}
// 需要进行加载的配置文件名称列表
const configFilenames = [
.eslintrc.cjs ,
.eslintrc.yaml ,
.eslintrc.yml ,
.eslintrc.json ,
.eslintrc ,
package.json
]
loadInDirectory(directoryPath, { basePath, name } = {}) {
const slots = internalSlotsMap.get(this);
// configFilenames中的index决定了优先级
for(const filename of configFilenames) {
const ctx = createContext();
if(fs.existsSync(ctx.filePath) && fs.statSync(ctx.filePath).isFile()) {
let configData;
try {
configData = loadConfigFile(ctx.filePath);
} catch(error) {}
if(configData) {
return new ConfigArray()
}
}
}
return new ConfigArray()
}
配置加载
extends
是一些扩展的配置文件,ESLint
允许使用插件中的配置,或者第三方模块中的配置。ESLint
会去读取配置文件中的extends,如果extends的层级比较深,先做递归处理,然后再返回自己的配置,最终得到的顺序是【extends, 配置】。
/** 加载扩展 */
_loadExtends(extendName, ctx) {
...
return this._normalizeConfigData(loadConfigFile(ctx.filePath),ctx)
}
/** 格式化校验配置数据 */
_normalizeConifgData(config, ctx) {
const validator = new ConfigValidator();
validator.validateConfigSchema(configData, ctx.name || ctx.filePath);
return this._normalizeObjectConfigData(configData, ctx);
}
*_normalizeObjectConfigData(configData, ctx) {
const { files, excludedFiles, ...configBody } = configData;
const criteria = OverrideTester.create();
const elements = this._normalizeObjectConfigDataBody(configBody, ctx);
}
*_normalizeObjectConfigDataBody({extends: extend}, ctx) {
const extendList = Array.isArray(extend) ? extend : [extend];
...
// Flatten `extends`
for (const extendName of extendList.filter(Boolean)) {
/** 递归调用加载扩展配置 */
yield* this._loadExtends(extendName, ctx);
}
yield {
// Debug information.
type: ctx.type,
name: ctx.name,
filePath: ctx.filePath,
// Config data.
criteria: null,
env,
globals,
ignorePattern,
noInlineConfig,
parser,
parserOptions,
plugins,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings
};
}
虽然自由配置的顺序是在extend config之后,但是,当所有配置都加载完,使用的时候,会调用一个extractConfig
& createConfig
方法,把配置对象的顺序进行翻转&把所有的配置对象合并为一个对象。
extractConfig(filePath) {
const { cache } = internalSlotsMap.get(this);
const indices = getMatchedIndices(this, filePath);
const cacheKey = indices.join( , );
if (!cache.has(cacheKey)) {
cache.set(cacheKey, createConfig(this, indices));
}
return cache.get(cacheKey);
}
/** 把数组顺序反转过来 */
function getMatchedIndices(elements, filePath) {
const indices = []
for (let i = elements.length - 1; i >= 0; --i) {
const element = elements[i];
if (!element.criteria || (filePath && element.criteria.test(filePath))) {
indices.push(i);
}
}
return indices;
}
createConfig
代码语言:javascript复制function createConifg(instance, indices) {
const config = new ExtractedConfig();
const ignorePatterns = [];
// 合并元素
for(const index of indices) {
const element = instance[index];
// 获取paser & 赋值给config.parser,进行覆盖操作
if(!config.parser && element.parser) {
// 如果parser有报错直接抛出
if(element.parser.error) {
throw element.parser.error
}
config.parser = element.parser
}
// 获取processor & 赋值给config.processor,进行覆盖操作
if (!config.processor && element.processor) {
config.processor = element.processor;
}
// 获取noInlineConfig & 赋值给config.noInlineConfig,进行覆盖操作
if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) {
config.noInlineConfig = element.noInlineConfig;
config.configNameOfNoInlineConfig = element.name;
}
// 获取reportUnusedDisableDirectives & 赋值给config.reportUnusedDisableDirectives,进行覆盖操作
if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) {
config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives;
}
// 处理忽略
if(element.ignorePattern) {
ignorePatterns.push(element.ignorePattern);
}
// 合并操作
mergeWithoutOverwrite(config.env, element.env);
mergeWithoutOverwrite(config.globals, element.globals);
mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
mergeWithoutOverwrite(config.settings, element.settings);
mergePlugins(config.plugins, element.plugins);
mergeRuleConfigs(config.rules, element.rules);
}
if (ignorePatterns.length > 0) {
config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse());
}
return config;
}
结论:
- parser、processor、noInlineConfig、reportUnusedDisableDirectives,后面的配置会覆盖前面的配置。
- env、globals、parserOptions、settings会进行合并操作,但是在
mergeWithoutOverwrite
函数中的合并中是进行并集。 - rules 是后面的配置优先级高于前面的。
parser & plugin
parser
和 plugin
是以第三方模块的形式加载进来的,所以如果要自定义,需要先发布在使用,约定包名为eslint-plugin-xxx
,配置中可以把xxx
的前缀省略。
_loadParser(nameOrPath, ctx) {
try {
const filePath = resolver.resolve(nameOrPath, relativeTo);
return new ConfigDependency({
definition: require(filePath),
...
});
} catch(error) {
// If the parser name is espree , load the espree of ESLint.
if (nameOrPath === espree ) {
debug( Fallback espree. );
return new ConfigDependency({
definition: require( espree ),
...
});
}
return new ConfigDependency({
error,
id: nameOrPath,
importerName: ctx.name,
importerPath: ctx.filePath
});
}
}
_loadPlugin(name, ctx) {
// 处理包名
const request = naming.normalizePackageName(name, eslint-plugin );
const id = naming.getShorthandName(request, eslint-plugin );
const relativeTo = path.join(ctx.pluginBasePath, __placeholder__.js );
// 检查插件池,有则复用
const plugin =
additionalPluginPool.get(request) ||
additionalPluginPool.get(id);
if (plugin) {
return new ConfigDependency(
definition: normalizePlugin(plugin),
filePath: , // It's unknown where the plugin came from.
id,
importerName: ctx.name,
importerPath: ctx.filePath
});
}
let filePath;
let error;
filePath = resolver.resolve(request, relativeTo);
if (filePath) {
try {
const startTime = Date.now();
const pluginDefinition = require(filePath);
return new ConfigDependency({...});
} catch (loadError) {
error = loadError;
}
}
}
前半部分总结
上面聊得就是ESLint对于整个配置读取以及配置加载的流程以及原理,这里简单用一个代码总结一下都做了啥
代码语言:javascript复制reading:
// 是否有 eslintrc or package.json
switch:
case: eslintrc || (eslintrc && package.json)
read eslitrc
load()
break
case: package.json
read package.json
load()
break
load:
switch:
case: extends
read extends
case !extends
current end
isRoot ? all end : reading();
对你的代码进行校验 verify
Eslint的源码中 verfiy方法主要就做一些判断,然后根据条件分流到其他的方法进行处理:
代码语言:javascript复制verify(textOrSourceCode, config, filenameOrOptions) {
const { configType } = internalSlotsMap.get(this);
if (config) {
if (configType === flat ) {
let configArray = config;
if (!Array.isArray(config) || typeof config.getConfig !== function ) {
configArray = new FlatConfigArray(config);
configArray.normalizeSync();
}
return this._distinguishSuppressedMessages(this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true));
}
if (typeof config.extractConfig === function ) {
return this._distinguishSuppressedMessages(this._verifyWithConfigArray(textOrSourceCode, config, options));
}
}
if (options.preprocess || options.postprocess) {
return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, config, options));
}
return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, config, options));
}
基本是以先处理processor
,解析获取AST
和节点数组
,跑runRules
processor
processor是一个预处理器,用于处理特定后缀的文件,包含两个方法preprocess
& postprocess
。
- preprocess 的参数为源码or文件名,返回一个数组,每一项为需要被校验的代码块或者文件
- postprocess 主要是对校验完文件之后的问题(error,wraning)进行统一处理
AST对象
ESLint的解析规则是如果没有指定parser,默认使用expree,否则使用指定的parser,这里需要对AST有足够的了解,大家只需要知道AST对象,就是把你写的代码转换成一个可以可供分析的对象,也可以理解为JS的虚拟DOM, 举个