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

简介: 前言ESLint,众所周知,他的主要工作是校验我们写的代码,然后规范我们的代码,今天聊聊ESLint是怎么工作的

前言

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来阻止逐层读取

底层实现:

// 加载配置在目录中
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

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的前缀省略。

_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对于整个配置读取以及配置加载的流程以及原理,这里简单用一个代码总结一下都做了啥

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方法主要就做一些判断,然后根据条件分流到其他的方法进行处理:

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, 举个🌰

var name = 'HoMeTown'
// AST 
{
  "type": "Program",
  "start": 0,
  "end": 22,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 21,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 21,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 8,
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "start": 11,
            "end": 21,
            "value": "HoMeTown",
            "raw": "'HoMeTown'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}
复制代码

ruleRules

前面聊得那些其实都是ESLint的一些工作机制,规则才是ESLint的核心,工作原理其实也就是通过保存AST节点,然后遍历所有配置中的rulename,通过rule的名称找到对应的rule对象(也就是具体的规则),具体的方法为给每一个AST节点添加监听函数,遍历nodeQueue的时候触发响应的处理函数。

// ruleListeners AST节点
Object.keys(ruleListeners).forEach(selector => {
    const ruleListener = timing.enabled
        ? timing.time(ruleId, ruleListeners[selector])
        : ruleListeners[selector];
    emitter.on(
        selector,
        addRuleErrorHandler(ruleListener)
    );
});
....
// 遍历节点数组,不同的节点触发不同的坚定函数,在监听函数中调用方法,收集问题
nodeQueue.forEach(traversalInfo => {
    currentNode = traversalInfo.node;
    try {
        if (traversalInfo.isEntering) {
            eventGenerator.enterNode(currentNode);
        } else {
            eventGenerator.leaveNode(currentNode);
        }
    } catch (err) {
        err.currentNode = currentNode;
        throw err;
    }
});
复制代码

disabled

大家都知道我们可以利用eslint-disabledeslint-disabled-line禁用lint,需要注意的是,他是在lint完AST、get problem之后,对所有的问题进行一次过滤。

取出:

function getDirectiveComments(ast){
    const directives = [];
    ast.comments.forEach(comment => {
        const match = /^[#@](eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(comment.trim());
        if (match) {
            const directiveText = match[1];
            ...
            directives.push({ type: xxx, line: loc.start.line, column: loc.start.column + 1, ruleId });
        }
    }
    return directives;
}
复制代码

其实就是对 AST 中所有的 comments 的内容做一下正则的匹配,如果是支持的 directive,就把它收集起来,并且记录下对应的行列号。

之后就是对 problems 的过滤了。

function applyDisableDirectives(problems, disableDirectives) {
  const filteredProblems = [];
  const disabledRuleMap = new Map();
  let nextIndex = 0;
  for (const problem of problems) {
    // 对每一个 probelm,都要找到当前被禁用的 rule
    while (nextIndex < disableDirectives.length && compareLocations(disableDirectives[nextIndex], problem) <= 0) {
      const directive = disableDirectives[nextIndex++];
      switch (directive.type) {
        case 'disable':
          disabledRuleMap.set(directive.ruleId, directive);
          break;
        case 'enable':
          disabledRuleMap.delete(directive.ruleId);
          break;
      }
    }
    // 如果 problem 对应的 rule 没有被禁用,则返回
    if (!disabledRuleMap.has(problem.ruleId)) {
      filteredProblems.push(problem);
    }
  }
  return filteredProblems;
}
function compareLocations(itemA, itemB) {
  return itemA.line - itemB.line || itemA.column - itemB.column;
}
复制代码

Fix

接下来就是修复了,主要用到SourceCodeFixer类中的applyFixes这个方法

/**
 * Try to use the 'fix' from a problem.
 * @param {Message} problem The message object to apply fixes from
 * @returns {boolean} Whether fix was successfully applied
 */
function attemptFix(problem) {
    const fix = problem.fix;
    const start = fix.range[0];
    const end = fix.range[1];
    // 如果重叠或为负范围,则将其视为问题
    if (lastPos >= start || start > end) {
                    remainingMessages.push(problem);
                    return false;
    }
    // 移除非法结束符.
    if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
                    output =  "" ;
    }
    // 拼接修复后的结果, output是一个全局变量
    output += text.slice(Math.max(0, lastPos), Math.max(0, start));
    output += fix.text;
    lastPos = end;
    return true;
}
复制代码

全文总结

ESLint的流程大概就是这样,下面再做一个总结

读取各种配置(eslintrc, package.json.eslintConfig)
...
加载插件(获取到插件的规则配置)
...
读取parser配置,解析AST
...
深度优先遍历AST收集节点
...
注册所有配置里的节点监听函数
...
遍历收集到的AST节点,触发节点监听函数,获取lint纹理
...
根据注释中的禁用命令进行过滤
...
修复
复制代码

完结~


目录
相关文章
|
4月前
|
前端开发 测试技术 UED
React性能优化的神奇之处:如何用懒加载与代码分割让你的项目一鸣惊人?
【8月更文挑战第31天】在现代Web开发中,性能优化至关重要。本文探讨了React中的懒加载与代码分割技术,通过示例展示了如何在实际项目中应用这些技术。懒加载能够延迟加载组件,提高页面加载速度;代码分割则将应用程序代码分割成多个块,按需加载。两者结合使用,可以显著提升用户体验。遵循合理使用懒加载、编写测试及关注性能等最佳实践,能够更高效地进行性能优化,提升应用程序的整体表现。随着React生态的发展,懒加载与代码分割技术将在未来Web开发中发挥更大作用。
54 0
|
6月前
|
存储 C语言 C++
一文搞懂:世界上最神奇的mif文件生成方案
一文搞懂:世界上最神奇的mif文件生成方案
71 0
|
安全 5G 网络安全
5 分钟搞懂 5G 安全增强
5 分钟搞懂 5G 安全增强
204 0
|
JSON 前端开发 数据可视化
umi3源码探究简析
作为蚂蚁金服整个生态圈最为核心的部分,umi可谓是王冠上的红宝石,因而个人认为对于整个umi架构内核的学习及设计哲学的理解,可能比如何使用要来的更为重要;作为一个使用者,希望能从各位大佬的源码中汲取一些养分以及获得一些灵感
244 0
|
JavaScript 测试技术 API
深入解析 Vue 的热更新原理,尤大是如何巧用源码中的细节?
大家都用过 Vue-CLI 创建 vue 应用,在开发的时候我们修改了 vue 文件,保存了文件,浏览器上就自动更新出我们写的组件内容,非常的顺滑流畅,大大提高了开发效率。想知道这背后是怎么实现的吗,其实代码并不复杂。
|
前端开发
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
|
JavaScript 前端开发 开发者
快速了解ES6模块化少不了这篇文章
在之前的JavaScript中是没有模块化概念的,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。如果要进行模块化操作,就需要引入第三方的类库。随着技术的发展,前后端分离,前端的业务变的越来越复杂化,于是才有了ES6模块化的诞生。 为什么要有模块化,或者模块化的好处是什么呢?
|
JavaScript 编译器
学习Vue源码之前要先打基础--概念解析
在学习源码之前,我们需要了解这么几个东西的概念。什么是AST、什么是render函数、什么是模板等等
192 0