ty 类型检查器中的函数装饰器推断循环:mdtest 回归测试 #3593 深入解读
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
导读
在 Rust 仓库 ruff 中,crates/ty_python_semantic承载着新一代类型检查器 ty 的类型推断核心。本文围绕 regression/3593_function_known_decorators_cycle.md 这一回归测试文档展开,剖析"函数装饰器推断循环"(Function decorator inference cycle)这一底层问题:当装饰器识别、前向引用与描述符协议互相纠缠时,Salsa 增量查询如何避免死循环并给出正确的诊断与退化类型。读完本文,你将掌握 mdtest 测试格式的读写方法、ty 中装饰器推断查询的设计意图,以及如何亲手运行这一回归测试验证行为。
关联文档在仓库中的角色
该文档位于 crates/ty_python_semantic/resources/mdtest/regression/,是 ty 类型检查器的基于 Markdown 的测试(mdtest)套件中的一份回归测试。按 resources/README.md 的说明:
Markdown files within the
mdtest/subdirectory are tests of type inference and type checking; executed by thetests/mdtest.rsintegration test.
即resources/mdtest/下的每一个 Markdown 文件都是一个类型推断/类型检查测试用例,由 tests/mdtest.rs 集成测试执行;该测试通过datatest_stable::harness!以r"\.md$"模式扫描整个resources/mdtest目录(见 tests/mdtest.rs)。与之并列的还有resources/lint_docs/(lint 规则文档测试)与snapshots/目录(错误消息快照)。
这份回归测试专门针对 ty 仓库的#3593 号 issue,用最小化代码复现并固化"函数装饰器推断中出现循环依赖"时的类型检查行为。它验证两件事:一是推断过程能够收敛(不 panic、不无限递归、不触发 Salsa 循环死锁),二是收敛后产出的诊断与类型符合预期。
mdtest 回归测试格式速览
mdtest 文件的核心格式是:一个可选的 TOML 环境配置块 + 若干 Python(或.pyi、.ipynb、.toml)代码块,代码块内通过注释声明预期诊断。本文件完整呈现了这一格式:
[environment] python-version = "3.14"[environment]表用于配置被测代码的运行环境,此处声明python-version = "3.14",使测试在最新的 Python 语义(如typing.Self、@overload等特性)下运行。除 Python 版本外,该格式还支持其他环境选项,可参考 mdtest_config.md 与 ruff.toml。
代码块内的错误标注语法为:
# error: [错误代码]紧随其后的语句行必须产生对应错误代码的诊断;reveal_type(...)则通过# revealed: ...声明期望推断出的类型。实际执行时,crates/ruff_mdtest/src/lib.rs 会把 Markdown 中提取的代码写入内存文件系统(/src根目录),逐个代码块运行类型检查器,再交给matcher逐行匹配错误标注与内联快照。python-version会被解析进 linter 配置,用于决定解析器版本(见 crates/ruff_mdtest/src/lib.rs)。
回归测试代码逐段解读
原文测试代码共三段逻辑,恰好覆盖循环推断的三个侧面。
前向引用属性与无效属性访问
from typing import Self, overload, reveal_type class C: a: D # error: [invalid-attribute-access] C.a # error: [invalid-attribute-access] reveal_type(C().a) # revealed: Unknown | D类C声明了注解属性a: D,其中D是前向引用——它在文件更下方才被定义。类型检查器在推断C.a的类型时,必须解析D;而解析D又会触及其函数定义(含装饰器)的推断,由此埋下循环的种子。
由于C.a只有注解、没有类体赋值,运行时该属性并不真实存在,因此访问C.a与C().a都会报出invalid-attribute-access错误。值得注意的是reveal_type(C().a)的期望结果:Unknown | D。可以这样理解:在循环尚未完全收敛时,a的推断类型有一部分退化为Unknown(未知),检查器将退化结果与声明的D类型取并集,而不是直接报错或返回单一类型——这正是"循环恢复"(cycle recovery)机制的直观体现,与 cycle/basic.md 中多处 "Divergent"/"Unknown" 退化约定一致。
装饰器推断循环的核心:@overload描述符方法
class D: @overload # error: [invalid-overload] # error: [invalid-overload] def __get__() -> Self: pass类D定义了一个__get__方法,且用@overload标注。__get__是描述符协议(descriptor protocol)的方法:定义了__get__的属性值即非数据描述符,会在属性访问时被调用(描述符的完整语义可见 descriptor_protocol.md,其中展示了__get__/__set__/__delete__的优先级链与重载区分实例/类访问的例子)。
这里的__get__声明显然是不完整的:
- 它没有参数(真正的描述符
__get__至少应接收instance与owner); - 它只有
@overload声明而没有对应的实现; - 返回类型
Self需要解析"当前类"的类型,这本身又依赖D的类推断。
因此检查器对这两处缺陷分别报出invalid-overload错误(注释中连写两个# error:)。结合文件名中的 "function_known_decorators" 可以推断:@overload属于"已知装饰器"(known decorator),检查器在识别装饰器时需要查询其类型与标志,而这个查询正处在D自身定义推断的依赖环上——这就是本回归测试要固化的"函数装饰器推断循环"。
循环从何而来:function_known_decorators查询
理解这份回归测试,关键在于 crates/ty_python_semantic/src/types/infer.rs 中的function_known_decorators查询。它的注释直接点明了设计动机:
Infer decorator expression types for a function definition. This is a lightweight query that avoids the cycle risk of calling
infer_definition_typeswhen we need to check decorators while already inside definition inference (e.g. checkingSelfin a@staticmethod).
即:在定义推断内部需要检查装饰器时(例如在@staticmethod中检查Self),直接调用完整的infer_definition_types会有 Salsa 循环风险,因此抽出一个"轻量查询"只做装饰器相关推断。该查询被声明为#[salsa::tracked],并显式提供循环恢复值:
#[salsa::tracked( returns(ref), cycle_initial=|_, _, _| FunctionDecoratorInference::default(), ... )] pub(crate) fn function_known_decorators<'db>(...)cycle_initial保证:一旦查询在增量计算中形成环,Salsa 立即以FunctionDecoratorInference::default()作为占位结果返回,而不是死锁或无限递归。FunctionDecoratorInference结构(infer.rs)只保存装饰器表达式类型、绑定、被调用函数、已知装饰器标志与诊断——紧凑且可安全地默认实例化。调用方则通过function_known_decorator_flags读取其中的known_decorators()标志集合。
在 crates/ty_python_semantic/src/types/infer/builder/function.rs 中可以看到该查询的实际消费方式:
- function.rs#L92-L96:方法接收者分类(
MethodReceiverKind)依据function_known_decorator_flags判断是否带@staticmethod/@classmethod; - function.rs#L427-L436:推断函数定义时,若有装饰器列表则调用
function_known_decorators,并把其中的诊断、表达式类型、绑定合并进当前推断上下文; - function.rs#L443-L478:遍历装饰器,通过
FunctionDecorators::from_decorator_type识别@staticmethod、@classmethod、@final、@no_type_check、@abstractmethod等已知装饰器,未识别的则进入通用装饰器应用路径。
由此,本回归测试的意义就落到了实处:当reveal_type(C().a)需要解析D,而解析D又需要识别其@overload装饰器时,function_known_decorators的轻量性与cycle_initial兜底保证了推断能够收敛,最终以Unknown | D的退化并集类型安全返回,同时不吞掉D内部__get__声明的invalid-overload诊断。
更广泛的循环推断场景佐证
"装饰器/定义推断循环"并非孤例。在 cycle/basic.md 中,同属 ty 的 mdtest 套件固化了一整组循环推断问题:
- 自引用装饰函数(对应 ty #4308):
f = lambda: f; assert f后再@property def f(x=lambda: f): ...,解析装饰函数的可调用签名时不能急切推断默认值,否则默认值引用自身会使断言的可达性检查重入,导致推断不收敛; - 自引用函数默认值(对应 ty #1402):参数默认值引用可调用对象自身时,类型回退为
Unknown,防止栈溢出; - 装饰器诊断的自引用(对应 ty #4440):显示装饰器签名可能触发推断自引用默认值,因此错误报告被推迟到函数推断结束之后;
- 递归 lambda、递归增长元组、字面量在循环恢复中的加宽等,共同定义了循环恢复(cycle recovery)的语义边界。
本回归测试(#3593)与这些案例属于同一类设计目标:让 Salsa 增量查询的循环恢复在类型检查层面可预测、可验证。而 descriptor_protocol.md 则为__get__描述符提供了完整的正例(如Ten类通过__get__返回Literal[10]),与本文测试中"不完整__get__+ 循环"的反例形成对照,帮助读者理解描述符在属性访问推断中的角色。
如何运行与验证该测试
方式一:cargo 集成测试
mdtest 由ty_python_semantic的集成测试驱动(tests/mdtest.rs),可用 cargo 过滤运行本用例:
cargo test --package ty_python_semantic --test mdtest 3593_function_known_decorators_cycle方式二:mdtest 专属运行器(推荐)
仓库提供了带 watch 模式的 Markdown 测试运行器 crates/ty_python_semantic/mdtest.py,它基于uv脚本模式(文件头部声明了rich与watchfiles依赖),可精确过滤部分路径:
cd crates/ty_python_semantic uv run mdtest.py regression/3593_function_known_decorators_cycle.md运行器的过滤器参数支持loops/for.md或invalid-argument-type.md这类部分路径(内部会自动去除.md后缀,见 mdtest.py)。该运行器会自动编译cargo test --test=mdtest产物、以--exact mdtest::<相对路径>执行单个用例,并支持--enable-external(外部依赖测试)、--no-lockfile-upgrades、--no-snapshot-updates等开关;不传参数时则进入文件监听模式,任一.rs、.pyi或 mdtest 文档变更都会触发对应重编译与重跑(见 mdtest.py)。在 mdtest.py 中可以看到它同时托管mdtest与lint_doc两套用例,快照统一存放在resources/mdtest/snapshots/下。
若修改了内联快照期望,运行器默认会自动更新过期的快照(INSTA_FORCE_PASS=1、INSTA_OUTPUT=none等环境变量由 mdtest.py 注入)。
小结
3593_function_known_decorators_cycle.md是一份浓缩的"问题档案":它以 mdtest 格式,用最少代码复现了 ty 类型检查器在装饰器识别与前向引用相交织时产生的推断循环,并固化了期望行为——C().a的类型退化为Unknown | D,__get__的不完整@overload声明报出invalid-overload,同时整个推断过程必须收敛。其背后是 function_known_decorators 这一带cycle_initial兜底的轻量 Salsa 查询,以及 cycle/basic.md 中一整套循环恢复语义。对类型检查器开发者而言,这份文档既是理解"装饰器推断为何会成环、如何安全恢复"的最佳入口,也是新增装饰器特性时必须守护的回归基线。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考