ty 类型检查器中的 `invalid-explicit-override` 规则:如何用 `@override` 声明并校验方法覆盖
ty 类型检查器中的 invalid-explicit-override 规则:如何用 @override 声明并校验方法覆盖
导读
本文围绕 Ruff 仓库内置类型检查器 ty(crates/ty_python_semantic)中的 invalid-explicit-override 规则展开,讲解它如何检测"被 @override 装饰却并未真正覆盖任何父类方法"的代码。读完本文,你将掌握 typing.override 的正确使用方式、该规则的触发与豁免场景、其背后的源码实现原理,以及它与 missing-override-decorator、invalid-method-override(Liskov 替换原则)等规则的协作方式。
规则要做什么:@override 声明的双向校验
What it does
invalid-explicit-override 检查的是:被 @override 装饰器装饰、但实际上没有覆盖任何父类(superclass)方法的方法。
该规则的官方文档位于 crates/ty_python_semantic/resources/lint_docs/invalid-explicit-override.md,对应诊断定义位于 crates/ty_python_semantic/src/types/diagnostic.rs:
declare_lint! {
#[doc = include_str!("../../resources/lint_docs/invalid-explicit-override.md")]
pub(crate) static INVALID_EXPLICIT_OVERRIDE = {
summary: "detects methods that are decorated with `@override` but do not override any method in a superclass",
status: LintStatus::stable("0.0.1-alpha.28"),
default_level: Level::Error,
}
}
从定义可以看出三个关键事实:
- 默认级别为
error:只要代码被该规则命中,ty 默认直接报错,而不是警告; - 稳定版本号为
0.0.1-alpha.28:该规则从 ty 早期版本起就已存在并保持稳定; - 规则文档直接内嵌于源码:
include_str!表明 lint_docs 目录下的 Markdown 就是该规则唯一的权威说明,生成规则参考页(crates/ty/docs/rules.md)时也复用了同一份内容。
Why is this bad
给方法加上 @override 装饰器,本质上是向类型检查器(以及阅读代码的人)声明"这个方法是我刻意覆盖父类同名成员的"。typing.override 的语义见 crates/ty_python_semantic/resources/mdtest/override.md 开头的测试说明:
Decorating a method with
typing.overridedecorator is an explicit indication to a type checker that the method is intended to override a method on a superclass. If the decorated method does not in fact override anything, a type checker should report a diagnostic on that method.
当这一声明与实际不符(父类里根本没有同名成员)时,通常意味着:
- 方法名拼写错误(例如把
foo写成ffooo),而开发者原本期望覆盖; - 父类重构后删除了该方法,子类残留了多余的
@override; - 开发者误以为继承关系存在,实际基类并不包含该成员。
这些问题都应在开发阶段被尽早发现,而不是等到运行时才暴露。
完整示例:错误与正确的边界
ty 规则文档给出的示例非常直观,它同时覆盖了错误与正确两种情况。先看完整配置与代码:
[environment]
python-version = "3.12"
from typing import override
class A:
@override
def foo(self): ... # error
class B(A):
@override
def ffooo(self): ... # error
class C:
@override
def __repr__(self): ... # fine: overrides `object.__repr__`
class D(A):
@override
def foo(self): ... # fine: overrides `A.foo`
逐条解读:
A.foo(error):类A本身没有父类成员foo可覆盖(未显式继承任何类时,ty 仍会考虑object的成员,但foo不在其中),所以@override是无效声明;B.ffooo(error):类B虽然继承自A,但A中定义的是foo而非ffooo,这是典型的方法名拼写错误场景,@override恰好帮助 ty 将其暴露出来;C.__repr__(fine):类C未显式继承任何类,但__repr__是object的内置方法,所有类都隐式继承object,因此它确实覆盖了object.__repr__;D.foo(fine):D继承自A,且A.foo存在,覆盖成立。
注意第一个要点:检查对象是"隐式基类 object 的成员也会被纳入覆盖判定"。这是该规则的一个隐含前提——__repr__、__str__、__eq__ 等魔术方法即使在不写 class C(object) 的现代 Python 代码中也始终可被覆盖。
为什么示例中要指定 python-version = "3.12"
示例配置显式声明了 [environment] python-version = "3.12"。原因是 typing.override 是 Python 3.12 才进入标准库 typing 模块的装饰器;在更早的 Python 版本中需要从 typing_extensions 导入。ty 的实现会在给出修复建议时按版本切换推荐导入来源,见 crates/ty_python_semantic/src/types/overrides.rs:
let override_module =
if context.program_environment().python_version(db) >= PythonVersion::PY312 {
"typing"
} else {
"typing_extensions"
};
也就是说,在 < 3.12 的环境下,代码应写作 from typing_extensions import override。
源码实现:检查流程与触发条件
该规则的检查逻辑集中在 crates/ty_python_semantic/src/types/overrides.rs,模块开头就说明了职责范围——检查子类中的非法方法覆盖,包括(但不限于)违反 Liskov 替换原则(LSP) 的情况。
规则开关如何映射到检查配置
ty 用位标志(bitflags)聚合一组覆盖相关规则。在 overrides.rs 的 OverrideRulesConfig::from 中,INVALID_EXPLICIT_OVERRIDE 对应 OverrideRulesConfig::EXPLICIT_OVERRIDE:
if rule_selection.is_enabled(LintId::of(&INVALID_EXPLICIT_OVERRIDE)) {
config |= OverrideRulesConfig::EXPLICIT_OVERRIDE;
}
如果所有覆盖相关规则都被关闭,no_rules_enabled() 会提前返回,避免无谓遍历(见 overrides.rs 的 check_class 入口)。
触发条件:只在"确实没覆盖任何父类成员"时报错
在 overrides.rs 中,check_explicit_overrides 的调用受到严格前置条件约束:
if !subclass_overrides_superclass_declaration
&& !has_dynamic_superclass
&& (
first_reachable_definition.kind(db).is_function_def()
)
{
check_explicit_overrides(context, member, class_scope, class);
}
三个条件缺一不可:
!subclass_overrides_superclass_declaration:在 MRO(方法解析顺序)中找不到任何被覆盖的父类声明时才检查;!has_dynamic_superclass:如果类有动态基类(无法静态解析的基类),ty 放弃检查——因为无法确定父类是否真的有同名成员,避免误报。代码中还注释了"TODO: Support dynamic class literals"(overrides.rs),表明对动态类字面量的支持仍在计划中;- 成员必须是函数定义(
is_function_def):@override只应作用于方法,ty 在属性等非函数成员上不触发本规则。
诊断如何产生
check_explicit_overrides 的实际报错逻辑在 overrides.rs:
let Some(builder) = context.report_lint(&INVALID_EXPLICIT_OVERRIDE, definition.focus_range)
else {
return;
};
let mut diagnostic = builder.into_diagnostic(format_args!(
"Method `{}` is decorated with `@override` but does not override anything",
member.name
));
if let Some(decorator_span) = definition.focus_override_decorator_span {
diagnostic.annotate(Annotation::secondary(decorator_span));
}
diagnostic.info(format_args!(
"No `{member}` definitions were found on any superclasses of `{class}`",
member = member.name,
class = class.name(db)
));
生成的诊断由两部分组成:
- 主诊断指向方法名所在位置(
focus_range),消息为"MethodXis decorated with@overridebut does not override anything"; - 辅助标注(
Annotation::secondary)指向@override装饰器本身,帮助开发者快速定位到出问题的装饰器; - 附加信息(
info)补充说明"在Class的所有父类上都找不到member的定义",明确给出判定依据。
边界情况:哪些 @override 是合法的
@override 的合法性远不止"普通方法覆盖普通方法"。ty 通过 mdtest 用例(crates/ty_python_semantic/resources/mdtest/override.md)系统验证了多种组合,其快照见 crates/ty_python_semantic/resources/mdtest/snapshots/override.md_-typing.override-Basics(b7c220f8171f11f0).snap.snap)。从这些用例可以归纳出以下判定规则:
1. 覆盖 object 的内置方法
即使类没有显式父类,覆盖 object 成员也是合法的:
class A:
@override
def __repr__(self): ... # fine: overrides `object.__repr__`
2. @property 与装饰器顺序无关
@override 放在 @property 之上或之下都是合法的,ty 对二者一视同仁:
class Parent:
@property
def my_property1(self) -> int: ...
@property
def my_property2(self) -> int: ...
class Child(Parent):
@property
@override
def my_property1(self) -> int: ... # fine
@override
@property
def my_property2(self) -> int: ... # fine
3. classmethod / staticmethod
类方法与静态方法同样可以合法覆盖父类的对应成员:
class Child(Parent):
@classmethod
@override
def class_method1(cls) -> int: ... # fine
@staticmethod
@override
def static_method1() -> int: ... # fine
@override
@classmethod
def class_method2(cls) -> int: ... # fine
@override
@staticmethod
def static_method2() -> int: ... # fine
4. 覆盖普通类属性(属性遮蔽)
即使父类成员是普通赋值属性(baz = None),子类用方法覆盖它同样被视为合法覆盖:
class Parent:
baz = None
class Child(Parent):
@override
def baz(self): ... # fine: overrides `Parent.baz`
5. 签名被"有损装饰器"擦除的方法
如果父类方法被一个会抹掉原签名的装饰器(如 @lossy_decorator(fn: Callable[..., Any]) -> Callable[..., Any])包裹,子类仍可覆盖。有趣的是,无论 @override 与 @lossy_decorator 的相对顺序如何,ty 都能正确识别:
class Child(Parent):
@override
def decorated_1(self): ... # fine
@override
@lossy_decorator
def decorated_2(self): ... # fine
@lossy_decorator
@override
def decorated_3(self): ... # fine
6. 多重继承与 MRO
对于多重继承产生的中间类,ty 同样能沿着 MRO 找到被覆盖的方法。Grandchild(OtherChild) 继承自 OtherChild(Parent),只要 Parent 定义了同名成员,孙类上的 @override 依然合法(测试用例见 override.md 后续部分)。
7. 重载(overload)函数:任何一处带 @override 都算
Python 的 @typing.overload 会把一个逻辑函数拆成多个声明 + 一个实现。LocalOverrideDefinition 结构体(overrides.rs)专门处理了这种情况:
any_definition_has_override_decorator:只要任一重载或实现带@override,invalid-explicit-override就认为声明有效(即使@override落在"非焦点"的重载上);focus_definition_has_override_decorator:missing-override-decorator规则用的是这个更严格的字段,保证"@override放错重载位置"时仍会在实现上报告缺装饰器;focus_override_decorator_span:记录焦点定义上装饰器源码区间,用于诊断中的辅助标注。
焦点定义的选择由 overrides.rs 的 overriding_definition 决定:在源码文件(非 stub)中优先指向实现(implementation),在 .pyi stub 中指向第一个重载。这与类型检查器"stub 中重载即契约、源码中实现才生效"的惯例一致。
与相邻规则的协作:覆盖检查的完整拼图
invalid-explicit-override 并非孤立规则,它与同文件(diagnostic.rs)声明的一组覆盖相关规则共同构成完整的覆盖校验体系:
| 规则 | 检查方向 | 默认级别 |
|---|---|---|
invalid-explicit-override |
有 @override 但没覆盖任何东西(多标) |
error |
missing-override-decorator |
覆盖了父类成员却没写 @override(漏标) |
ignore |
invalid-method-override |
覆盖了但签名违反 Liskov 替换原则 | error |
invalid-attribute-override |
属性覆盖不兼容 | error |
override-of-final-method / override-of-final-variable |
覆盖了 @final 成员 |
error |
其中 invalid-explicit-override 与 missing-override-decorator 是一对"反向"规则:前者防止 @override 被误用,后者鼓励对真实的覆盖显式标注。二者在 overrides.rs 中共享 extract_local_override_definitions 提取逻辑,只是筛选条件相反。
与 Ruff 既有规则(如 ARG002)的相互作用
ty 的 FAQ 文档 专门讨论了 @override 与 Ruff 静态规则的关系:Ruff 的 ARG002(未使用的方法参数)建议删除或重命名未使用的参数,但这样的修改可能使子类方法不再接受父类方法的全部参数组合,从而触发 ty 的 invalid-method-override(LSP 违规)。解决方式正是给方法加上 @typing.override——Ruff 识别到该装饰器后,会认为该方法有明确的覆盖意图,从而对这类方法跳过 ARG002 等建议;而 @override 本身的正确使用则由 ty 的 invalid-explicit-override 兜底校验。这正是"显式覆盖声明"在双工具协作中的价值所在。
如何在 ty 中启用与配置
invalid-explicit-override 的默认级别就是 error,因此开箱即用,无需显式配置。需要调整行为时,可在 ty 的规则选择(rule selection)中控制,例如在配置文件中按需设置:
[tool.ty.rules]
# 保持默认 error 级别,也可显式写出
"invalid-explicit-override" = "error"
# 临时放行可改为 ignore
# "invalid-explicit-override" = "ignore"
规则的启用/关闭通过 rule_selection.is_enabled 查询(见 overrides.rs),它支持全局规则表以及针对特定文件路径的 [[tool.ty.overrides]] 覆盖段(后者在 crates/ty/tests/cli/rule_selection.rs 中有完整的 CLI 测试覆盖,包括通配符 include/exclude、优先级与继承语义)。
配置时还需注意两点:
- Python 版本:使用标准库的
typing.override需要环境声明python-version = "3.12"(或更高);更早版本请从typing_extensions导入; - 动态基类豁免:当父类无法静态解析时(
has_dynamic_superclass为真),ty 会跳过本检查,这是有意为之的保守策略,避免在信息不完整时误报。
小结
invalid-explicit-override 是 ty 覆盖检查体系中对"过度声明"的把关者:它以 error 级别的默认行为,确保每一个 @override 都名实相符。结合源码可以看到,它的实现覆盖了 object 内置方法、属性遮蔽、classmethod/staticmethod/@property 的任意装饰器顺序、多重继承 MRO、重载函数与 stub 文件等多种真实场景,并且与 missing-override-decorator(反向规则)、invalid-method-override(LSP 校验)以及 Ruff 的 ARG002 形成协同。对使用 ty 进行类型检查的开发者而言,正确运用 @override 不仅能让覆盖意图显式化,更能让拼写错误、父类重构遗留等隐蔽问题在检查阶段就无所遁形。