深度解析:Scope、Variable、Reference 与 Definition 完全指南)
ESLint 作用域管理接口ScopeManager深度解析Scope、Variable、Reference 与 Definition 完全指南【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslintScopeManager作用域管理器是 ESLint 静态分析体系的中枢组件它由解析器生成负责收集一份 JavaScript 源码中所有变量的作用域、所有标识符引用以及变量定义信息是no-unused-vars、no-shadow、no-use-before-define等大量核心规则判断“变量是否被使用、是否被遮蔽、是否越界访问”的事实基础。本文以 docs/src/extend/scope-manager-interface.md 为骨架结合当前仓库中的类型声明lib/types/index.d.ts与运行时调用链lib/linter/linter.js、lib/languages/js/source-code/source-code.js逐字段、逐方法讲解ScopeManager、Scope、Variable、Reference、Definition五大接口并说明哪些成员是 ESLint 实际使用、哪些已被弃用。读完本文你将能读懂 ESLint 内置规则对作用域信息的消费方式并能在自定义规则中正确利用context.sourceCode.getScope()与getDeclaredVariables()完成变量级分析。一、ScopeManager 接口所有作用域的容器ScopeManager对象持有一次分析得到的所有变量作用域。ESLint 通过 lib/linter/linter.js 中的analyzeScope()函数调用eslint-scope的analyze()生成它function analyzeScope(ast, languageOptions, visitorKeys) { const parserOptions languageOptions.parserOptions; const ecmaFeatures parserOptions.ecmaFeatures || {}; const ecmaVersion languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION; return eslintScope.analyze(ast, { ignoreEval: true, nodejsScope: ecmaFeatures.globalReturn, impliedStrict: ecmaFeatures.impliedStrict, ecmaVersion: typeof ecmaVersion number ? ecmaVersion : 6, sourceType: languageOptions.sourceType || script, childVisitorKeys: visitorKeys || evk.KEYS, fallback: Traverser.getKeys, jsx: ecmaFeatures.jsx, }); }从这里可以看出 ESLint 与作用域分析的两个关键约定依赖eslint-scope当前仓库在 package.json 中声明eslint-scope: ^9.1.2。本文档正是基于eslint-scopeescope的一个 fork的实现编写并弃用了 ESLint 并未使用的部分成员。分析参数决定语义ignoreEval: true表示不把eval调用视为会动态引入变量nodejsScope来自ecmaFeatures.globalReturnNode.js 环境下允许顶层returnimpliedStrict对应隐式严格模式sourceType决定是script还是module。ScopeManager只有两个公开字段、三个可用的方法字段字段类型说明scopesScope[]所有作用域对象globalScopeScope根作用域全局作用域方法acquire(node, inner false)参数nodeASTNode要获取作用域的 AST 节点。innerboolean默认false。当一个节点拥有多个作用域时默认返回最外层作用域设为true则返回最内层作用域。返回Scope | null说明获取给定 AST 节点的作用域。返回的作用域其block属性就是该节点。此方法永远不会返回function-expression-name作用域若该节点没有对应作用域则返回null。ESLint 的SourceCode.getScope()正是围绕acquire构建的见 lib/languages/js/source-code/source-code.jsgetScope(currentNode) { if (!currentNode) { throw new TypeError(Missing required argument: node.); } // check cache first const cache this[caches].get(scopes); const cachedScope cache.get(currentNode); if (cachedScope) { return cachedScope; } // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. const inner currentNode.type ! Program; for (let node currentNode; node; node node.parent) { const scope this.scopeManager.acquire(node, inner); if (scope) { if (scope.type function-expression-name) { cache.set(currentNode, scope.childScopes[0]); return scope.childScopes[0]; } cache.set(currentNode, scope); return scope; } } cache.set(currentNode, this.scopeManager.scopes[0]); return this.scopeManager.scopes[0]; }几个值得注意的实现细节对Program节点强制使用最外层作用域inner false以避免误返回 Node.js 特殊函数作用域或 ES 模块作用域。若acquire返回了function-expression-name作用域getScope会直接跳过它并返回其第一个子作用域——这与文档中“acquire永不返回function-expression-name作用域”的语义保持一致因为对规则而言具名函数表达式的名字绑定如const f function g() {}中的g通常不是规则关心的“当前代码所处作用域”。若向上遍历所有祖先都找不到作用域兜底返回globalScope即scopeManager.scopes[0]。addGlobals(names)参数namesstring[]——要添加到全局作用域的变量名列表。返回undefined说明向全局作用域添加变量并解析指向这些变量的引用。该方法仅供 ESLint 核心使用规则中绝不应调用它。ESLint 在解析完源码后会根据语言选项ecmaVersion、配置的globals、环境变量等把内置全局变量注入作用域。例如 lib/languages/js/source-code/source-code.js 中解析配置后调用scopeManager.addGlobals(names);随后在 lib/linter/linter.js 中通过sourceCode.applyLanguageOptions?.(languageOptions)统一把ecmaVersion与配置的 globals 落入全局作用域。这也解释了为什么no-undef规则能知道console、window等内置全局是“已声明”的——它们是在分析阶段通过addGlobals显式注册进全局作用域的。getDeclaredVariables(node)参数nodeASTNode——要获取变量的 AST 节点。返回Variable[]说明获取给定 AST 节点声明的变量。返回的变量其def[].node/def[].parent属性即该节点若节点未声明任何变量返回空数组。SourceCode.getDeclaredVariables()直接透传给此方法lib/languages/js/source-code/source-code.jsgetDeclaredVariables(node) { return this.scopeManager.getDeclaredVariables(node); }no-unused-vars规则用它来枚举函数参数、解构绑定等声明见 lib/rules/no-unused-vars.jsconst params sourceCode.getDeclaredVariables(def.node);已弃用成员以下成员在eslint-scope中仍然存在但 ESLint 并未使用本文档对其做了弃用标注成员返回类型说明isModule()boolean程序是否为 ES 模块isImpliedStrict()boolean程序是否为隐式严格模式即options.impliedStrict trueisStrictModeSupported()boolean程序是否支持严格模式即options.ecmaVersion 5acquireAll(node)Scope[] \| null获取给定节点的全部作用域其block属性为该节点节点无作用域时返回null二、Scope 接口作用域对象Scope对象承载某个作用域内所有变量与所有引用。ESLint 的类型声明lib/types/index.d.ts与文档保持一致。字段type类型string说明作用域类型取值为block、catch、class、class-field-initializer、class-static-block、for、function、function-expression-name、global、module、switch、with之一。注意类型清单中包含class-field-initializer类字段初始化器与class-static-block类静态块——这两者都被视为隐式函数作用域因为类字段初始化器与静态块内部的var声明不会泄漏到外层。isStrict类型boolean说明该作用域是否处于严格模式。upper类型Scope | null说明父作用域。全局作用域的upper为null。作用域链表scope.upper逐级上溯是变量名解析与markVariableAsUsed的基础遍历路径见 lib/languages/js/source-code/source-code.js。childScopes类型Scope[]说明直接子作用域数组不包含孙作用域。variableScope类型Scope说明最近的、类型为class-field-initializer、class-static-block、function、global或module的祖先对于上述类型的作用域自身该属性是自引用。文档特别强调这代表“最低层级的封闭函数或顶层作用域”。类字段初始化器与类静态块是隐式函数。从历史上看这就是承载var声明所定义变量的作用域因此得名variableScope。也就是说现代语义中var提升的“归宿”不再局限于函数/顶层还包括类字段初始化器与类静态块这两个隐式函数。block类型ASTNode说明创建该作用域的 AST 节点函数声明对应FunctionDeclaration节点、块级作用域对应BlockStatement节点等等。variables类型Variable[]说明该作用域内定义的全部变量数组不包含子作用域中定义的变量。set类型Mapstring, Variable说明变量名到Variable对象的映射表。这是作用域内按名查变量的主入口。SourceCode.isGlobalReference()就用scopeManager.scopes[0].set快速判定一个标识符是否指向全局变量见 lib/languages/js/source-code/source-code.js。references类型Reference[]说明该作用域上的全部引用数组不包含子作用域中的引用。through类型Reference[]说明在该作用域内无法解析的引用数组即引用了一个在当前作用域链中找不到声明的名字。no-undef规则的核心逻辑就是对最内层作用域的through引用逐条判断“是否命中已注册全局”。functionExpressionScope类型boolean说明是否为function-expression-name作用域。implicit仅存在于全局作用域该字段只存在于根Scope对象全局作用域上提供隐式全局变量的信息。所谓隐式全局变量是指既非内置、也未被显式声明而是在非严格代码中通过给未声明变量赋值而隐式创建的变量。这些变量的Variable对象不会出现在根作用域的variables与set字段中。implicit字段是一个包含两个属性的对象属性类型说明variablesVariable[]所有隐式全局变量setMapstring, Variable隐式全局变量名到Variable对象的映射::: tip 代表隐式全局变量的Variable对象中references始终是空数组。对这些变量的引用可以在根Scope对象全局作用域的through字段以及其他未解析引用中找到。 :::已弃用成员成员类型说明taintsMapstring, boolean变量名到tainted标志的映射dynamicboolean该作用域是否动态即类型为global或withdirectCallToEvalScopeboolean该作用域是否包含eval()调用thisFoundboolean该作用域是否包含thisresolve(node)Reference \| null返回this.references.find(r r.identifier node)node类型必须为IdentifierisStatic()boolean返回!this.dynamicisArgumentsMaterialized()boolean是否为使用了arguments变量的function作用域isThisMaterialized()boolean返回this.thisFoundisUsedName(name)boolean给定名字是否在变量名或引用名中被使用这些成员在eslint-scope中定义但未被 ESLint 消费规则作者无需依赖它们。三、Variable 接口变量的信息对象Variable对象描述一个变量的完整信息。字段字段类型说明namestring变量名scopeScope定义该变量的作用域identifiersASTNode[]定义该变量的Identifier节点数组若变量被重复声明数组会包含两个及以上节点referencesReference[]指向该变量的引用数组defsDefinition[]该变量的定义数组在no-unused-vars的实现中lib/rules/no-unused-vars.js规则遍历scope.variables把variable.references为空且未被markVariableAsUsed()标记过的变量判定为“未使用”const variables scope.variables; // ... // skip variables marked with markVariableAsUsed()markVariableAsUsed(name, refNode)lib/languages/js/source-code/source-code.js从引用节点所在作用域开始沿upper链逐级查找同名变量找到后置variable.eslintUsed true模块环境下会先从全局作用域跳到block this.ast的顶层子作用域再开始搜索避免漏掉 ESM 的模块作用域与 CommonJS 的外层函数作用域中var声明的变量。这正是/* exported foo */等注释机制能让no-unused-vars放行变量的底层原理。已弃用成员成员类型说明taintedbooleantainted标志恒为falsestackbooleanstack标志原文档也注明其含义不明确四、Reference 接口引用的信息对象Reference对象描述一次标识符“读取/写入”的引用。字段字段类型说明identifierASTNode该引用的Identifier或JSXIdentifier节点fromScope该引用所在的作用域对象resolvedVariable \| null该引用指向的Variable对象若该变量未被定义则为nullwriteExprASTNode \| null赋值的右侧表达式节点initboolean该写引用是否为变量初始化或默认值方法方法返回类型说明isWrite()boolean该引用是否为写引用isRead()boolean该引用是否为读引用isWriteOnly()boolean是写引用但不是读引用isReadOnly()boolean是读引用但不是写引用isReadWrite()boolean既是读引用又是写引用如x、x 1读/写分类是no-unused-expressions、no-self-assign等规则判断“赋值是否产生副作用”的判定基础例如x x中引用同时是读与写而x 1则是纯写引用。已弃用成员成员类型说明taintedbooleantainted标志恒为falseflagnumber1为读2为写3为读写partialbooleanpartial标志isStatic()boolean该引用是否被静态解析isStatic()对应现代实现中resolved字段静态解析成功即非nullflag的数值语义已被isRead()/isWrite()等方法取代。五、Definition 接口变量定义的信息对象Definition对象描述“一个变量是如何被定义的”。Variable.defs数组中每个元素的node与parent指向定义该变量的 AST 节点——这也是getDeclaredVariables(node)返回值“def[].node/def[].parent即为传入节点”这一契约的来源。字段type类型string说明定义类型为CatchClause、ClassName、FunctionName、ImplicitGlobalVariable、ImportBinding、Parameter、Variable之一。name类型ASTNode说明该定义的Identifier节点。node类型ASTNode说明名字的外层包络节点。不同类型对应的node如下typenodeCatchClauseCatchClauseClassNameClassDeclaration或ClassExpressionFunctionNameFunctionDeclaration或FunctionExpressionImplicitGlobalVariableAssignmentExpression、ForInStatement或ForOfStatementImportBindingImportSpecifier、ImportDefaultSpecifier或ImportNamespaceSpecifierParameterFunctionDeclaration、FunctionExpression或ArrowFunctionExpressionVariableVariableDeclaratorparent类型ASTNode | undefined | null说明名字的外层语句节点。不同类型对应的parent如下typeparentCatchClausenullClassNamenullFunctionNamenullImplicitGlobalVariablenullImportBindingImportDeclarationParameternullVariableVariableDeclaration值得注意只有ImportBinding与Variable两种定义类型拥有非null的parentImplicitGlobalVariable的node指向产生隐式全局变量的赋值/for-in/for-of 语句与第一节中implicit字段隐式全局变量的概念直接对应。已弃用成员成员类型说明indexnumber \| undefined \| null在声明语句中的索引kindstring \| undefined \| null声明语句的种类如var/let/const六、在自定义规则中消费作用域信息综合以上接口一个典型的变量分析型自定义规则会这样工作通过context.sourceCode.getScope(node)获取当前节点所在作用域内部走ScopeManager.acquire通过context.sourceCode.getDeclaredVariables(node)获取某节点声明的变量内部走ScopeManager.getDeclaredVariables遍历scope.variables/scope.set枚举作用域内声明遍历scope.references/scope.through追踪引用与未解析引用对Variable检查references与defs对Reference检查isWrite()/isRead()/isWriteOnly()等方法与resolved指向。这两条SourceCode方法是最安全、最受支持的入口因为ScopeManager自身及其大部分成员属于eslint-scope的实现细节——这正是本文档将isModule、taints、resolve等成员标注为“deprecatedESLint 未使用”的原因。规则作者应只依赖文档明确列出的核心字段与方法避免触碰弃用成员。小结ScopeManager是作用域容器的根scopes保存全部作用域globalScope是根acquire()按节点查作用域inner控制内外层getDeclaredVariables()按节点查变量addGlobals()仅供 ESLint 核心注入全局。Scope通过type区分 12 种作用域upper/childScopes构成作用域树variables/set是本层声明references/through是本层引用与未解析引用全局作用域独有的implicit字段承载隐式全局变量。Variable汇总变量名、定义作用域、标识符、引用与定义配合markVariableAsUsed()支撑no-unused-vars的“已使用”判定。Reference描述一次读写isWrite()/isRead()/isWriteOnly()/isReadOnly()/isReadWrite()精确刻画引用性质。Definition描述变量定义方式node/parent两张映射表明确了每种定义类型对应的 AST 节点形态。要深入阅读第一手实现建议继续查看 lib/linter/linter.js作用域分析入口、lib/languages/js/source-code/source-code.jsSourceCode对作用域接口的封装以及 lib/types/index.d.ts官方类型声明是文档接口契约的代码级印证。【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考