ty 类型检查器invalid-metaclass规则详解:拦截无效的metaclass=实参
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
导读
本文围绕 Ruff 仓库中类型检查器 ty 的invalid-metaclass静态规则展开。该规则会在静态分析阶段拦截那些语法上合法、但运行时必然抛TypeError的metaclass=实参(例如非可调用对象、未实例化的泛型元类等),把错误前置到开发期。读完本文,你将掌握该规则的触发条件、错误分类体系、诊断范围定位方式,以及如何从仓库源码与 mdtest 用例中验证其行为边界与已知局限。
规则概述:元类实参并不"任意"
Python 的类定义语法允许把任意表达式写在metaclass=之后,但从语义上说,这个表达式必须满足两个硬性约束:
- 必须可调用(callable);
- 被调用时必须接受与
type.__new__相同的参数(即类名、基类元组与类体命名空间),因为类对象的创建最终会委托给它。
invalid-metaclass规则正是对第一个约束(及泛型实例化约束)的静态检查。其官方行为描述位于 invalid-metaclass.md,规则元信息登记于 diagnostic.rs:
declare_lint! { #[doc = include_str!("../../resources/lint_docs/invalid-metaclass.md")] pub(crate) static INVALID_METACLASS = { summary: "detects invalid `metaclass=` arguments", status: LintStatus::stable("0.0.1-alpha.1"), default_level: Level::Error, } }也就是说,该规则自0.0.1-alpha.1起进入稳定(stable)状态,默认级别为error,一旦命中会直接报错而不仅是告警。ty 的完整规则目录可查阅 rules.md。
为什么要报错:运行时的必然失败
看文档给出的最小反例:
# TypeError: 'int' object is not callable class B(metaclass=42): ... # error42是一个合法的表达式,却不是可调用对象。Python 解释器执行该语句时会在类创建阶段尝试调用元类,从而抛出TypeError: 'int' object is not callable。这类错误只有程序真正运行到那条类定义时才会暴露,且经常潜伏在导入链深处。invalid-metaclass的价值就在于把这种运行时崩溃转换为可静态定位、可提前修复的编译期诊断。
类似的运行时崩溃还包括对已实例化对象误用元类、未完整实例化的泛型元类等,详见下文错误分类。
底层实现:try_metaclass与错误分类
触发入口
规则的检查点在static_class.rs的类语义后处理阶段。对每个类,ty 先尝试解析其元类,一旦失败就按错误类型分发诊断(见 static_class.rs):
// Check that the class's metaclass can be determined without error. if let Err(metaclass_error) = class.try_metaclass(db) { let invalid_metaclass_range = class_node .arguments .as_ref() .and_then(|arguments| arguments.find_keyword("metaclass")) .map(Ranged::range) .unwrap_or_else(|| class.header_range(db)); match metaclass_error.reason() { MetaclassErrorKind::GenericMetaclass => { /* → INVALID_METACLASS */ } MetaclassErrorKind::NotCallable(ty) => { /* → INVALID_METACLASS */ } MetaclassErrorKind::PartlyNotCallable(ty) => { /* → INVALID_METACLASS */ } MetaclassErrorKind::Conflict { .. } => { /* → conflicting-metaclass / CONFLICTING_METACLASS */ } MetaclassErrorKind::Cycle => { /* → CYCLIC_CLASS_DEFINITION */ } } }从源码结构可以清晰看出:invalid-metaclass只负责其中三类错误,而**元类冲突(Conflict)与循环继承(Cycle)**分别交由conflicting-metaclass与cyclic-class-definition规则处理,职责边界分明。
错误分类全集
try_metaclass返回的错误类型定义在 class.rs,MetaclassErrorKind枚举共五种变体:
| 变体 | 含义 | 归属规则 |
|---|---|---|
NotCallable(Type) | 元类是某个不可调用的类型(如int) | invalid-metaclass |
PartlyNotCallable(Type) | 元类是联合类型,其中部分成员不可调用 | invalid-metaclass |
GenericMetaclass | 元类是仍被类型变量参数化的泛型类 | invalid-metaclass |
Conflict { candidate, base_metaclass, base } | 继承层级中出现互相不兼容的元类 | conflicting-metaclass |
Cycle | 解析元类时检测到循环 | cyclic-class-definition |
NotCallable与PartlyNotCallable两个分支给出的诊断消息分别为Metaclass type '{ty}' is not callable与Metaclass type '{ty}' is partly not callable;而GenericMetaclass分支直接输出固定文案Generic metaclasses are not supported。
三种触发场景示例
从 mdtest 行为测试 metaclass.md 中可以提取出对应三种场景:
场景一:元类类型本身不可调用
def _(n: int): # error: [invalid-metaclass] class B(metaclass=n): ...场景二:联合类型中部分成员不可调用
def _(flag: bool): m = f if flag else 42 # error: [invalid-metaclass] class C(metaclass=m): ...m的可能类型是函数类型 | int,属于"部分不可调用"(PartlyNotCallable),ty 依然选择报错。
场景三:带未绑定类型变量的泛型元类(PEP 695)
class FooT: x: T # error: [invalid-metaclass] class BarT: ...对应的旧式typing.TypeVar + Generic写法同样被拦截,诊断消息为Generic metaclasses are not supported:
from typing import TypeVar, Generic T = TypeVar("T") class GenericMeta(type, Generic[T]): ... # error: [invalid-metaclass] "Generic metaclasses are not supported" class GenericMetaInstance(metaclass=GenericMeta[T]): ...泛型元类:边界在哪里
需要强调的是,GenericMetaclass只拒绝"仍被类型变量参数化"的元类。若泛型元类已被具体类型完全特化,ty 是允许的(见 metaclass.md):
class FooT: x: T class Bar(metaclass=Foo[int]): ... reveal_type(Bar.__class__) # revealed: <class 'Foo[int]'>诊断范围:精确到关键字实参
一个值得注意的实现细节是诊断位置的选择。从触发入口代码可以看出,ty 会先在类定义节点的参数列表中查找名为metaclass的关键字参数(arguments.find_keyword("metaclass")),并把整个关键字实参的 range作为报告位置;只有当类定义根本没有arguments时才回退到类头范围(class.header_range(db))。
mdtest 中用# snapshot: invalid-metaclass指令固化了这一输出格式(见 metaclass.md):
def _(n: int): # snapshot: invalid-metaclass class B(metaclass=n): x = 1 y = 2对应的快照输出为:
error[invalid-metaclass]: Metaclass type `int` is not callable --> src/mdtest_snippet.py:3:13 | 3 | class B(metaclass=n): | ^^^^^^^^^^^可见光标^^^精确覆盖了metaclass=n整个关键字实参(而非整个类头或整行),方便开发者在长类名、多基类的复杂类定义中一眼定位问题出处。
与相邻规则及已知局限
invalid-metaclass并非孤立存在,它与同模块内的元类相关检查共同构成一道防线:
conflicting-metaclass:当派生类的元类不是其所有基类元类的(非严格)子类时触发,例如显式metaclass=与某个基类自带的元类产生冲突;cyclic-class-definition:元类解析过程遇到循环类定义时触发,同时可防止循环引用造成类型检查器无限递归(mdtest 中专门用A(B) → B(C) → C(A)的用例验证了这一点,见 metaclass.md)。
在实现上,MetaclassError与MetaclassErrorKind均派生于 salsa 查询结果(salsa::SalsaValue),因此元类解析会参与增量计算缓存,重复检查同一文件不会重复推导。
从源码与测试的 TODO 注释还可以了解到当前已知的局限:例如元类可调用但签名与type.__new__不兼容的情况(如下面这个"签名不匹配"的元类)尚未产生诊断:
class SignatureMismatch: ... # TODO: Emit a diagnostic class D(metaclass=SignatureMismatch): ...也就是说,该规则目前覆盖的是"可调用性"与"泛型特化完整性"两类静态可判定的错误,而对元类签名与调用协议的深入校验仍是待完善方向(见 metaclass.md)。
实战小结
在基于 ty 的静态检查流程中,遇到error[invalid-metaclass]时可按以下顺序排查:
- 确认实参类型可调用:若传入的是普通实例或值类型(如
int、42),改用继承自type的元类; - 联合类型:若实参在分支中类型不同,保证所有可能分支都是可调用类型;
- 泛型元类:若元类声明了类型变量,使用前必须完全特化(如
Foo[int]),不要直接传入Foo[T]这类仍带自由类型变量的形态; - 区分相邻错误:若报错指向多个基类间的元类不兼容,应查阅
conflicting-metaclass的诊断说明。
整体上,invalid-metaclass是 ty 元类推导能力的一个小而关键的出口——它以MetaclassErrorKind五种分类为骨架,把"运行时才暴露的元类构造失败"系统性地前置到了静态阶段,并借助快照测试保证了诊断文本与定位范围的长期稳定。
延伸阅读
- 规则文档原始出处:invalid-metaclass.md
- 规则登记与元信息:diagnostic.rs
- 元类错误枚举定义:class.rs
- 检查触发与消息构造:static_class.rs
- 行为级测试与快照:metaclass.md
- 规则总览目录:rules.md
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考