Ruff 类型检查器(ty)legacy 泛型类型参数排序诊断解析:invalid-generic-class 与 invalid_type_parameter_order 测试全解
Ruff 类型检查器tylegacy 泛型类型参数排序诊断解析invalid-generic-class 与 invalid_type_parameter_order 测试全解【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本篇技术指南以 Ruff 仓库中crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md这一 mdtest 诊断测试为骨架深入剖析 tyRuff 内置类型检查器对 legacy 泛型语法中带默认值的类型参数必须位于不带默认值参数之后这一规则的检测实现。读完本文你将掌握 PEP 696 类型参数默认值在Generic/Protocol继承场景下的合法排序规则、mdtest 快照测试的编写与输出格式以及对应invalid-generic-class诊断在源码中的触发链路与消息生成细节。一、背景legacy 泛型语法与类型参数默认值Python 的泛型类存在两套声明语法legacy 语法通过Generic[T]、Protocol[T]作为基类显式订阅类型变量用TypeVar单独声明。这也是typing模块自 PEP 484 以来一贯的写法PEP 695 语法直接使用class C[T]: ...的新式类型参数。PEP 696 为TypeVar增加了default关键字参数允许为类型参数声明默认类型。当类型参数带有默认值时泛型类在特化subscription时可以省略该参数的实参。随之而来的是一条硬性约束带默认值的类型参数不能出现在不带默认值的类型参数之前。这条规则与普通函数参数无默认值的参数不能跟在有默认值的参数后面逻辑同源一旦某个位置之后的参数都可以省略那么该位置之后就不能再出现必须显式提供的参数否则类型实参的省略语义会产生歧义。Ruff 的类型检查器 ty 将这条规则固化为invalid-generic-class诊断并通过 invalid_type_parameter_order.md 这个 mdtest 用例做了系统性的快照验证。二、mdtest 机制与测试文档结构2.1 什么是 mdtestmdtest 是 Ruff 中用于类型检查器行为验证的 Markdown 驱动测试框架运行器位于 crates/mdtest/src/lib.rs。它从 Markdown 文档中提取 Python 代码片段交由 ty 类型检查器分析再基于文档内的# error: [diagnostic-code]注释断言诊断产生的位置与数量最终将完整诊断输出渲染为 insta 快照。本测试文档以!-- snapshot-diagnostics --标记声明其意图生成诊断快照。文档头部通过 TOML front-matter 指定了测试环境[environment] python-version 3.13即该用例在 Python 3.13 版本语义下运行。2.2 行内诊断断言语法测试代码通过# error: [invalid-generic-class]注释来标注预期诊断。注释可以出现在声明行的行尾单行写法也可以独立成行多行写法class Foo(Generic[T1, T2]): # error: [invalid-generic-class]行尾断言 class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class] class VeryBad( # error: [invalid-generic-class]独立行断言且可重复出现以匹配多条诊断 # error: [invalid-generic-class] Protocol[T1, T2, DefaultStrT, T3], Generic[T1, T2, DefaultStrT, T3], ): ...若注释还带引号字符串则进一步断言诊断的完整消息文本例如文档中的# error: [invalid-generic-class] Type parameter T2 without a default cannot follow earlier parameter T1 with a default三、测试用例逐行剖析测试代码首先声明了三个类型变量与一个合法基类from typing import TypeVar, Generic, Protocol T1 TypeVar(T1, defaultint) # 带默认值 int T2 TypeVar(T2) # 无默认值 T3 TypeVar(T3) # 无默认值 DefaultStrT TypeVar(DefaultStrT, defaultstr) # 带默认值 str class SubclassMe(Generic[T1, DefaultStrT]): x: DefaultStrT class Baz(SubclassMe[int, DefaultStrT]): pass这里SubclassMe合法T1有默认值在DefaultStrT有默认值之前两个都带默认值满足默认参数后置规则。Baz特化时只提供int一个实参同样合法——因为DefaultStrT有默认值str可以省略。随后是四种违规声明逐一对应invalid-generic-class诊断类基类订阅违规原因FooGeneric[T1, T2]无默认值的T2位于有默认值的T1之后BarGeneric[T2, T1, T3]无默认值的T3位于有默认值的T1之后SpamGeneric[T1, T2, DefaultStrT, T3]无默认值的T2、T3均位于有默认值的参数之后HamProtocol[T1, T2, DefaultStrT, T3]同Spam但以Protocol作为基类值得注意Spam/Ham的细节列表中间虽然夹着带默认值的DefaultStrT但排序规则检验的是是否存在更早的带默认值参数因此位于T1之后的T2与位于DefaultStrT之后的T3都被判定为违规一条诊断会同时列出所有违规的无默认参数。VeryBad则是双重违规叠加class VeryBad( # error: [invalid-generic-class] # error: [invalid-generic-class] Protocol[T1, T2, DefaultStrT, T3], Generic[T1, T2, DefaultStrT, T3], ): ...它同时继承了下标化的Protocol[...]与下标化的Generic[...]触发两条诊断一条是同时继承订阅版Protocol和Generic附带自动修复建议另一条是参数排序违规作用于Protocol[T1, T2, DefaultStrT, T3]的参数切片。这也是文档中两条独立行注释的由来——每个注释对应一条诊断。四、快照输出解读诊断消息的完整形态该用例的快照位于 invalid_type_parameter…_(eaa359e8d6b3031d).snap.snap)其中呈现了 ty 渲染的完整诊断。以Foo为例error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults -- src/mdtest_snippet.py:17:19 | 17 | class Foo(Generic[T1, T2]): | ^^^^^^ | | | Type variable T2 does not have a default | Earlier TypeVar T1 does | ::: src/mdtest_snippet.py:3:1 | 3 | T1 TypeVar(T1, defaultint) | ------------------------------- T1 defined here 5 | T2 TypeVar(T2) | ------------------ T2 defined here该输出展示了诊断的四层信息结构主标题primary titleType parameters without defaults cannot follow type parameters with defaults概括违规规则简洁消息concise messageType parameterT2without a default cannot follow earlier parameterT1with a default精确点名违规参数与被违反的更早参数与测试文档中引号断言完全一致主标注primary annotation在Generic[...]的参数切片上用^高亮指明Type variableT2does not have a default并辅以Earlier TypeVarT1does说明冲突来源次标注secondary annotations以:::段落追溯T1、T2各自的TypeVar定义位置标注T1 defined here、T2 defined here帮助开发者快速定位声明源头。当违规参数不止一个时主标注消息会枚举全部参数如Spam/Ham输出中的Type variables T2 and T3 do not have defaults Earlier TypeVar T1 doesVeryBad的第一条诊断Cannot both inherit from subscriptedProtocoland subscriptedGeneric还携带了可交互的修复建议help: Remove the type parameters from the Protocol base note: This is an unsafe fix and may change runtime behavior其中-/形式的 diff 提示将Protocol[T1, T2, DefaultStrT, T3]改写为Protocol并明确标注这是不安全的修复可能改变运行时行为——因为移除下标参数后Protocol不再绑定类型变量泛型语义会发生变化。五、源码实现诊断从何处触发5.1 Lint 声明invalid-generic-class由宏declare_lint!声明于 crates/ty_python_semantic/src/types/diagnostic.rs#L593-L600declare_lint! { #[doc include_str!(../../resources/lint_docs/invalid-generic-class.md)] pub(crate) static INVALID_GENERIC_CLASS { summary: detects invalid generic classes, status: LintStatus::stable(0.0.1-alpha.1), default_level: Level::Error, } }该 lint 自0.0.1-alpha.1起即为稳定项默认级别为Error。其面向用户的行为说明与更多示例收录在 lint_docs/invalid-generic-class.md其中还包含另一类同 lint 场景PEP 695 语法与 legacy 语法混用如class CU: ...同样报invalid-generic-class。5.2 报告函数report_invalid_type_param_order参数排序违规由 report_invalid_type_param_order 负责报告其函数签名接收类定义节点、带默认值的类型变量typevar_with_default、以及后续所有违规的无默认类型变量切片invalid_later_typevars。实现要点如下定位基类遍历class.explicit_bases()查找SubscriptedProtocol或SubscriptedGeneric基类diagnostic.rs#L4607-L4623并断言必然存在expect注释解释了前提legacy 泛型上下文必然来自二者之一诊断范围取基类订阅表达式的slice区间作为主诊断范围即Generic[...]中括号内的参数列表消息分级主标题固定为Type parameters without defaults cannot follow type parameters with defaults简洁消息则用format_args!动态嵌入参数名diagnostic.rs#L4642-L4646单/多参数分支若只有一个违规参数主标注消息为Type variableXdoes not have a default否则调用format_enumeration生成Type variablesXandYdo not have defaultsdiagnostic.rs#L4648-L4659定义追溯对更早的带默认值参数与首个违规参数分别追加次标注定位到各自TypeVar声明的完整区间并标注X defined herediagnostic.rs#L4669-L4679。从源码结构可以推断上游的类型推断阶段ty_python_semantic/src/types/infer在解析类基类订阅时完成合法性检查收集typevar_with_default与invalid_later_typevars之后调用本函数产出诊断同类消息模板在 function.rs#L537 附近亦有出现说明该排序规则同样适用于函数签名的类型参数场景。六、违规修复与最佳实践6.1 修复方法默认参数统一置后修复的核心原则是调整Generic/Protocol订阅中的参数顺序将所有带默认值的类型参数移到列表末尾。以测试用例为例# 违规无默认值的 T2 在带默认值的 T1 之后 class Foo(Generic[T1, T2]): ... # 修复按 无默认值在前、带默认值在后 排列 class Foo(Generic[T2, T1]): ... # 违规T3 位于带默认值的 T1 之后 class Bar(Generic[T2, T1, T3]): ... # 修复 class Bar(Generic[T2, T3, T1]): ...对于Spam/Ham这类多参数混合同样只需保证参数列表尾部全部为带默认值的参数# 违规Generic[T1, T2, DefaultStrT, T3] # 修复Generic[T2, T3, T1, DefaultStrT]6.2 设计建议声明顺序即特化契约类型参数列表的顺序决定了调用方Generic[...]特化时实参的对应关系。一旦允许省略尾部参数任何实参都会按位置绑定因此务必让可省略的全部靠后保持与函数默认参数一致的直觉避免基类冗余订阅VeryBad的教训是不要同时在下标化的Protocol[...]与Generic[...]中重复声明同一组类型参数这会同时触发重复继承订阅版基类与排序违规两类错误优先考虑 PEP 695 语法invalid-generic-classlint 同时覆盖 PEP 695 与 legacy 混用场景见 lint_docs/invalid-generic-class.md。新代码可直接使用class C[T, U int]: ...新式语法从根本上规避两类语法叠加带来的复杂性。七、如何在本仓库复现该测试该测试是 mdtest 快照测试体系的一部分快照生成由 insta 完成。复现方式定位测试文档invalid_type_parameter_order.md检查对应快照invalid_type_parameter…_(eaa359e8d6b3031d).snap.snap)快照头部source: crates/mdtest/src/lib.rs标注了运行器来源快照中mdtest name与mdtest path两个字段建立了文档用例 ↔ 快照文件的双向对应关系任何文档改动新增断言、修改消息都会导致快照失配从而在测试中暴露差异。这一机制保证了诊断消息的任何措辞调整例如主标题或简洁消息的改写都会立即在所有相关快照中被检视是 ty 保持诊断输出稳定性的重要保障。八、总结围绕invalid_type_parameter_order.md这一个 mdtest 用例本文完整还原了 ty 对legacy 类型参数默认值排序规则的检测链路从文档中的# error: [invalid-generic-class]断言到快照中多层次的诊断渲染主标题、简洁消息、主/次标注、修复建议再到 diagnostic.rs 中report_invalid_type_param_order的具体实现。理解这条链路既有助于写出符合 PEP 696 语义、可在 Python 3.13 稳定运行的泛型类也能为贡献 ty 诊断或扩展新 lint 规则提供一份可对照的实现范本。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考