Ruff 仓库内 ty 类型检查器的override-of-final-method规则解析:拦截子类对@final方法的非法重写
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
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).finaldecorator"(见 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): ... # errorclass 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装饰器,而不是子类本身。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考