首页
/ Ruff 仓库内 ty 类型检查器的 `override-of-final-method` 规则解析:拦截子类对 `@final` 方法的非法重写

Ruff 仓库内 ty 类型检查器的 `override-of-final-method` 规则解析:拦截子类对 `@final` 方法的非法重写

2026-09-08 21:05:22作者:邵娇湘

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 @final declares to the type checker that it should not be overridden on any subclass.

给方法打上 @final 装饰器,等同于向类型检查器作出一个静态契约:该方法不允许在任何子类中被重写。原因在于 API 设计中,作者一旦用 @final 标记方法,通常意味着:

  1. 该方法的实现对类的内部不变量至关重要,覆盖会破坏对象状态的一致性;
  2. 该方法的行为被下游代码信任(例如文档承诺、序列化协议、生命周期钩子),重写会导致契约破裂;
  3. 作者未来可能将该方法实现替换为 C 扩展、缓存代理或其他不可子类化的形态。

注意一个容易被误解的点:@finalFinal 类型限定符一样,是纯静态层面的声明,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} 或针对重载/属性场景的变体(见下文自动修复节)。

源码里还有两个容易忽略的工程细节:

  1. 属性(property)重写要定位到 getter。当子类成员是以 @property 重写 final 方法时,代码刻意做了一次"劫持":如果被检查定义是函数定义且子类类型是 PropertyInstance,就改取其 getter 的定义进行报告,避免把错误标在 setter 上(L5170-L5185);
  2. 超类与子类同名时的消歧。当子类在自己的模块里恰好与超类同名(例如多文件场景中 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#L198KnownFunction::Final(即 typing.final/typing_extensions.final)映射为 FunctionDecorators::FINAL,随后超类静态成员推断时即记录该标志(参考 static_literal.rsclass.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.pyiGood 类);把 @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 的静态语义覆盖是成体系的。

阅读与深入路径

实践要点回顾:不要重写任何被 @final 装饰的方法——包括 __init__、属性 getter、classmethod、staticmethod 及重载中的任一签名;如需对 final 方法做行为扩展,请改在子类中提供新的独立方法名,或推动上游放开该契约。若你只是阅读他人代码,看到 error[override-of-final-method] 时应优先把目光投向诊断中标注的超类 @final 装饰器,而不是子类本身。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391