首页
/ Ruff 类型检查规则解析:invalid-generic-class —— 无效泛型类定义的静态检测与源码实现

Ruff 类型检查规则解析:invalid-generic-class —— 无效泛型类定义的静态检测与源码实现

2026-09-08 11:28:14作者:袁立春Spencer

导读

invalid-generic-class 是 Ruff 内置静态类型检查器 ty(位于仓库 crates/tycrates/ty_python_semantic)提供的一组类型诊断规则,用于在**编译期(而非运行期)**发现"泛型类定义不合规、运行时会抛出 TypeError"的代码。本文以该规则的官方文档 invalid-generic-class.md 为骨架,完整还原其核心语义(PEP 695 语法与旧式 Generic 语法混用、带默认值的类型参数必须全部排在无默认值参数之后等),并结合 diagnostic.rsstatic_class.rs 等源码,拆解该规则在 ty 检查器内部的触发路径。读完本文,你将能够:判断哪些泛型类写法会被本规则拦截并准确理解原因;同时掌握在仓库中用 mdtest 测试夹具复现与验证这些诊断的方法。


规则定位:在类型检查阶段兜住运行期 TypeError

在 Python 中定义泛型类并非"随便写写即可"。类型系统对泛型类的声明方式有一整套必须遵守的约束,违反其中任何一条,都可能在类创建或实例化阶段由运行时直接抛出 TypeError。本规则的职责,就是把这些"会炸"的写法在静态检查阶段就拦截下来。

在源码中,该规则以如下形式声明(见 diagnostic.rs):

declare_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,
    }
}

值得注意的细节:

  • 规则文档正文正是通过 include_str!resources/lint_docs/invalid-generic-class.md 直接嵌入,说明本文作为规则说明与该 lint 的实现严格一一对应,属于 ty 诊断体系的"一等公民"。
  • default_level: Level::Error 表明其默认严重级别为 Error:一旦命中,会被当作类型错误处理,而不是可忽略的提示。
  • 从代码中的调用点(context.report_lint(&INVALID_GENERIC_CLASS, ...))可以看出,它并不是单条检查,而是一个"规则家族",覆盖从类声明、继承列表到泛型下标(subscript)的多种边界场景(详见下文第 4 节)。

规则规范层面,typing 官方规范(typing spec)中的 Generics 章节对本规则覆盖的"泛型类定义要求"给出了权威定义,这也是该文档在 References 一节中列出的依据来源。


一图看懂两类核心违规场景

在介绍源码前,先完整呈现规则文档中的两个核心示例。它们分别命中本规则两大"主战场"。

[environment]
python-version = "3.12"
from typing_extensions import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U", default=int)


# 场景一:类同时使用了 PEP-695 语法与旧式语法
class CU: ...  # error


# 场景二:带默认值的类型参数排在无默认值类型参数之前
class D(Generic[U, T]): ...  # error

配置要点说明:

  • 示例环境声明 python-version = "3.12":PEP 695 内置类型参数语法 class C[U] 需要 Python 3.12+;
  • 示例从 typing_extensions 导入而非 typing,是因为 TypeVardefault= 参数要到 Python 3.13 才进入标准库 typing,在 3.12 下必须借助 typing_extensions 才能写出该特性——这也让"默认值排序"这类校验可以在更早的 Python 版本上被统一测试。

下面分别拆解两个场景的"为什么错"以及 ty 内部如何识别它们。


场景一:PEP 695 语法与旧式 Generic 语法混用

class CU: ...  # error

为什么这是错的

class CUPEP 695(Python 3.12+)语法,类型参数写在类名后的方括号中;而 Generic[T]旧式(legacy) 泛型语法,类型参数通过继承 typing.Generic 下标来表达。规范禁止将两套机制混合到同一个类上:

  • 若类声明了 PEP 695 类型参数,它就不能再从旧式的 Generic[...] 继承;
  • 反之,若想用旧式 Generic[T],就必须放弃类级方括号语法。

一旦违反,运行时在构建类时就会抛出 TypeError——这也是本文开头"运行时才会炸"说法的第一处典型体现。

ty 内部如何识别

ty 在类检查阶段会尝试构建方法解析顺序(MRO)。当 MRO 解析返回 StaticMroErrorKind::Pep695ClassWithGenericInheritance 错误种类时,代码将诊断上报给本规则(见 static_class.rs):

StaticMroErrorKind::Pep695ClassWithGenericInheritance => {
    if let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, class_node) {
        builder.into_diagnostic(
            "Cannot both inherit from `typing.Generic` \
                and use PEP 695 type variables",
        );
    }
}

也就是说,ty 把"旧式 Generic 基类"与"PEP 695 类型参数"的共存视为一个 MRO 层面的结构性错误,诊断文案为:"Cannot both inherit from typing.Generic and use PEP 695 type variables"(不能同时继承 typing.Generic 又使用 PEP 695 类型变量)。

与此同族的另一条独立检查(仍在 static_class.rs)负责拦截更隐蔽的变体——类本身是 PEP 695 泛型,但其旧式基类中夹带了 legacy TypeVar

if class.has_pep_695_type_params(db)
    && let Some(generic_context) = class.inherited_legacy_generic_context(db)
    && let Some(typevar) = generic_context
        .variables(db)
        .find(|typevar| !typevar.typevar(db).is_self(db))
    && let Some(builder) = context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
    builder.into_diagnostic(format_args!(
        "Legacy type variable `{}` cannot be used in a PEP 695 class base",
        typevar.name(db),
    ));
}

对应诊断信息为:"Legacy type variable {} cannot be used in a PEP 695 class base"(旧式类型变量 X 不能用在 PEP 695 类基类中),从"基类带入的类型变量"维度封堵了 PEP 695 与 legacy 的类型参数体系互相渗透。


场景二:带默认值的类型参数排在无默认值参数之前

class D(Generic[U, T]): ...  # error

其中 U = TypeVar("U", default=int) 带默认值,而 T = TypeVar("T") 不带默认值

为什么这是错的

泛型下标的特化依赖位置参数:D[int] 会把第一个类型参数绑定为 int。类型参数默认值的语义是"特化时可以省略尾部的若干类型实参"——因此规范要求:

一旦出现带默认值的类型参数,其后不得再出现不带默认值的类型参数。

示例中 Generic[U, T] 令带默认值的 U 排在了不带默认值的 T 之前,此时若省略末尾实参(等价于试图只特化 U),T 将无值可取;运行时会因这种歧义抛出 TypeError

ty 内部如何识别

这一场景由 static_class.rs 中的顺序扫描逻辑检出:对 legacy 泛型上下文中的每个类型参数,一旦记录到某个带默认值的参数(typevar.default_type(db, env).is_some()),后续任何不带默认值的参数都会被收集为违规者,最后统一交给 report_invalid_type_param_order 上报。

诊断本身是一段带多处标注(annotation)的富文本,核心文案在 diagnostic.rs

  • 主诊断:"Type parameters without defaults cannot follow type parameters with defaults"(无默认值的类型参数不能跟在有默认值的类型参数之后);
  • 精简消息:Type parameter \T` without a default cannot follow earlier parameter `U` with a default`——精确到具体是哪两个参数犯了顺序错误;
  • 主标注:如果只有一个违规参数,则标注 Type variable \T` does not have a default,多个则枚举 Type variables {names} do not have defaults`;
  • 反向标注:在"带默认值、位置靠前"的参数处标注 Earlier TypeVar \U` does`;
  • 次级标注:在被涉及的两个 TypeVar 各自的定义处TU 的赋值行)追加 \U` defined here/`T` defined here`,帮助用户直接跳回定义点理解"默认值归属"。

这段"多点标注 + 定位到定义处"的产出形态,让读者能像阅读编译器错误报告一样快速定位问题根因。


同一规则覆盖的其它边界场景

正如前文所说,INVALID_GENERIC_CLASS 实际承载了多类"无效泛型类"检查。除文档两个主示例外,从源码可以归纳出以下同族边界场景(源码证据均来自当前仓库):

3.1 泛型下标(subscript)层面的约束

subscript.rs 中,对 Generic[...] / Protocol[...] 等泛型类的下标特化也统一上报本规则:

  • 重复类型参数Type parameter \{name}` cannot appear multiple times in `{origin}` subscription——例如把同一个 TypeVar` 在同一次特化里传入两次;
  • TypeVarTuple 未解包TypeVarTuple must be unpacked with \*` or `Unpack[]` when used as an argument to `{origin}``;
  • 多个 TypeVarTupleOnly one \TypeVarTuple` parameter is allowed in a `{origin}` subscription`。

这些约束的共同点在于:它们在"类型被实际用作泛型实参"的瞬间检查合法性,而不是等到类定义完成之后。

3.2 冲突的泛型祖先特化

同一泛型祖先若从不同基类继承到不兼容的特化,类定义同样非法。见 diagnostic.rsreport_inconsistent_generic_bases 文档注释示例:

class Grandparent(Generic[T1, T2]): ...
class Parent(Grandparent[T1, T2]): ...
class BadChild(Parent[T1, T2], Grandparent[T2, T1]): ...  # Error

BadChild 同时要求 Grandparent 的特化在两条继承路径上不一致,ty 会通过逐条遍历 MRO、为每个泛型祖先记录"首次见到的特化"来发现这类冲突并上报本规则。

3.3 ProtocolGeneric 同时下标化

static_class.rs 还拦截"既继承下标化的 Protocol 又继承下标化的 Generic"的类,诊断信息为 "Cannot both inherit from subscripted Protocol and subscripted Generic"。当两处传入的类型参数完全相同时,规则还会给出修复建议("Remove the type parameters from the Protocol base")并附带一个将 Protocol[...][...] 下标部分直接删除的 unsafe fix。

3.4 泛型基类的覆盖范围不足

static_class.rs 中的一条检查要求:

Generic base class must include all type variables used in other base classes

即:当类既继承 Generic[...] 又继承其它用到了 TypeVar 的泛型基类时,Generic 的实参列表必须覆盖其它基类用到的全部类型变量,否则类无法统一约束这些变量。

3.5 PEP 695 泛型参数的顺序约束(与场景二同构的"现代语法"版)

对 PEP 695 语法的类(class C[T, *Ts, U=...]),static_class.rs 会复用 type_param_validation 模块执行额外检查:

  • check_single_typevar_tuple_pep695:PEP 695 泛型类中至多允许一个 TypeVarTuple
  • check_no_default_after_typevar_tuple_pep695:带默认值的参数不能排在 TypeVarTuple 之后(因为 TypeVarTuple 会吞掉其后所有剩余位置实参,默认值将永远无法命中)。

3.6 默认值引用顺序

report_invalid_typevar_default_reference 负责检查默认值本身的合法性:

  • 若某类型参数的默认值引用了更靠后的参数,报 "Default of X cannot reference later type parameter Y";
  • 若引用了列表之外的变量,报 "Default of X cannot reference out-of-scope type variable Y"。

这与函数参数默认值的"只能引用此前已定义名称"规则遥相呼应,只不过作用域是类型参数列表。

3.7 从类延伸到函数

值得注意的是,本规则并不局限于类:function.rs 中对泛型函数的后推断校验同样会启用 INVALID_GENERIC_CLASS 并调用 report_lint 上报(如其中对函数 PEP 695/legacy 混用等情况的处理)。也就是说,读作"invalid generic class"的规则,其实现语义更准确地说应是"无效的泛型声明",覆盖类与函数两条路径。


触发时机:post-inference 校验阶段

从代码路径可以还原本规则的触发位置。ty 的类型推断(inference)完成后,会对类定义语句执行后推断校验(post-inference validation),集中入口在 static_class.rs,其执行顺序大致为:

  1. 逐基类检查特殊类型(NamedTupleProtocolGeneric 等)的使用是否合法(对应上文 3.3、文档场景一);
  2. 尝试解析 MRO,处理 DuplicateBasesInvalidBasesPep695ClassWithGenericInheritance(场景一在此抛出)、InheritanceCycle 等错误种类;
  3. 对泛型上下文做整体校验:检查 legacy 类型变量混入 PEP 695 基类(3.2 变体)、Generic 覆盖范围、默认值排序、默认值引用顺序、PEP 695 的 TypeVarTuple 顺序等(场景二在此抛出);
  4. 在类型被下标特化时(subscript.rs),执行重复参数、TypeVarTuple 解包等即时检查。

正因为这些检查跑在类创建之前的类型推断管道中,使用者不必等到 TypeError 真正抛出,就能在编辑器/CLI 中收到精确到"哪一个 TypeVar、定义在哪一行"的 Error 级诊断。


在仓库中验证与进一步阅读

若想亲手观察这些诊断的实际输出,仓库提供了两条路径:

1. 规则的官方文档(本文依据)

本规则说明文档即 invalid-generic-class.md,与其相邻的 ruff.toml 及同目录下数十个 invalid-*.md 文档共同构成 ty 的 lint 文档集,并被 declare_lint! 通过 include_str! 编译进规则注册表。

2. mdtest 语言级测试夹具

ty 使用 Markdown 驱动的测试体系(mdtest)来断言诊断行为,其中与本规则直接相关的夹具包括:

这些夹具中的预期诊断与快照(snapshots 目录下 *.snap 文件)一一对应,是理解每条错误文案精确形态的最佳"参考答案"。规则的完整清单可进一步查阅 crates/ty/docs/rules.md 规则参考页。


小结

invalid-generic-class 把 Python 泛型类"运行期才暴露"的一整类 TypeError 前置到了静态检查阶段:两套泛型语法不能混用、带默认值的类型参数必须全部排后、TypeVarTuple 与默认值的相互制约、泛型祖先特化必须一致、泛型基类必须覆盖全部类型变量……每一条都对应着 ty_python_semantic 中精确到具体类型参数的诊断与源码级实现。理解这一规则家族,既是写出健壮泛型代码的前提,也是读懂 ty 类型检查器"类校验"设计的一把钥匙。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390