ty 类型检查器中的 `unresolved-global` 规则:为何 `global` 语句必须在模块级有明确定义
2026/9/10 6:46:39 网站建设 项目流程

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 reference

f()可能从未被调用,也可能在g()之前或之后执行。对静态分析工具而言,这意味着:

  1. 无法确定xg()执行时是否已被绑定(boundness 无法推断);
  2. 无法确定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 可以据此推断xint类型,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_boundis_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 ty's 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 scope"

x(已绑定)、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语句"声明"的名字在模块级无定义。它关注声明点,级别为warn
  • invalid-syntax(语义语法错误):同名在同一个作用域内既被使用又被global声明、或globalnonlocal冲突,属于更底层的语法级错误(见 global.md 中 "Semantic syntax errors" 一节的大量用例),与unresolved-global的检查阶段不同。

实践要点小结

  1. global x之前,先检查模块顶部是否有x = ...x: T;两者满足其一即可通过检查,否则会得到warn级别的unresolved-global提示;
  2. __file____name__等隐式模块属性可以合法地写global,但intlen等内建名不行——后者会遮蔽内建表,规则会如实报错;
  3. global语句与类型系统兼容:赋值后的局部收窄、条件分支下的类型联合(如Literal[1, 2])在测试中均有验证,正确声明的全局变量可以参与完整的类型推断;
  4. 该规则自 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询