ty 类型检查器中的unresolved-global规则为何global语句必须在模块级有明确定义【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本文讲解 ruff 仓库中 ty 类型检查器的unresolved-global诊断规则它检测那些在内部作用域中声明为global、但在模块级全局作用域中既无赋值也无类型声明的变量。读完本文你将理解该规则被触发的原理、标准报错信息与两种修复方式并能结合 ty 的类型推断源码infer_global_statement与官方 mdtest 测试用例掌握该规则的检查逻辑边界——包括隐式全局名如__file__与内建名的区别以及它对*导入导出范围的影响。规则概述unresolved-global检测什么unresolved-global是 ty 内置的一条 lint 规则用于检测在函数等内部作用域中使用global语句声明、但在全局作用域中**没有任何显式绑定binding或声明declaration**的变量。规则文档正文位于 unresolved-global.md其元信息在 diagnostic.rs 中注册declare_lint! { #[doc include_str!(../../resources/lint_docs/unresolved-global.md)] pub(crate) static UNRESOLVED_GLOBAL { summary: detects global statements with no definition in the global scope, status: LintStatus::stable(0.0.1-alpha.15), default_level: Level::Warn, } }可以确认两个关键事实默认级别为warn触发该规则只会产生警告而非错误不会让类型检查整体失败自0.0.1-alpha.15起即为 stable 状态规则行为不会随意变更。生成的诊断文档页面见 rules.md其中同样标注了默认级别warn与引入版本。为什么这是一个问题global语句的语义允许函数体在任意时刻、以任意顺序甚至从不被执行。考虑下面的代码def f(): # unresolved global global x # error x 42 def g(): print(x) # unresolved referencef()可能从未被调用也可能在g()之前或之后执行。对静态分析工具而言这意味着无法确定x在g()执行时是否已被绑定boundness 无法推断无法确定x的类型——如果f()是唯一给x赋值的地方而f()的执行时机未知模块级就缺少一个可供类型推断的锚点。因此一旦允许仅靠函数体内的global语句创造新全局变量这种模式ty 就难以保证对模块级符号的绑定性、类型推断是准确的。这正是规则文档中 Why is this bad 一节的核心论点也是 builder.rs 中实现注释明确写出的设计动机。修复方式显式声明或初始化全局变量规则文档给出了两种标准修复路径核心思想都是在模块级为x提供一个显式的定义让类型检查器有可依赖的声明。方式一在模块级声明类型不初始化x: int def f(): global x x 42 def g(): print(x)x: int是一处无赋值的类型声明declaration它在模块级为x建立了类型锚点。ty 可以据此推断x为int类型global x语句因此合法。方式二声明并同时初始化x: int | None None def f(): global x x 42 def g(): print(x)当全局变量可能尚未被任何函数赋值时使用int | None这样的联合类型更贴合运行时事实g()若先于f()执行x仍是None。官方测试用例 global.md 中的 narrowing 场景展示了这种方式带来的推断能力——在global语句之后对x赋值局部作用域中的类型会随之收窄x: int | None def f(): global x x 1 reveal_type(x) # revealed: Literal[1]也就是说global语句不仅不阻碍分析还会参与局部作用域内的类型收窄narrowing。源码级实现infer_global_statement的三层判断规则的实现入口是类型推断构建器中的infer_global_statement位于 builder.rs。它的注释首先澄清了与 CPython 的语义差异// CPython allows examples like this, where a global variable is never explicitly defined // in the global scope: ... // However, allowing this pattern would make it hard for us to guarantee // accurate analysis about the types and boundness of global-scope symbols, // so we require the variable to be explicitly defined (either bound or declared) // in the global scope.即 CPython 本身允许这种写法但 ty 出于分析准确性要求显式定义。对global语句中的每个名字检查按以下三层顺序进行第一层模块级作用域中是否已绑定或声明。let global_place_table self.index.place_table(FileScopeId::global()); for name in names { if let Some(symbol_id) global_place_table.symbol_id(name) { let symbol global_place_table.symbol(symbol_id); if symbol.is_bound() || symbol.is_declared() { // 该名字已在全局作用域显式定义 continue; } }这里通过place_table索引查询模块级FileScopeId::global()的作用域表只要符号满足is_bound()有赋值或is_declared()有类型声明即通过检查。这解释了前文两种修复方式为何都有效——它们分别满足is_bound与is_declared两个条件之一。第二层是否属于模块的隐式全局符号。if !module_type_implicit_global_symbol(self.db(), self.program_file(), name) .place .is_undefined() { // 该名字是 __file__ 这类隐式全局名但 int 这类内建名不算 continue; }module_type_implicit_global_symbol定义在 place.rs用于识别types.ModuleType上声明的隐式全局符号例如__file__、__name__。官方测试用例明确验证了这一边界def f(): global __file__ # allowed, implicit global global int # error: [unresolved-global] Invalid global declaration of int: int has no declarations or bindings in the global scope注意区分隐式模块属性__file__免检而内建名int不免检——把内建名声明为global恰恰是危险写法因为它会在运行时把内建表遮蔽成模块属性。第三层既未定义也非隐式全局则报告诊断。let Some(builder) self.context.report_lint(UNRESOLVED_GLOBAL, name) else { return; }; let mut diag builder.into_diagnostic(format_args!(Invalid global declaration of {name})); diag.set_primary_annotation_message(format_args!( {name} has no declarations or bindings in the global scope )); diag.info( This limits tys ability to make accurate inferences \ about the boundness and types of global-scope symbols, ); diag.info(format_args!( Consider adding a declaration to the global scope, e.g. {name}: int ));诊断消息结构与规则文档一一对应主消息指出无效的全局声明注解解释该名字在全局作用域中没有声明或绑定两条info则说明危害限制 ty 对全局符号的推断并给出建议修法在模块级加{name}: int这类声明。测试用例规则的判定边界ty 用 mdtest 机制.md中嵌入# error:/reveal_type断言由 mdtest.rs 驱动执行维护该规则的行为契约相关用例集中在 scopes/global.md部分合法、部分报错的混合声明。global语句可以一次声明多个名字检查是逐名独立进行的x 1 y: int # z is neither bound nor declared in the global scope def f(): global x, y, z # error: [unresolved-global] Invalid global declaration of z: z has no declarations or bindings in the global scopex已绑定、y已声明通过只有z报错。与*导入的联动。未解析的global语句不能为模块创造可导出的名字。import/star.md 中的用例表明a.py里通过global g, h创造的全局名会被该规则标记并且不会被from a import *视为a的成员。对已赋值后声明的顺序宽容。测试中还有这样的场景函数内先global x、之后在模块级才出现x 1不报错——因为 ty 对作用域符号的定义判断基于整个作用域表而非源码中的文本先后位置。与其他相关诊断的区分理解unresolved-global时容易与两个相邻概念混淆可以借助源码结构加以区分unresolved-reference未解析引用指读取一个不存在的名字例如前文示例中g()里的print(x)。它关注引用点unresolved-global未解析全局声明指global语句声明的名字在模块级无定义。它关注声明点级别为warninvalid-syntax语义语法错误同名在同一个作用域内既被使用又被global声明、或global与nonlocal冲突属于更底层的语法级错误见 global.md 中 Semantic syntax errors 一节的大量用例与unresolved-global的检查阶段不同。实践要点小结写global x之前先检查模块顶部是否有x ...或x: T两者满足其一即可通过检查否则会得到warn级别的unresolved-global提示__file__、__name__等隐式模块属性可以合法地写global但int、len等内建名不行——后者会遮蔽内建表规则会如实报错global语句与类型系统兼容赋值后的局部收窄、条件分支下的类型联合如Literal[1, 2]在测试中均有验证正确声明的全局变量可以参与完整的类型推断该规则自 ty0.0.1-alpha.15起稳定默认warn若团队希望更严格可在配置中将其提升为error级别规则级别配置机制见 rules.md 的 rule levels 说明。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
