likes
comments
collection
share

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

作者站长头像
站长
· 阅读数 17

前言

在前端工程化的标准中有一项就是自动化,自动化当中就包括了代码规范自动化。实现代码规范自动化可以解放团队生产力,提供团队生产效率。既然代码规范自动化可以带来如此的好处,我们自然要好好了解它并且掌握它。

现在几乎所有的团队使用的代码检查工具都是以 ESLint 为代表的 Linter 和 Prettier 进行配套使用。通过这些工具进行代码自动化检查,让开发者可以完全聚焦业务开发,不必在代码整理上花费过多的心思。相对其他需要编译的语言可以在编译阶段进行代码检查不同, JavaScript 不具备先天编译流程,同时是一种动态、宽松类型的语言,所以容易在运行时暴露错误。而通过 ESLint 可以让开发者提前将更多的错误或不合理的写法暴露在开发阶段。

任何工具我们都只有理解它的原理才能真正地掌握它,所以下面我们将从 ESLint 的原理剖析开始吧。

编译的基本原理

ESLint 是基于静态语法分析(AST)进行工作的,默认使用 JavaScript 编译器 Espree 来解析 JS 代码生成 AST。有了 AST ,我们就可以基于 AST 对代码进行检查和修改。AST 是: Abstract Syntax Tree 的首字母缩写,中文叫做:抽象语法树。所谓抽象语法树指的是用来描述被编译的代码的一个对象。

在这里我们需要简单了解一下编译技术的基本原理。所谓编译技术就是通过编译器将源代码转换成目标代码,这个过程就叫编译,其中包含词法分析、语法分析、语义分析、编译优化、目标代码生成等步骤。

第一阶段就是将源码通过词法分析和语法分析得到源码 AST,第二阶段就是将源码 AST 转换(transform)成目标代码 AST,最后根据目标代码 AST 生成目标代码

而 ESLint 中只用到传统编译流程的第一阶段,而第二阶段 ESLint 则有自己的实现逻辑。

ESLint 中主要涉及到的 AST 知识点

词法分析、语法分析、语义分析这一阶段的工作是编译器 Espree 来做的,这一部分本文不会作过多分析,我们只需知道通过编译器解析源代码便生成了 AST。我们需要做的工作简单来说就是下面的代码实现:

const fs = require('fs')
const path = require('path')
// 先要安装 npm install espree 包
const espree = require('espree')

const filePath = path.resolve('./test.js')
// 注意是通过 utf8 格式读取文件内容
const text = fs.readFileSync(filePath, "utf8")

// 编译成 AST
const ast = espree.parse(text,{ 
    comment: true, // 创建包含所有注释的顶级注释数组
    ecmaVersion: 6, // JS 版本
    // 指定其他语言功能,
    ecmaFeatures: { 
        jsx: true, // 启用JSX解析
        globalReturn: true // 在全局范围内启用return(当sourceType为“commonjs”时自动设置为true)
    }, 
    loc: true, // 将行/列位置信息附加到每个节点
    range: true, // 将范围信息附加到每个节点
    tokens: true // 创建包含所有标记的顶级标记数组
})

通过上述代码我们就可以将一个 JavaScript 文件内容转换成 AST。

我们设置 test.js 的内容为:

使用 fs.readFileSync 通过 utf8 字符集读取内容:

const filePath = path.resolve('./test.js')
const text = fs.readFileSync(filePath, "utf8")

读取到的内容为:

我们可以看到源文件的内容是两行,而使用 fs.readFileSync 通过 utf8 字符集读取到的内容则变成了一行,换行则被替换成 \r\n 。我们的编译器正是根据这些特点进行设置 AST 数据的。

我们还可以通过 astexplorer.net 这个网站,查看代码被编译成 AST 之后的样子。

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

图中红色圈圈住的是代表所选用的编译器是 Espree 及其设置项。

我们从上图可以看到被编译后的 AST 有两个顶级属性:start 和 end,end 的值为 25,顾名思义是结束的位置是 25,那么这个是怎么算出来的呢?其实我们把上面读取到的内容:

\r\n 当作一个字符,整一行文本的字符个数(包括空字符)刚好就是 25。

loc 则是行/列的位置信息,那么是怎么确定行和列的呢?很明显是通过 \r\n 进行正则匹配来确定的,有多少个 \r\n 就有多少行(这么说好像也不太严谨,但通过 \r\n 就可以确定行数了),每一行的列则又由每一行的字符数位置来确定。

range 则代表读取源文件之后的文本内容的位置信息。在本文中指的就是:

比如 “稀土” 的 range 位置信息为:

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

因为内容不多,我们又可以通过一个字符一个字符计算进行印证。

AST 就是记录了读取源文件之后的文本内容的各个单位的位置信息,这样我们就可以通过操作 AST 修改需要修改的内容,然后再根据修改后的 AST 信息进行修改对应的文本内容。比如我们把上文中的 var 关键字修改成 let ,那么我们就先对 AST 对应的 var 内容进行修改为 let ,得到修改之后的 AST 数据,再根据修改后的 AST 数据去修改对应的文本内容。所谓修改就是字符串替换,因为我们已经知道了对应的位置信息。

AST 的二次封装

直接通过编译器 Espree 生成的 AST 是不方便操作的,所以我们还需要进一步对其进行包装处理:

const sourceCode = new SourceCode({
    text, // 读取源文件之后的文本内容
    ast // 通过 Espree 生成的 AST 
})

ESLint 中是通过一个 SourceCode 类对通过 Espree 生成的 AST 进行二次封装的。封装生成后的对象如下:

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

相信我们有很多同学有写过或者了解过 ESLint 插件的规则相关知识,ESLint 规则代码结构如下:

module.exports = {
    create: function(context) {
        var sourceCode = context.getSourceCode();

        // ...
    }
};

上述代码中通过 context.getSourceCode() 获取到 sourceCode 对象就是前面说到的 ESLint 中是通过一个 SourceCode 类对通过 Espree 生成的 AST 进行二次封装的实例对象。

这个实例对象提供了非常多方法:

  • getText(node) - 返回给定节点的源码。省略 node,返回整个源码。
  • getAllComments() - 返回一个包含源中所有注释的数组。
  • getCommentsBefore(nodeOrToken) - 返回一个在给定的节点或 token 之前的注释的数组。
  • getCommentsAfter(nodeOrToken) - 返回一个在给定的节点或 token 之后的注释的数组。
  • getCommentsInside(node) - 返回一个在给定的节点内的注释的数组。
  • getJSDocComment(node) - 返回给定节点的 JSDoc 注释,如果没有则返回 null。
  • isSpaceBetweenTokens(first, second) - 如果两个记号之间有空白,返回 true
  • getFirstToken(node, skipOptions) - 返回代表给定节点的第一个token

这里只罗列部分方法,更多可以查看 ESLint 的官网。

ESLint 中的访问者模式

我们从前文知道源码经过编译器解析成 AST 之后,就是进行编译优化(transform),再进行转换成目标源码。而在 ESLint 中在获得源码的 AST 之后主要的工作就遍历 AST,然后找到需要 AST 的节点获取相关信息。具体 ESLint 中则使用了设计模式的访问者模式。所谓访问者模式就是当被操作的对象结构比较稳定时,就可以把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化

AST 中有很多类型的数据节点,比如 Literal 字面量Identifer 标识符VariableDeclarator 变量声明FunctionDeclaration 函数声明,那么我们需要这些节点数据进行操作的时候,就可以根据不同的类型创建不同的访问者,在遍历 AST 的时候,当命中对应访问者的时候,就调用对应访问者操作逻辑对节点进行操作。

那么要遍历 AST,就需要把 AST 组装成数组形式。ESLint 中是把 AST 数据进行铺平,把所有的节点组装成一个一维数组。具体就是通过 Traverser 类的 traverse 方法:

const nodeQueue = [];
Traverser.traverse(sourceCode.ast, {
    enter(node, parent) {
        node.parent = parent;
        nodeQueue.push({ isEntering: true, node });
    },
    leave(node) {
        nodeQueue.push({ isEntering: false, node });
    },
    visitorKeys: sourceCode.visitorKeys
});

traverse 方法的实现原理也很简单,就是一个递归,在递归开始的时候调用 enter 方法,在递归结束的时候调用 leave 方法,这样就每个节点的数据都形成了进入退出的标记。这样在封装操作节点数据的访问者逻辑的时代就可以设置是在进入时进行处理,还是在退出的时候进行处理。

ESLint 的灵魂——规则

ESLint 中操作节点数据的访问者逻辑其实就在 ESLint 中的规则进行设置的。ESLint 的灵魂就是,每条规则都是独立且插件化的。下面就是一条 ESLint 的规则,主要功能就是检测代码中如果有使用到 var 关键字进行声明变量的,则进行提示和修复,具体修复就是把 var 关键字变成 let 关键字。

module.exports = {
    create(context) {
        const sourceCode = context.getSourceCode()
        return {
            VariableDeclaration(node) {
                if(node.kind === 'var') {
                    context.report({
                        node,
                        message:'不能用var',
                        fix(fixer) {
                            const varToken = sourceCode.getFirstToken(node)
                            return fixer.replaceText(varToken, 'let')
                        }
                    })
                }
            }
        };
    },
};

上述代码中的 VariableDeclaration 函数就是一个访问者,它对应要操作的 AST 数据则是 VariableDeclarator 变量声明 类型的节点,有因为变量声明有 const、let、var 所以又进一步判断是不是 var。

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

上图表示的是 VariableDeclarator 变量声明 类型的节点。

我们从上面 ESLint 的规则插件代码中可以看到 create 方法有一个上下文参数 context,context 对象中拥有各种方法属性。下面我们来看看这个上下文对象是怎么来的。

规则中的上下文对象

到这里我们再回头梳理一遍 ESLint 运行流程,首先对源码通过编译器进行解析成 AST,得到原始的 AST 数据,再对原始的 AST 数据进行封装处理。为了方便遍历操作数据,我们又把 AST 数据进行组装成了一个一维数组,数组的元素就是每一个 AST 的节点,到里这里我就需要遍历数组进行处理 AST 节点。ESLint 是通过规则插件方式处理 AST 节点的,在规则插件中整合了处理 AST 节点的各种访问者操作逻辑函数,在调用规则插件获取各种访问者函数的之前,那么就需要进行封装规则插件所需的上下文对象。

创建规则上下文对象的核心流程代码:

// 加载规则
const rule = require("./rules-module/no-var")
const lintingProblems = [];
let reportTranslator = null;
// 构建规则插件的上下文对象
const ruleContext = {
    getSourceCode: () => sourceCode, // 就是经过 new SourceCode 处理的 AST 对象
    report(...args) {
        if (reportTranslator === null) {
            reportTranslator = createReportTranslator({
                sourceCode,
            });
        }
        const problem = reportTranslator(...args);
        lintingProblems.push(problem);
    }
}
// rule 就是规则插件,其中就包含 create 和 meta,meta 我们这里不作过多解析。
const ruleListeners = rule.create(ruleContext);

我们可以看到规则插件中的上下文对象中的 getSourceCode 方法就是去获取经过 new SourceCode 处理的 AST 对象,所以通过 context.getSourceCode() 获取到 sourceCode 对象就拥有各种方法;上下文对象中的 report 方法则是收集警报信息。

ESLint 中的发布订阅模式

调用规则插件的 create 方法得到的就是我们在规则插件中定义的那些要操作 AST 节点的访问者函数。在遍历 AST 的时候,当命中对应访问者的时候,就调用对应访问者操作逻辑对节点进行操作。又因为有非常多的规则插件,所以我们需要把所有的规则插件中的访问者函数收集起来存储起来,等到使用到的时候再进行读取出来进行执行操作。在 ESLint 的中是使用发布订阅模式。

例如我们上面的规则插件定义了一个 VariableDeclarator 变量声明 类型的 AST 节点处理函数,那么我们就可以在调度中心进行把所有规则插件中定义的 VariableDeclarator 变量声明 类型的 AST 节点处理函数都收集起来,相当于是订阅了一个处理 VariableDeclarator 变量声明 类型的 AST 节点事件,等到遍历到 VariableDeclarator 变量声明 类型的 AST 节点的时候再去看调度中心里面有没有订阅了处理该节点的事件访问者函数,有则全部取出来进行执行。

调度中心代码:

module.exports = () => {
    // 注册中心变量
    const listeners = Object.create(null);
    return Object.freeze({
        // 订阅
        on(eventName, listener) {
            if (eventName in listeners) {
                listeners[eventName].push(listener);
            } else {
                listeners[eventName] = [listener];
            }
        },
        // 发布
        emit(eventName, ...args) {
            if (eventName in listeners) {
                listeners[eventName].forEach(listener => listener(...args));
            }
        },
        // 获取已经注册的事件
        eventNames() {
            return Object.keys(listeners);
        }
    });
};

规则插件订阅:

const ruleListeners = rule.create(ruleContext);

Object.keys(ruleListeners).forEach(selector => {
    const ruleListener = ruleListeners[selector];
    emitter.on(
        selector,
        ruleListener
    );
});

通过规则插件的 create 方法获取到那些要操作 AST 节点的访问者函数,然后注册到调度中心中。

遍历 AST 数组:

// 创建事件处理对象
const eventGenerator = new NodeEventGenerator(emitter);

nodeQueue.forEach(traversalInfo => {
    currentNode = traversalInfo.node;
    try {
        if (traversalInfo.isEntering) {
            eventGenerator.enterNode(currentNode);
        } else {
            eventGenerator.leaveNode(currentNode);
        }
    } catch (err) {
        err.currentNode = currentNode;
        throw err;
    }
});

遍历 AST 数组,然后在 NodeEventGenerator 类中判断每个节点是否有在调度中心注册了处理改节点的访问者函数。在把 AST 处理成数组的时候,我们已经将每一个节点进行了标记是进入阶段还是退出阶段,遍历的时候,再根据标记进行调用不同的方法函数进行处理。

class NodeEventGenerator {
    constructor(emitter) {
        this.emitter = emitter;
        this.enterSelectorsByNodeType = new Map();
        this.exitSelectorsByNodeType = new Map();
        // 把注册中心的订阅的事件进行筛选出来
        emitter.eventNames().forEach(rawSelector => {
            const selector = parseSelector(rawSelector);

            if (selector.listenerTypes) {
                const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;

                selector.listenerTypes.forEach(nodeType => {
                    if (!typeMap.has(nodeType)) {
                        typeMap.set(nodeType, []);
                    }
                    typeMap.get(nodeType).push(selector);
                });
                return;
            }
        });
    }

    /**
     * 根据节点检查选择器,如果匹配则发出
     */
    applySelector(node, selector) {
        this.emitter.emit(selector.rawSelector, node);
    }

    /**
     * 按特定顺序将所有适当的选择器应用于节点
     */
    applySelectors(node, isExit) {
        const selectorsByNodeType = (isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType).get(node.type) || [];
        let selectorsByTypeIndex = 0;
        while (selectorsByTypeIndex < selectorsByNodeType.length) {
            this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
        }
    }

    /**
     * 发出进入AST节点的事件
     */
    enterNode(node) {
        this.applySelectors(node, false);
    }

    /**
     * 发出离开AST节点的事件
     */
    leaveNode(node) {
        this.applySelectors(node, true);
    }
}

这里我们可以不用太关注细节是怎么实现的,我只要知道它主要的功能就是了判断遍历的当前节点是否在调度中心注册有处理该类型的访问者函数。如果有,那么就调度注册中心取出并执行。

如何收集警报信息

以我们上面规则插件为例,在遍历 AST 数组的时候,会进行判断遍历的当前节点是否在调度中心注册有处理该类型的访问者函数:VariableDeclaration 。如果有,那么就调度注册中心取出并执行以下函数,也就是在规则中定义的访问者函数:

VariableDeclaration(node) {
    if(node.kind === 'var') {
        context.report({
            node,
            message:'不能用var',
            fix(fixer) {
                const varToken = sourceCode.getFirstToken(node)
                return fixer.replaceText(varToken, 'let')
            }
        })
    }
}

node 参数就是当前遍历的 AST 节点,因为变量声明有 const、let、var 所以又进一步判断是不是 var。然后执行插件上下文的 report 方法。而执行 report 方法最终执行的是 createProblem 方法:

function createReportTranslator(metadata) {
    return (...args) => {
        const descriptor = args[0];
	   // 执行 report 方法最终执行的是 createProblem 方法
        return createProblem({
            node: descriptor.node,
            message: descriptor.message,
            loc: normalizeReportLoc(descriptor),
            fix: descriptor.fix(ruleFixer),
        });
    };
};

createProblem 方法就是根据参数创建一个记录当前警报信息和当前 AST 节点的位置信息等:

function createProblem(options) {
    const problem = {
        message: options.message,
        line: options.loc.start.line,
        column: options.loc.start.column + 1,
        nodeType: options.node && options.node.type || null
    };

    if (options.loc.end) {
        problem.endLine = options.loc.end.line;
        problem.endColumn = options.loc.end.column + 1;
    }

    if (options.fix) {
        problem.fix = options.fix;
    }

    return problem;
}

这其中最重点的就是修复方法 fix 的理解,在执行 createProblem 方法的时候,就先执行 fix 方法,并且把带有各种修复方法的 ruleFixer 对象作为参数传进去。

关于 ruleFixer 对象,在我们上述规则插件中就只使用到了 replaceText 方法,所以我们这里只看 replaceText 方法:

const ruleFixer = Object.freeze({
    replaceText(nodeOrToken, text) {
        return this.replaceTextRange(nodeOrToken.range, text);
    },
    replaceTextRange(range, text) {
        // 返回要修改的信息,修改的范围位置及修改之后的文本
        return {
            range,
            text
        };
    }
});

从上述代码中我们可以知道所谓的修改就是根据当前的 AST 节点记录的位置信息返回该节点的范围位置信息及要修改的文本信息。

那么这其中最难理解的就是如何获取当前的 AST 节点位置信息,我们上述的规则插件中是通过以下的方式获取的:

 const varToken = sourceCode.getFirstToken(node)

上述代码的 sourceCode 对象是 ESLint 对编译之后的源码 AST 二次封装的对象,getFirstToken() 方法则是该对象提供的 API 方法。 那么这背后到底发生了什么呢?

AST 中的 tokens

这里我们需要再补充了解一个知识点,我们回到将源码编译成 AST 的阶段:

// 编译成 AST
const ast = espree.parse(text,{ 
    // ... 省略
    tokens: true // 创建包含所有标记的顶级标记数组
})

我们注意到在编译器的选项设置中有一个 tokens 的属性,设置为 true 后则会创建包含所有标记的顶级标记数组。

以下是通过 AST Explorer 可视化查看工具快捷查看的编译结果:

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

通过上图我们可以看到记录所有标记的 tokens 数组的结构是怎么样的,其中红色框的信息则是表示源码中第二行的 var 的标记信息。值得注意的是这些标记信息在 AST 对象的 body 属性里是没有这么具体的,要查找 var 具体的标记信息必须通过 tokens 属性进行查找。

在使用 SourceCode 类对通过 Espree 生成的 AST 进行二次封装的时候,会把 AST 对象的 body 属性的节点和 tokens 数组建立关联,所以就可以通过当前的 AST 节点位置信息找到对应 token,因为只有在具体的 token 上才有详细的每个标记的位置信息。这就是如何获取当前的 AST 节点位置信息的原理。

如何建立 token 索引关联位置 Map 及作用

我们前面已经讲到在使用 SourceCode 类对通过 Espree 生成的 AST 进行二次封装的时候,会把 AST 对象的 body 属性的节点和 tokens 数组建立关联,接下来我们就进行进一步了解。

建立 token 关联 Map 的函数:

function createIndexMap(tokens) {
    const map = Object.create(null);
    let tokenIndex = 0;
    let nextStart = 0;
    let range = null;

    while (tokenIndex < tokens.length) {
        nextStart = Number.MAX_SAFE_INTEGER;
        while (tokenIndex < tokens.length && (range = tokens[tokenIndex].range)[0] < nextStart) {
            map[range[0]] = tokenIndex;
            map[range[1] - 1] = tokenIndex;
            tokenIndex += 1;
        }
    }

    return map;
}

这函数乍眼一看,很难看懂什么意思,因为还需要跟取值的时候的逻辑结合一起才能明白其中的原理。

在 AST 对象的 tokens 数组中每一个标记都有详细位置范围信息,比如下图中红色框的信息则是表示源码中第二行的 var 的标记信息。我们可以看到第二行的 var 的位置范围是:13 ~ 16,那么就记录 13 ~ 16 这个范围的 tokons 数组的下标:4。

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

我们上述规则插件中遍历铺平之后的 AST 数组元素的时候,第二个 var 的 AST 节点则是下面的信息:

Element Plus 组件库相关技术揭秘:3. ESLint 核心原理剖析

我们可以看到这里的位置信息是:13 ~ 25,是第二行整一行的位置范围信息,那么就通过第一个位置信息 13 去前面建立的 tokons 位置与下标的关联 Map 中去查找,那么在关联的 Map 就记录了 13 这个位置的 tokons 下标是 4,那么这样就拿到了第二行 var 这个标记的具体位置范围信息了。

如何修复代码

通过 ESLint 的规则插件我们就可以收集到相关的警报信息和待修改的信息,例如我们本文章中的例子最终会收集到如下信息:

const lintingProblems = [
  {
    message: '不能用var',
    line: 1,
    column: 1,
    nodeType: 'VariableDeclaration',
    endLine: 1,
    endColumn: 13,
    fix: { range: [0,3], text: 'let' }
  },
  {
    message: '不能用var',
    line: 2,
    column: 1,
    nodeType: 'VariableDeclaration',
    endLine: 2,
    endColumn: 13,
    fix: { range: [14,17], text: 'let' }
  }
]

从上述代码我们可以知道警报信息以及代码位置信息,最重要的就是 fix 属性中包含待修改的信息,第一个元素的 fix 属性告诉我们要把位置 0 ~ 3 的地方修改成 let,第二个元素的 fix 属性告诉我们要把位置 14 ~ 17 的地方修改成 let。而这些正是我们预期要达到的目标,接下来我们看看 ESLint 中是如何修改源码的。

ESLint 中是修复源码的函数:

let lastPos = Number.NEGATIVE_INFINITY
let output = ''
// 修复替换函数
function attemptFix(problem) {
    const fix = problem.fix;
    const start = fix.range[0];
    const end = fix.range[1];
    // 使用 slice 函数进行截取替换,高端的程序往往采取最朴素的方法
    output += text.slice(Math.max(0, lastPos), Math.max(0, start));
    output += fix.text;
    lastPos = end;
}
// 循环收集到要修复的问题数组
for (const problem of lintingProblems) {
    attemptFix(problem);
}
// 最后还需要进行一个收尾截取,截取剩下的内容
output += text.slice(Math.max(0, lastPos));

最后把修复后的内容再输出覆盖原来的文件内容即可。

至此 ESLint 的核心原理就剖析完毕了。

总结

ESLint 的基本原理

通过编译器将源码编译成 AST 之后为了方便读取 AST 的数据,又对 AST 进行了一次封装处理。其中主要就是创建 token 索引关联位置 Map,这样后续可以通过该 Map 快捷找到对应的 token,其次就是提供各种 API 方法读取 AST 的节点信息。

再通过规则插件定义要检查的格式内容或者逻辑错误或者要修复的内容,然后将 AST 对象的节点进行平铺,组成一个一维的 AST 节点数组,再遍历 AST 节点数组找到对应节点,然后通过对应节点上的位置信息,根据 token 索引关联位置 Map 就可以知道具体的节点的 token 下标,然后就可以拿到具体的字符 token 信息,在具体字符 token 中记录了该字符的位置信息。

那么就可以知道什么位置的什么字符有什么问题;同样也知道了要修复的位置和要修复的内容,再去到具体的源码中进行替换,再把替换之后的内容输出覆盖原来的文件内容即可。

ESLint 中的 AST 作用

传统的编译原理就是将源码通过词法分析和语法分析得到源码 AST,第二阶段就是将源码 AST 转换(transform)成目标代码 AST,最后根据目标代码 AST 生成目标代码。ESLint 中也运用了编译技术,但主要是为了生成 AST 得到源码中的各个字符的位置信息,方便通过 AST 进行精准查找相关字符,和得到相关字符的在源码的位置信息。ESLint 中不存在传统编译原理的第二阶段的部分操作,ESLint 中的修改是通过相关字符的位置信息直接在源码的基础上进行替换修改。

本文主要讲解了 ESLint 的核心原理,而关于更多 ESLint 原理知识以及 Element Plus 组件库中的 ESLint 部分操作实践,将在下篇文章《ESLint 技术原理与实战及代码规范自动化详解》中进行讨论。

欢迎关注本专栏了解更多 Element Plus 组件库的相关技术。

本专栏的其他文章:

本专栏文章:

1. Vue3 组件库的设计和实现原理

2. 组件库工程化实战之 Monorepo 架构搭建

3. ESLint 核心原理剖析

4. ESLint 技术原理与实战及代码规范自动化详解

5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

6. CSS 架构模式之 BEM 在组件库中的实践

转载自:https://juejin.cn/post/7153659360177029150
评论
请登录