【源码角度】7分钟带你搞懂ESLint核心原理!

2022-10-26 08:36:17 浏览数 (3)

前言

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, 配置】。

代码语言:javascript复制
/** 加载扩展 */
_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方法,把配置对象的顺序进行翻转&把所有的配置对象合并为一个对象。

代码语言:javascript复制
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

parserplugin 是以第三方模块的形式加载进来的,所以如果要自定义,需要先发布在使用,约定包名为eslint-plugin-xxx,配置中可以把xxx的前缀省略。

代码语言:javascript复制
_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, 举个

1 人点赞