Ruff 仓库内 ty 类型检查器的 `override-of-final-method` 规则解析:拦截子类对 `@final` 方法的非法重写
override-of-final-method 是本仓库内置类型检查器 ty(源码位于 crates/ty_python_semantic)的一条稳定规则:一旦超类方法被 @final 修饰,就禁止任何子类以重定义、赋值类属性等方式覆盖它。本文以该规则的设计文档 override-of-final-method.md 为主体,结合 诊断源码、规则参考文档 与 mdtest 测试,完整讲解规则语义、默认行为、触发示例、诊断输出格式、自动修复边界以及装饰器顺序、重载、继承链等边界场景,帮助你彻底理解并正确规避这类类型错误。
规则是什么:检测对 @final 方法的重写
What it does:Checks for methods on subclasses that override superclass methods decorated with
@final.
规则原文给出的一句话定义是:检查子类中是否存在对"被 @final 装饰的超类方法"进行重写的成员定义。这条规则不关心运行期行为,它属于纯静态的类型检查范畴——无论被检查文件是普通 .py、桩文件 .pyi 还是 notebook,只要声明层面构成对 final 方法的重写就会触发。
在仓库实现中,该规则的声明位于 crates/ty_python_semantic/src/types/diagnostic.rs#L984-L991:
declare_lint! {
#[doc = include_str!("../../resources/lint_docs/override-of-final-method.md")]
pub(crate) static OVERRIDE_OF_FINAL_METHOD = {
summary: "detects overrides of final methods",
status: LintStatus::stable("0.0.1-alpha.29"),
default_level: Level::Error,
}
}
从中可以看到三件关键事实:
- 规则代码标识:
OVERRIDE_OF_FINAL_METHOD,命令行/诊断中呈现为override-of-final-method; - 默认等级为
error:这意味着在默认配置下运行检查就会直接报错,而不是警告或忽略; - 自 0.0.1-alpha.29 起即标记为稳定,不属于实验性规则。
官方生成的规则参考页位于 crates/ty/docs/rules.md#L4322-L4358,也确认了 Default level: error 与版本号。值得注意的机制细节:规则文档(lint_docs/*.md)通过 include_str! 宏直接嵌入到 Rust 源码的 doc 注释中,因此源码 cargo doc 生成的 API 文档与规则参考页内容天然一致,修改文档会同步进入两个出口。
为什么这是错误的:@final 的语义契约
Why is this bad:Decorating a method with
@finaldeclares to the type checker that it should not be overridden on any subclass.
给方法打上 @final 装饰器,等同于向类型检查器作出一个静态契约:该方法不允许在任何子类中被重写。原因在于 API 设计中,作者一旦用 @final 标记方法,通常意味着:
- 该方法的实现对类的内部不变量至关重要,覆盖会破坏对象状态的一致性;
- 该方法的行为被下游代码信任(例如文档承诺、序列化协议、生命周期钩子),重写会导致契约破裂;
- 作者未来可能将该方法实现替换为 C 扩展、缓存代理或其他不可子类化的形态。
注意一个容易被误解的点:@final 与 Final 类型限定符一样,是纯静态层面的声明,typing.final 在运行期几乎不做任何事(它返回原函数)。因此运行期你仍能"覆盖"它——类型检查器存在的意义正是在编译期而非运行期拦截这类设计错误。这一点与 typing/typing_extensions 的关系也很重要:规则测试标题即写作 "Tests for the @typing(_extensions).final decorator"(见 final.md),说明无论 from typing import final 还是 from typing_extensions import final,只要被识别为内建的 final 装饰器,规则都会生效。
最小触发示例
规则文档给出的最小示例完整如下(保留原文档全部内容):
from typing import final
class A:
@final
def foo(self): ...
class B(A):
def foo(self): ... # error
class B(A) 中重定义了超类 A 中被 @final 装饰的 foo,因此第 22 行会报出 override-of-final-method。把第 22 行注释为 # error 是 mdtest 的约定写法,实际诊断输出形如:
error[override-of-final-method]: Cannot override `A.foo`
help: Remove the override of `foo`
对这条规则的实际运行验证非常简单:克隆本仓库后用 crates/ty 下构建出的 ty 类型检查器直接检查上面的文件即可复现错误;仓库内的 crates/ty_python_semantic/resources/mdtest/snapshots 目录保存了对应的自动快照(snapshot)测试产物,可用于比对真实输出。
诊断输出:完整消息结构与多行定位标注
override-of-final-method 并非简单地打一行字,而是通过 report_overridden_final_method 函数(diagnostic.rs#L5159-L5343)构造一个带多级标注的诊断对象。从快照测试(对应 final.md 的"通过把函数赋给类变量来重写"场景)可以看到真实的完整输出:
error[override-of-final-method]: Cannot override `Base.method`
--> src/derived.py:5:5
|
5 | method = replacement_method # error: [override-of-final-method]
| ^^^^^^ Overrides a definition from superclass `Base`
info: `Base.method` is decorated with `@final`, forbidding overrides
--> src/base.py:4:5
|
4 | @final
| ------
5 | def method(self) -> None: ...
| ------ `Base.method` defined here
help: Remove the override of `method`
对照源码可以把这段输出逐层拆解:
- 主标题:
Cannot override{superclass_name}.{member}``(L5200-L5201); - 主标注消息(primary annotation):
Overrides a definition from superclass{superclass_name}``(L5202-L5204),定位在子类中被判定的"重写定义"上; - 简化消息(concise message):
Cannot override final member{member}from superclass{superclass_name}``(L5205-L5207),用于编辑器内联展示等只读一行文本的场合,与多行摘要消息不同; - Info 级子诊断:
{superclass_name}.{member}is decorated with@final, forbidding overrides(L5209-L5214),并把两个 secondary 标注落在超类上——一个是超类方法定义本身(...defined here),另一个是@final` 装饰器所在 span(L5239-L5243),让用户一眼看到"是谁的哪个装饰器在禁止重写"; - Help 文本:
Remove the override of {member}或针对重载/属性场景的变体(见下文自动修复节)。
源码里还有两个容易忽略的工程细节:
- 属性(property)重写要定位到 getter。当子类成员是以
@property重写 final 方法时,代码刻意做了一次"劫持":如果被检查定义是函数定义且子类类型是PropertyInstance,就改取其 getter 的定义进行报告,避免把错误标在 setter 上(L5170-L5185); - 超类与子类同名时的消歧。当子类在自己的模块里恰好与超类同名(例如多文件场景中
class Foo(module1.Foo)),superclass_name会改用超类的qualified_name全限定名来避免混淆(L5194-L5198),这一场景在测试 final.md#L153-L174 中被专门覆盖。
底层实现:如何判定成员"是 final"以及"构成重写"
要发出该诊断,类型推导器需要同时回答两个问题。
问题一:超类方法是不是 final? 判定发生在 report_overridden_final_method 内,它从超类同名方法定义中找出第一个带 final 装饰器的函数(L5216-L5221):
let first_final_superclass_definition = superclass_method_defs
.iter()
.find(|function| function.has_known_decorator(db, FunctionDecorators::FINAL))
.expect(
"At least one function definition in the superclass should be decorated with `@final`",
);
底层把装饰器"已知化"识别:源码 function.rs#L198 将 KnownFunction::Final(即 typing.final/typing_extensions.final)映射为 FunctionDecorators::FINAL,随后超类静态成员推断时即记录该标志(参考 static_literal.rs 与 class.rs 中的 KnownFunction::Final 分支)。对**桩文件(stub)**中的重载,还会调用 first_overload_or_implementation 取得首个重载或实现,以保证标注位置正确(L5223-L5229)。
问题二:子类成员是否构成对 final 方法的重写? 这一步在类成员合并/重写分析阶段完成——例如 overrides.rs#L733 在处理属性(property)重写时就会检查父类同名方法是否带 FunctionDecorators::FINAL。重写既包括常规的 def foo,也包括把函数赋给类变量(method = other_fn)、用 @property 替换等方法,它们最终都会汇聚到同一报告函数。
自动修复(Autofix)与刻意不修复的边界
不是所有命中都附带自动修复。源码中修复逻辑的取舍非常讲究(L5247-L5342),总结如下表:
| 子类重写形态 | 提供的 help | 自动修复 |
|---|---|---|
普通 def 方法(唯一成员) |
Remove the override of {member} |
有:将整个函数体替换为 pass(防止类体变成空语法错误) |
普通 def 方法(非唯一成员) |
同上 | 有:直接删除该函数定义区间 |
| 带多个重载的方法 | Remove all overloads for {member} / Remove all overloads and the implementation... |
有:逐个删除所有重载及实现(同样遵守"仅剩唯一成员则替换为 pass") |
| 带 setter 的属性 | Remove the getter and setter for {member} |
无 |
类变量赋值(method = replacement_method) |
Remove the override of {member} |
无 |
为何后两类刻意不给修复?源码注释给出了原因:
- 属性重写若删除 getter,还必须一并删除
@xxx.setter甚至@xxx.deleter的整段定义,而当前定义追踪尚未精确到足以保证安全(L5247-L5250); - 赋值式重写(
method = some_function)的函数可能定义在另一个文件里,安全修复应当是删除这条赋值语句,而删除跨文件赋值同样未实现(L5251-L5253)——测试 final.md#L329-L360 明确验证了"发出诊断但不提供 autofix"的行为。
此外,所有自动修复都被标记为 unsafe edits,并带 IsolationLevel::Group 隔离(L5291-L5297),确保批量修复时各修复互不冲突。
覆盖场景矩阵:装饰器顺序、特殊成员与继承链
规则文档之外,仓库在 mdtest/final.md 中用近 1500 行测试把规则边界钉得非常细,以下是最值得了解的几类场景。
1. 成员形态与装饰器顺序
@final 与 @property、@classmethod、@staticmethod 组合时无论先后顺序均被识别。测试(final.md#L43-L100)覆盖了 @final @property、@property @final、@classmethod @final、@staticmethod @final 等全部排列,子类中用属性 getter、classmethod、staticmethod 重写都会报错;属性场景下连 @my_property.setter / @my_property.deleter 补全也会被一并判定为重写。
2. 构造函数也受保护
__init__ 同样是方法。子类重定义被 @final 修饰的 __init__ 会触发同一规则(final.md#L362-L373)。
3. 重载(overload)方法的特殊约定
重载场景有明确规范(final.md#L176-L298):
- 桩文件中
@final应加在第一个重载上(stub.pyi的Good类);把@final放在后续重载上会被同族规则invalid-overload报错; - 运行期文件中
@final只应加在实现函数上; - 无论哪种形态,只要超类任一可达重载带 final,子类重写这些重载中任意一个都会被本规则捕获。
4. 继承链上的"只报一次"
如果 B(A) 重写了 A 的 final 方法且自己也加了 @final,然后 C(B) 再次重写,那么 C 处只发一条 override-of-final-method,不会沿链累积两条(final.md#L375-L396)。
5. 跨模块同名类的消歧
超类与子类同名、分处两个模块时(class Foo(module1.Foo)),诊断仍能正确指向 module1.Foo.f,这就是前文提到的 qualified_name 消歧逻辑的测试来源(final.md#L153-L174)。
6. 条件定义与可达性
规则只统计可达的 final 定义:
- 类体内
if coinflip(): @final def method1这类"可能定义"场景,只要某个分支定义了 final 版本,子类重写就会被报(final.md#L528-L606); - 基于
sys.version_info的静态分支中,不可达分支里的 final 定义不参与判定:在 Python 3.10 环境下,写在else:(即 3.10 以下)分支里的 final 方法可以被安全重写(final.md#L608-L655),重载同理(L657-L705)。
7. 已知但不报告的边界(实现备注)
测试中还以 TODO 形式标注了当前实现刻意从宽的场景:例如"final 方法被一个丢失签名(lossy)的装饰器包裹后再重写"(decorated_1/decorated_2)、"实例属性隐式覆盖 final 方法"(self.method: Any = 42)等处尚未发出诊断(final.md#L98-L100、L514-L526)。这些属于后续演进空间,不是规则的既定行为,不应当作约束依赖。
8. 只对"字面函数定义"传播 final(与其他检查器对齐)
若超类 final 方法先被赋给另一个类做类属性(class B: method = A.method),再在 C(B) 中重写 method,当前实现不报错(final.md#L300-L327)。测试注释说明这是刻意选择:mypy 与 pyright 同样不报,为最大化兼容性而跟随——尽管这与它们对 Final 限定符"跨作用域传播"的处理在语义上并不完全一致,未来可能调整。
相关规则家族:一条完整的 final 语义防线
override-of-final-method 并非孤立规则。在 diagnostic.rs#L966-L1045 附近,ty 围绕 final 建立了完整的规则家族,全部默认等级为 error:
| 规则 | 检查内容 | 文档 |
|---|---|---|
subclass-of-final-class |
继承被 @final 装饰的类 |
subclass-of-final-class.md |
override-of-final-method |
子类重写 @final 方法(本文) |
override-of-final-method.md |
override-of-final-variable |
子类覆盖 Final 类变量 |
override-of-final-variable.md |
ineffective-final |
以无法被类型检查器理解的方式调用 final() |
ineffective-final.md |
final-on-non-method |
把 @final 用在模块级/嵌套函数上 |
final-on-non-method.md |
abstract-and-final-method |
方法同时是 @abstractmethod 与 @final(自相矛盾) |
abstract-and-final-method.md |
abstract-method-in-final-class |
final 类残留未实现的抽象方法 | abstract-method-in-final-class.md |
设计上它们彼此咬合:例如"既抽象又 final"的方法是矛盾体(抽象方法必须被子类实现、final 方法禁止被重写),由独立规则负责;而"final 类带未实现抽象方法"则因为 final 类无法再被继承去补实现而成为一个独立缺陷。把这些规则合起来看,ty 对 @final 的静态语义覆盖是成体系的。
阅读与深入路径
- 规则正文(单一事实来源):lint_docs/override-of-final-method.md,被
include_str!嵌入 Rustdoc; - 规则声明与默认等级:diagnostic.rs#L984-L991;
- 报告函数与修复逻辑:diagnostic.rs#L5159-L5343;
- 规则参考页(含版本/等级元数据):crates/ty/docs/rules.md#L4322-L4358;
- 行为规格测试(约 1500 行边界场景):mdtest/final.md,自动生成的快照见 mdtest/snapshots;
- 修饰器识别映射:function.rs#L198。
实践要点回顾:不要重写任何被 @final 装饰的方法——包括 __init__、属性 getter、classmethod、staticmethod 及重载中的任一签名;如需对 final 方法做行为扩展,请改在子类中提供新的独立方法名,或推动上游放开该契约。若你只是阅读他人代码,看到 error[override-of-final-method] 时应优先把目光投向诊断中标注的超类 @final 装饰器,而不是子类本身。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00