ruff/ty 类型检查器 unsupported-base 规则详解:当类基类无法解析 MRO 时
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
本篇指南聚焦 ruff 仓库中类型检查器 ty 的unsupported-base诊断规则(对应 crates/ty/docs/rules.md 中收录的 lint,源定义位于 crates/ty_python_semantic/src/types/diagnostic.rs)。文章会先复现该规则的全部文档语义(检测什么、为何危险、触发示例),再结合实现源码与 mdtest 测试用例深入剖析触发条件、诊断信息结构、与invalid-base等相邻规则的边界,帮助你在使用 ruff/ty 做静态类型检查时准确理解并修复此类告警。
规则概述:检测什么
unsupported-base用于检查类定义中使用了 ty 不支持的基类的写法。其官方文档原文如下:
Checks for class definitions that have bases which are unsupported by ty.
换句话说,当你在class Foo(Base): ...的基类位置放入了 ty 无法静态处理的类型时,ty 就会针对该基类报出unsupported-base。
为什么需要这条规则
ty 是一个全静态类型检查器,它需要为每个类解析出确定的方法解析顺序(Method Resolution Order,MRO),才能判断属性查找、方法重写、super()调用等一系列类型行为。而一旦某个基类是"复杂类型"的实例——例如一个联合类型(union type)——MRO 就无法被唯一确定:
If a class has a base that is an instance of a complex type such as a union type, ty will not be able to resolve the method resolution order (MRO) for the class. This will lead to an inferior understanding of your codebase and unpredictable type-checking behavior.
后果是双重的:一方面 ty 对代码库的理解精度下降(成员解析、重写检查等都会退化为不精确的结果);另一方面类型检查行为变得不可预测(同一份代码在不同检查路径下可能得到不同结论)。因此 ty 用一条默认级别为warn的 lint 主动提示你这类写法。
从源码确认,该规则在 crates/ty_python_semantic/src/types/diagnostic.rs#L530-L537 中声明:
declare_lint! { #[doc = include_str!("../../resources/lint_docs/unsupported-base.md")] pub(crate) static UNSUPPORTED_BASE = { summary: "detects class bases that are unsupported as ty could not feasibly calculate the class's MRO", status: LintStatus::stable("0.0.1-alpha.7"), default_level: Level::Warn, } }几个关键元信息:
- summary:ty 无法可行地计算出类的 MRO 时检测不支持的类基类;
- status:自
0.0.1-alpha.7起标记为 stable; - default_level:默认告警级别为
Warn(区别于同族规则invalid-base的Error,原因下文详述)。
触发示例与行为分析
原文档给出了一个典型示例,完整复现如下:
import datetime class A: ... class B: ... if datetime.date.today().weekday() != 6: C = A else: C = B class D(C): ... # error: [unsupported-base]逐行解读
- 定义了
A、B两个普通类; - 通过运行时条件判断,将全局名字
C绑定到A或B之一; class D(C)把C用作基类。
关键在于:ty 是静态分析,它无法预知datetime.date.today().weekday() != 6在运行时的真值,因此C的静态类型是A | B(联合类型)。把一个联合类型放在基类位置时,D的 MRO 取决于C究竟解析成哪个类,而这是运行时才决定的——ty 无法在这种情况下计算出确定、一致的 MRO,于是报出unsupported-base。
实际报错消息
在 mdtest 快照中(见 crates/ty_python_semantic/resources/mdtest/snapshots/ 下的unsupported_base_dyn…系列快照),此类诊断的完整消息形态为:
error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"在 crates/ty_python_semantic/resources/mdtest/mro.md 中也有多处断言,例如第 303 行:
# error: 11 [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`"源码实现:诊断的生成与消息构成
unsupported-base的诊断主体实现在 crates/ty_python_semantic/src/types/diagnostic.rs#L4232-L4256 的report_unsupported_base函数:
pub(crate) fn report_unsupported_base( context: &InferContext, base_node: &ast::Expr, base_type: Type, class: StaticClassLiteral, ) { let Some(builder) = context.report_lint(&UNSUPPORTED_BASE, base_node) else { return; }; let db = context.db(); let env = &context.program_environment(); let mut diagnostic = builder.into_diagnostic("Unsupported class base"); diagnostic .set_primary_annotation_message(format_args!("Has type `{}`", base_type.display(db, env))); diagnostic.set_concise_message(format_args!( "Unsupported class base with type `{}`", base_type.display(db, env) )); diagnostic.info(format_args!( "ty cannot resolve a consistent method resolution order (MRO) for class `{}` \ due to this base", class.name(db) )); diagnostic.info("Only class objects or `Any` are supported as class bases"); }从这段代码可以看出诊断的三个信息层级:
- 主标注(primary annotation):
Has type \<具体类型>``,直接标注在出问题的基类表达式上; - 简明消息(concise message):
Unsupported class base with type \<具体类型>``,即 CLI / IDE 摘要栏展示的一行; - 补充说明(info):两条固定文案——"ty cannot resolve a consistent method resolution order (MRO) for class
Xdue to this base"(解释根因)和 "Only class objects orAnyare supported as class bases"(给出修复方向)。
修复方向从源码中得到印证
"Only class objects orAnyare supported as class bases" 这条信息非常关键,它划定了 ty 对类基类的支持边界:
- 类对象(如
class D(A),A是类); Any(显式声明的Any类型,ty 对其放弃精确推导)。
其余一切"表达式计算出复杂类型"的基类,都会触发本规则。这是实现层面的硬约束,也是排查时的直接依据:把基类位置的表达式改为确定指向某个类对象(或用Any显式声明),即可消除该告警。
触发路径:MRO 求解失败后的分流
unsupported-base并不是独立扫描出来的,而是 ty 在类定义后处理阶段求解 MRO 失败时分流派生的。调用链位于 crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs#L543-L557:
// Check that the class's MRO is resolvable match class.try_mro(db, None) { Err(mro_error) => match mro_error.reason() { StaticMroErrorKind::DuplicateBases(duplicates) => { /* 报 duplicate-bases */ } StaticMroErrorKind::InvalidBases(bases) => { for (index, base_ty) in bases { let base_node = expanded_base_entries[*index].source_node(); report_invalid_or_unsupported_base(context, base_node, *base_ty, class); } } StaticMroErrorKind::UnresolvableMro { .. } => { /* 报 inconsistent-mro */ } // ... }, // ... }也就是说,class.try_mro失败后按错误种类分流:
| MRO 错误类型 | 派生的规则 |
|---|---|
DuplicateBases(基类列表重复) | duplicate-base |
InvalidBases(存在无法作为基类的类型) | invalid-base/unsupported-base(由report_invalid_or_unsupported_base分流) |
UnresolvableMro(基类顺序导致 MRO 不一致) | inconsistent-mro |
InheritanceCycle(继承环) | cyclic-class-definition |
PEP 695 泛型与Generic混用 | invalid-generic-class |
invalid-base 与 unsupported-base 的分界
report_invalid_or_unsupported_base(crates/ty_python_semantic/src/types/diagnostic.rs#L4107)给出了两者精确的分界逻辑:
- 若基类类型可赋值给
type的实例类型(即它"确实是个类"),则直接报unsupported-base; - 若基类是
NewType的实例,报invalid-base并提示改用X = NewType('X', ...)写法; - 否则尝试调用基类的
__mro_entries__(以一个由 type 实例组成的同构元组为参数):- 调用成功且返回类型可赋值给"type 实例元组" → 报
unsupported-base(该类型理论上能参与 MRO 构造,但 ty 无法静态展开); - 调用成功但返回类型不符 → 报
invalid-base,并说明"__mro_entries__没有返回类型元组"; - 调用失败(无该方法 / 可能未绑定 / 不可调用 / 参数不匹配等)→ 报
invalid-base,并附带__mro_entries__的期望签名说明def __mro_entries__(self, bases: tuple[type, ...], /) -> tuple[type, ...]。
- 调用成功且返回类型可赋值给"type 实例元组" → 报
由此可以理解两条规则的语义差别与默认级别差异:
invalid-base(默认 Error):该基类在运行时就会让类定义抛TypeError,是"必然出错";unsupported-base(默认 Warn):该基类在运行时未必报错,只是 ty 静态上无法为其计算 MRO,是"ty 能力边界导致的降级"。
mdtest 中 crates/ty_python_semantic/resources/mdtest/mro.md 第 435–442 行专门注释了这一区分:
exception at runtime, so we issue
unsupported-baserather thaninvalid-base:
class Bar(Foo()): ... # error: [unsupported-base]Foo()是实例表达式,不是类对象,但 ty 认为其可能通过某种途径参与 MRO 构造(并非必然运行时异常),因此归入unsupported-base而不是invalid-base。
其他触发场景:变长元组解包与动态基类
除 MRO 求解失败外,static_class.rs中还有一处独立的触发点(crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs#L532-L541):
// Check for starred variable-length tuples that cannot be unpacked for base in class_node.bases() { if let ast::Expr::Starred(starred) = base && let starred_ty = definition_expression_type(db, class_definition, &starred.value) && let Some(tuple_spec) = starred_ty.tuple_instance_spec(db, env) && !matches!(tuple_spec.as_ref(), Tuple::Fixed(_)) { report_unsupported_base(context, base, starred_ty, class); } }这段代码针对星号解包基类(如class D(*bases))中的变长元组:如果被解包的元组长度在静态上无法确定(不是固定长度元组),ty 无法得知展开后的基类列表,于是同样报unsupported-base。mro.md 第 788 行有对应断言:
class D(D.a): # error: [unsupported-base]此外,规则家族中还有一条高度相关的规则unsupported-dynamic-base(crates/ty/docs/rules.md#L6511 指出):
This is equivalent to
unsupported-basebut applies to classes created viatype()rather than class statements.
它专门针对通过type(name, bases, namespace)动态创建类的场景,声明位置在 crates/ty_python_semantic/src/types/diagnostic.rs#L539-L546,默认级别为Ignore。排查时若发现unsupported-base未覆盖动态建类,可确认是否应同时关注该规则。
综合示例与修复建议
把文档示例扩展成一个可对照的完整场景:
import datetime from typing import Any class A: def method(self) -> int: return 1 class B: def method(self) -> str: return "x" if datetime.date.today().weekday() != 6: C = A else: C = B class D(C): # warning: [unsupported-base] "Unsupported class base with type `<class 'A'> | <class 'B'>`" pass class E(Any): # 合法:ty 明确支持 Any 作为基类 pass修复思路优先级:
- 消除基类表达式的多态性:将
C = A / C = B的条件赋值改写为运行时分支中各自独立的类定义,或引入显式基类选择函数,确保基类位置静态上是单一类对象; - 显式声明
Any:若确实需要动态基类且不关心该部分的精确检查,可让基类表达式带Any类型,ty 会停止对其推导; - 改用动态建类:确认该动态基类语义后,可评估是否走
type()路径并配合unsupported-dynamic-base的配置管理告警级别。
小结
unsupported-base是 ty 在无法为类计算一致 MRO 时给出的warn级诊断,覆盖联合类型基类、非类对象基类以及静态长度未知的星号解包元组基类等场景;- 诊断消息明确标注了基类的实际类型,并提示"Only class objects or
Anyare supported as class bases",可直接作为修复指引; - 它与
invalid-base的分界在于"运行时是否必然异常":invalid-base对应必然的运行时错误(默认 Error),unsupported-base对应 ty 静态能力的边界(默认 Warn); - 实现与测试证据分别位于 crates/ty_python_semantic/src/types/diagnostic.rs、crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs 及 crates/ty_python_semantic/resources/mdtest/mro.md,读者可沿这些路径继续深入。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考