typescript-eslint 的 eslint-scope 兼容测试套件:scope-manager 回归保障与源码剖析
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
本指南以packages/scope-manager/tests/eslint-scope/目录下的测试套件为核心,讲解 typescript-eslint 如何移植并持续维护 eslint-scope 的测试用例,以保障@typescript-eslint/scope-manager在作用域分析能力上与上游保持行为兼容。读完你将掌握该测试套件的结构、运行方式、断言模式,以及analyze/Referencer/ScopeManager背后的作用域构建原理,并能在自己的规则开发或二次移植中复用这套测试方法论。
这套测试从哪里来:移植自 eslint-scope 的回归保障
关联文档 开篇即点明这些测试的来源:它们全部取自 ESLint 官方的eslint-scope包,意图是帮助 typescript-eslint 团队“确保相对原包不产生功能回归(do not regress functionality)”。
从源码看,scope-manager 包(npm 包名@typescript-eslint/scope-manager)本身就是 typescript-eslint 对eslint-scope作用域分析能力的 TypeScript 重实现:它接收@typescript-eslint/typescript-estree解析出的 AST,产出与 eslint-scope 同构的ScopeManager。正因二者承担相同职责,直接移植上游测试集就成了最经济的兼容性验证手段——原包的测试覆盖了二十余个作用域语义场景,几乎全部可以原样复用。
这些测试并非简单拷贝,而是做了三处本地化改造(README 原文所列):
- 用 TypeScript 编写:所有测试文件均为
.test.ts,可直接被 vitest 运行; - 适配本仓库目录结构:通过
../../src/index.js引入ScopeType等内部类型,通过../test-utils/index.js引入辅助工具; - 遵循本仓库的格式化与 lint 风格:例如统一使用带
.js后缀的 ESM 导入路径。
其上游基线是eslint-scope仓库中dbddf14d5771b21b5da704213e4508c660ca1c64这个 commit 下的tests/目录。这意味着,该套件守护的兼容性边界是“这一历史基线所定义的行为”——后续若想跟进上游新增的测试,可以按同一思路从该基线向后移植。
测试套件总览:从 es6 语义到 TypeScript 扩展
packages/scope-manager/tests/eslint-scope/目录下共有 31 个测试文件,覆盖范围非常广,可归纳为几大类。
ES6 / ES2015 新语义是其中体量最大的部分:
es6-block-scope.test.ts:块级作用域中let的物化(materialization)、let不可提升语义;es6-arrow-function-expression.test.ts:箭头函数的函数作用域、参数绑定与arguments缺失;es6-class.test.ts:类声明/表达式的 class 作用域、构造器、计算属性键;es6-import.test.ts与es6-export.test.ts:模块作用域下的 import/export 绑定;es6-default-parameters.test.ts、es6-rest-args.test.ts、es6-destructuring-assignments.test.ts:默认参数、剩余参数、解构赋值的作用域行为;es6-catch.test.ts、es6-switch.test.ts、es6-iteration-scope.test.ts、es6-for-in/for-of相关:catch参数、switch、循环体各自的作用域隔离;es6-new-target.test.ts、es6-super.test.ts、es6-object.test.ts、es6-template-literal.test.ts:new.target、super、对象简写与模板字面量中的引用解析。
作用域细节语义包括:function-expression-name.test.ts(具名函数表达式名的自引用作用域)、label.test.ts(标签语句)、with-scope.test.ts(with作用域)、arguments.test.ts、references.test.ts(引用解析细节)、get-declared-variables.test.ts(获取声明变量)。
全局与严格模式方面有:global-return.test.ts(globalReturn选项)、global-increment.test.ts、implied-strict.test.ts(impliedStrict选项)、use-strict-directive.test.ts("use strict"指令)、implicit-global-reference.test.ts(隐式全局引用)、add-globals.test.ts(注入全局变量)、child-visitor-keys.test.ts(自定义 visitor keys 的能力)。
TypeScript 专属扩展:typescript.test.ts专门验证 scope-manager 相对纯 JS 场景的增量能力——例如 TS 重载声明TSDeclareFunction是否同样为每个重载签名创建函数作用域。
此外class-fields.test.ts覆盖类字段,catch-scope.test.ts覆盖catch子句作用域。这些文件共同构成了一个高密度的作用域语义“行为契约”。
测试运行与工具链:parseAndAnalyze 是怎么工作的
运行入口
整个仓库使用 vitest 作为测试框架,测试配置在 vitest.config.mts 中定义。你可以进入packages/scope-manager目录后运行:
# 运行 scope-manager 全部测试 npx vitest run # 只运行 eslint-scope 移植套件 npx vitest run tests/eslint-scope # 或从仓库根目录借助 pnpm workspace 运行 pnpm --filter @typescript-eslint/scope-manager test核心工具:parseAndAnalyze
绝大多数测试都复用同一个工具函数parseAndAnalyze,其实现位于 test-utils/parse.ts。它的工作流清晰体现了作用域分析的两段式管线:
- 用
@typescript-eslint/typescript-estree的tseslint.parse把代码解析为 TS-ESTree AST,解析选项强制开启range: true(分析器需要 range 信息); - 把 AST 交给 scope-manager 的
analyze函数生成ScopeManager,并默认传入lib: []——不注入任何 TS lib 全局类型变量,避免污染测试断言。
export function parseAndAnalyze( code: string, sourceType: SourceType, ): ParseAndAnalyze; export function parseAndAnalyze( code: string, analyzeOptions?: AnalyzeOptions, parserOptions?: tseslint.TSESTreeOptions, ): ParseAndAnalyze;注意第二个参数是重载的:既可以传'script'/'module'这样的源码类型字符串(会被包装成{ sourceType }),也可以直接传完整的AnalyzeOptions。第三个参数则透传给解析器。两个默认值分别对应DEFAULT_ANALYZE_OPTIONS = { lib: [] }和DEFAULT_PARSER_OPTIONS = { range: true }。
另一个辅助工具是getRealVariables(见 test-utils/misc.ts),它用instanceof ImplicitLibVariable过滤掉隐式库变量,让断言只关注源码中真实声明的变量——配合lib: []双保险地确保测试的可预测性。
断言模式剖析:用 scopeManager.scopes 验证作用域结构
移植测试的核心断言模式,是直接检查scopeManager.scopes数组中每个作用域的类型、绑定变量与引用,再配合自定义匹配器assert.isScopeOfType(定义于 custom-matchers)断言作用域类型。以es6-arrow-function-expression.test.ts为例:
const { scopeManager } = parseAndAnalyze(` var arrow = () => { let i = 0; var j = 20; console.log(i); } `); expect(scopeManager.scopes).toHaveLength(2); // scope[0] 是全局作用域 assert.isScopeOfType(scope, ScopeType.global); expect(scope.block.type).toBe(AST_NODE_TYPES.Program); expect(scope.isStrict).toBe(false); expect(variables).toHaveLength(1); // 只有 arrow // scope[1] 是箭头函数作用域 assert.isScopeOfType(scope, ScopeType.function); expect(scope.block.type).toBe(AST_NODE_TYPES.ArrowFunctionExpression); expect(variables).toHaveLength(2); // i 与 j // 关键断言:箭头函数没有独立的 arguments 绑定 expect(variables[0].name).toBe('i'); expect(variables[1].name).toBe('j');这段测试同时验证了三点:箭头函数创建ScopeType.function作用域、块内let与var都在其中物化、以及箭头函数“没有自己的arguments”。ScopeType的完整枚举可在 src/scope/ScopeType.ts 看到,除block、catch、class、function、global、module、with等 JS 经典类型外,还扩展了classFieldInitializer、classStaticBlock、tsEnum、tsModule、conditionalType、mappedType、functionType、type等 TS 专属类型。
严格模式传播也是高频断言点:es6-arrow-function-expression.test.ts中分别验证了“继承上层严格性”(外层"use strict"让箭头函数isStrict: true)与“函数内"use strict"指令自身生效”两种情况。
典型场景逐例拆解
块级作用域与 let 不可提升(es6-block-scope.test.ts)
parseAndAnalyze(` var i = 42; { i; // 运行时 ReferenceError let i = 20; i; } `);断言块内 3 个i引用全部resolved到块级作用域的i变量,而不是外层var i。这精确刻画了 TDZ 语义在作用域模型中的表达:let声明“遮蔽”外部同名变量,即便在声明语句之前出现的引用也解析到本块内的let。更复杂的嵌套版本(第 92-155 行)通过三层{}验证了同名let的层层遮蔽,每个引用都严格resolved到最近一层块的变量——这是验证Referencer解析顺序正确性的典型用例。
类作用域(es6-class.test.ts)
class Derived extends Base { constructor() {} } new Derived();class关键字创建一个独立的ScopeType.class作用域:全局作用域中有Derived变量,class 作用域内部捕获对Base的引用(extends子句在 class 作用域内求值),构造器则创建嵌套的函数作用域。类体默认严格模式(scope.isStrict为true)。es6-class.test.ts还专门覆盖了匿名类表达式、计算属性键[yuyushiki]() {}对闭包变量的引用,以及regression #49这一历史回归用例。
模块作用域与 import 绑定(es6-import.test.ts)
parseAndAnalyze('import v from "mod";', 'module');sourceType: 'module'会产生一个ScopeType.module作用域且isStrict: true。导入的名字以DefinitionType.ImportBinding定义类型挂到该作用域的变量上;默认导入、命名空间导入import * as ns、命名导入import {x}、重命名导入import {x as v}全部覆盖。最后的“引用导入”用例把三种 import 形式与const x = v;组合,验证引用正确resolved到import变量。
TypeScript 专属扩展(typescript.test.ts)
function foo(bar: number): number; function foo(bar: string): string; function foo(bar: string | number): string | number { return bar; }断言:全局作用域中foo变量有3 个 defs(两个重载 + 一个实现);从scopeManager.scopes[1]到[3]依次是三个ScopeType.function作用域;其中TSDeclareFunction(重载声明)作用域没有引用,而实现体的函数作用域有 1 个引用。这体现了 scope-manager 对 TS 重载的建模:每个声明签名都独立物化为函数作用域。
其余语义类测试一览
es6-catch.test.ts/catch-scope.test.ts:catch (e)参数存在于独立的ScopeType.catch作用域,块内同名let会与之隔离;global-return.test.ts:配合AnalyzeOptions.globalReturn: true,全局作用域后会追加一个函数作用域,模拟 Node.js 的模块包装;implied-strict.test.ts:impliedStrict: true使所有作用域继承严格模式,无需逐文件写"use strict";add-globals.test.ts:验证向作用域管理器注入自定义全局变量的能力;child-visitor-keys.test.ts:验证AnalyzeOptions.childVisitorKeys自定义遍历键的能力;function-expression-name.test.ts:具名函数表达式var f = function g() {}中g存在于独立的function-expression-name作用域。
源码级原理:analyze 如何把这些断言变成现实
parseAndAnalyze最终调用的是 src/analyze.ts 中导出的analyze。其AnalyzeOptions接口完整列出了与测试相关的配置项(src/analyze.ts),默认值如下(src/analyze.ts):
const DEFAULT_OPTIONS: Required<AnalyzeOptions> = { childVisitorKeys: visitorKeys, // 来自 @typescript-eslint/visitor-keys emitDecoratorMetadata: false, globalReturn: false, impliedStrict: false, jsxFragmentName: null, jsxPragma: 'React', lib: ['es2018'], sourceType: 'script', };其中visitorKeys由@typescript-eslint/visitor-keys包提供,测试中child-visitor-keys.test.ts正是围绕替换这套键表展开的。
analyze的内部管线(src/analyze.ts)大致是:
- 创建
ScopeManager(全局作用域先行,globalReturn: true时额外追加函数作用域); - 若配置了
lib,注入来自 TS lib 的ImplicitLibVariable(这也是getRealVariables必须过滤它们的原因); - 实例化
Referencer(见 src/referencer,其中Referencer.ts是遍历入口、patternVisitor处理各种声明模式); - 按
childVisitorKeys对 AST 做深度优先遍历,遇到声明即在当前作用域define变量,遇到标识符即建立引用并尝试resolve到声明变量; - 遍历完成、
currentScope归零后,把ScopeManager返回给调用方。
因此,测试中对scope.references[i].resolved的断言,本质上是在验证Referencer的引用解析算法是否把每个标识符精确绑定到“词法上最近”的变量——这正是 scope-manager 提供给 ESLint 规则(如no-use-before-define、no-shadow这类依赖变量解析的规则)的关键能力。测试套件守护的兼容性,直接决定了基于它的 ESLint 规则在上游 eslint-scope 与 typescript-eslint 之间能否产生一致的结果。
复用这套测试方法论
即便你不直接修改本仓库,这套测试设计也值得借鉴:
- 移植上游测试作为兼容性契约:用“同一批测试输入 + 相同断言”锁定重实现与上游的行为一致,是替换核心库时成本最低的回归保障手段;
- 用
lib: []+ 过滤隐式变量的双保险保证断言纯粹:让测试只反映源码语义,不受环境库影响; - 重载式工具函数:
parseAndAnalyze的第二参数既接受sourceType字符串又接受完整AnalyzeOptions,把“快速用例”与“精细配置用例”统一到一个入口,值得在测试基建中推广; - 按语义维度拆分测试文件:每个文件只聚焦一种作用域语义(块、类、catch、模块……),失败时能立刻定位到具体能力面。
如果你想验证某个 ESLint 规则在 scope-manager 下的解析行为,也可以直接在packages/scope-manager/tests/eslint-scope/下参考既有用例,用parseAndAnalyze写出最小复现——它能让你在不启动完整 ESLint 的情况下,单独观察 AST 与作用域结构。
结语
packages/scope-manager/tests/eslint-scope/这套从eslint-scope移植、以 TypeScript 重写并持续维护的测试套件,是@typescript-eslint/scope-manager兼容性承诺的基石:它把上游三十余个文件所覆盖的 ES6 新语义、严格模式、全局与模块作用域乃至 TypeScript 重载等行为,逐一固化为可自动执行的断言。理解了它的来源、工具链与断言模式,也就理解了 scope-manager 的作用域建模方式,以及 typescript-eslint 如何在重写核心解析能力的同时,保住与 ESLint 生态的行为一致性。
【免费下载链接】typescript-eslint:sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript项目地址: https://gitcode.com/GitHub_Trending/ty/typescript-eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考