ruff ty 类型检查器规则解析:`@final` 不得用于非方法函数(final-on-non-method)
@final 是 Python 类型系统里用来表达"禁止继承/禁止覆写"语义的关键装饰器,但它并非对任何函数都有效:把它贴到模块级函数或嵌套函数上,不会产生任何运行时与类型层面的约束,只会让代码意图落空。本文以 ruff 仓库中 ty(基于 Rust 的高性能类型检查器)内置的 final-on-non-method 规则为对象,讲解该规则的检测目标、触发条件、消息格式、源码实现与正确写法,帮助你写出语义自洽的类型标注代码,也能让读者理解在 ruff 项目中 @final 相关规则的实现与测试思路。
本规则在类型检查器 ty 中默认启用(级别为 Error),定位在 final-on-non-method.md 这一规则文档中,可直接作为开发与教学的一手资料使用。
一、规则是什么:检测施加在非方法函数上的 @final
规则文档开宗明义(crates/ty_python_semantic/resources/lint_docs/final-on-non-method.md):
- What it does:检查
@final装饰器是否被应用到了非方法(non-method)函数上; - Why is this bad:
@final装饰器只在**方法(method)和类(class)**上才有意义。把它应用到模块级函数或嵌套函数上不会有任何效果,且很可能是写作者的失误。
从声明源码看,规则注册在 crates/ty_python_semantic/src/types/diagnostic.rs:
declare_lint! {
#[doc = include_str!("../../resources/lint_docs/final-on-non-method.md")]
pub(crate) static FINAL_ON_NON_METHOD = {
summary: "detects `@final` applied to non-method functions",
status: LintStatus::stable("0.0.20"),
default_level: Level::Error,
}
}
值得注意的工程细节是:该 Markdown 文档不是游离于代码之外的说明书,而是通过 include_str! 直接内嵌为规则的 doc 注释,再在 diagnostic.rs 中通过 registry.register_lint(&FINAL_ON_NON_METHOD) 完成注册。也就是说,lint_docs 目录下的每份规则文档与代码中的 Lint 一一对应,既是文档又是 Rust 编译单元的一部分。
规则元数据汇总如下:
| 项目 | 值 |
|---|---|
| 规则代码(诊断输出中的标识) | final-on-non-method |
| 静态变量名 | FINAL_ON_NON_METHOD |
| summary | 检测被应用到非方法函数上的 @final |
| 默认级别 | Error(默认开启,报错级) |
| 状态 | stable("0.0.20")(自该版本起稳定) |
二、典型误用示例与诊断消息
规则文档给出的最小示例直接命中"模块级函数"这一场景(final-on-non-method.md):
from typing import final
# @final is not allowed on non-method functions
@final # error
def my_function() -> int:
return 0
在 ty 的类型检查测试语料中,@final 施加到非方法函数的错误被更细致地展开为三类场景(crates/ty_python_semantic/resources/mdtest/final.md):
from typing import final
@final # error: [final-on-non-method] "`@final` cannot be applied to non-method function `func1`"
def func1(): ...
# Nested function decorated with `@final` is also invalid
def outer():
@final # error: [final-on-non-method]
def inner(): ...
# A function nested inside a method is also not a method
class F:
def method(self):
@final # error: [final-on-non-method]
def not_a_method(): ...
可以看到 ty 实际输出的错误代码为 [final-on-non-method],完整消息为 `@final` cannot be applied to non-method function `func1`。这里有个非常容易误解的边界:
- 模块顶层函数(
func1)→ 报错; - 普通函数体内定义的嵌套函数(
inner)→ 报错; - 方法体内定义的嵌套函数(
not_a_method)→ 同样报错!因为它的直接宿主作用域是"方法这个函数作用域",而不是"类作用域"。所谓 method,指的是直接声明在类体中的函数成员,方法体内的局部函数并不算方法。
三、为什么 @final 只对方法和类有意义
typing.final / typing_extensions.final 的语义是"标记一个方法或类为 final,禁止子类覆写/继承"。类型系统里它的可检验语义只有两类:
- 禁止对 final 方法做覆写(override)——由
override-of-final-method规则负责; - 禁止继承 final 类——由
subclass-of-final-class规则负责。
一个模块级函数或嵌套函数既没有"子类覆写"这一概念,也没有"继承者"概念;@final 不会影响其可调用性、参数检查或返回值推断。因此把它写在非方法函数上,等于向读者宣称一条不存在且永远不会被验证的约束——检查器将其视为错误,是合理的保守选择。
在 crates/ty_python_semantic/src/types/diagnostic.rs 中可以看到与 final 语义相关的完整规则族:
| 规则代码 | summary | 关注点 |
|---|---|---|
override-of-final-method |
检测对 final 方法的覆写 | 子类不可覆写 final 方法 |
override-of-final-variable |
检测对 Final 类变量的覆写 |
子类不可覆写 final 类变量 |
subclass-of-final-class |
检测 final 类的子类 | final 类不可被继承 |
ineffective-final |
检测类型检查器无法解释的 final() 调用 |
无效的 final 表达 |
final-without-value |
检测没有赋值的 Final 声明 |
Final 声明形式错误 |
abstract-and-final-method |
检测既 abstract 又 final 的方法 | 两种语义互斥 |
final-on-non-method 正是这个规则族中负责"收窄 @final 合法使用范围"的一环:它不负责验证 final 语义是否被破坏,而是从源头拦截"把 @final 用错了对象"的低级错误。
四、合法用法:方法和类
与上述误用相对,@final 在以下位置是合法的:
from typing import final
# 1) 普通方法(实例方法)
class Service:
@final
def run(self) -> None: ...
# 2) 类方法 / 静态方法 / 属性——装饰器顺序无关紧要
@final
@classmethod
def create(cls) -> "Service": ...
@final
@property
def version(self) -> str: ...
# 3) 构造方法同样适用(禁止子类改写 __init__)
@final
def __init__(self) -> None: ...
# 4) 类
@final
class ImmutableConfig:
pass
以上合法性均可以从 ty 自身的测试断言得到印证。例如 mdtest/final.md 中的父类同时用 @final 标注了实例方法、property(多种装饰器顺序)、@classmethod、@staticmethod,且这些声明均不产生 final-on-non-method 错误;而子类对它们的覆写则统一触发 [override-of-final-method]。这组对照测试清楚地说明了两类规则的职责分工:前者把守装饰器放置位置,后者把守覆写行为。
此外,测试还覆盖了 @final 与 @overload 的组合规则(mdtest/final.md):stub 文件里 @final 应放在第一个 overload 上,运行期文件里 @final 应只放在实现函数上——放错位置会触发 [invalid-overload] 而非本规则。
五、源码级实现:ty 如何在推断函数时触发该诊断
final-on-non-method 的触发点位于函数类型推断构造器 crates/ty_python_semantic/src/types/infer/builder/function.rs:
// Check for `@final` applied to non-method functions.
// `@final` is only meaningful on methods and classes.
if let Some(final_decorator) = final_decorator
&& !self
.index
.scope(self.scope().file_scope_id(db))
.kind()
.is_class()
&& let Some(builder) = self
.context
.report_lint(&FINAL_ON_NON_METHOD, final_decorator)
{
let mut diagnostic = builder.into_diagnostic(format_args!(
"`@final` cannot be applied to non-method function `{name}`",
));
diagnostic.info("`@final` is only meaningful on methods and classes");
}
实现要点可以拆解为三层:
-
识别
@final装饰器。在遍历装饰器列表(function.rs)时,若某装饰器推导出的类型是Type::FunctionLiteral且其known归属为KnownFunction::Final,就把它记为final_decorator。由于final的"知名函数"识别同时覆盖typing.final与typing_extensions.final,两种导入来源都会命中,这一点在 mdtest/final.md 中被同时验证(typing与typing_extensions各有用例)。 -
判断函数所处的作用域类别。
self.scope().file_scope_id(db)拿到的是定义这条函数语句所在作用域:模块级函数处于 module scope,嵌套函数处于外层函数(method 或 function)作用域,只有直接写在类体里的成员函数其所在作用域才是 class scope。代码据此用.kind().is_class()判负——不是类作用域,就说明这不是一个"方法"。 -
生成诊断。命中后通过
report_lint(&FINAL_ON_NON_METHOD, final_decorator)上报,并把诊断锚点(span)落在@final装饰器本身,而不是函数名上,便于用户在长函数前快速定位出错的那一行装饰器;消息体带上函数名,附注(info)则说明正确语义。
从代码结构可以推断,@final 放在普通(非类)作用域内会被直接拦截并 continue 跳过后续装饰器处理,因此该函数不会携带 final 标记进入任何覆写/继承判定——这与规则文档"施加无效果"的表述一致:错误在声明处即被捕获,不会留下隐患蔓延到下游检查。
六、如何触发与验证:mdtest 测试与 CLI
本仓库把文档、源码、测试三者的关系组织得很清晰:
lint_docs/下的 Markdown:规则的人类可读文档(即本文主体),被include_str!嵌入代码;mdtest/下的 Markdown:规则的可执行测试语料。其格式约定见 crates/ty_test/README.md,通过 resources/README.md 可知它们由tests/mdtest.rs集成测试执行;mdtest/测试通过行内注释断言结果,语法形如# error: [rule-code] "message"(解析逻辑见 crates/mdtest/src/assertion.rs),规则文档中的代码块同样遵守这套断言注释。
如果你想在自己的项目中复现本规则,ty 检查器就构建在本仓库的 crates/ty 中,其 CLI 提供了 check 与 explain 子命令(入口见 crates/ty/src/lib.rs,主程序见 crates/ty/src/main.rs)。大致用法:
# 对当前目录执行类型检查,命中 final-on-non-method 时会以 Error 级别报出
ty check .
# 查看某条规则的说明文档
ty explain final-on-non-method
验证脚本可以这样组织:
# misuse.py
from typing import final
@final # error: [final-on-non-method]
def helper() -> int:
return 42
对 misuse.py 运行 ty check,你会得到与 mdtest/final.md 中完全一致的诊断(规则代码、消息文本、装饰器行号位置均已由 mdtest 快照锁定)。由于默认级别是 Error,此类代码会被当作类型检查错误直接拦截,而不会静默通过。
七、修复方式与编写建议
该规则不提供自动修复(autofix)——ty 的选择是:与其替你删除一行可能有争议的装饰器,不如把语义决策留给开发者。正确做法很简单:
- 删除模块级/嵌套函数上的
@final; - 若函数的真实意图就是"不希望被子类覆写",那么它本应作为方法直接声明在类体中(此时
@final合法且生效); - 若想约束的是"变量不可被覆写/重新绑定",应改用类型限定符
Final(对应final-without-value、override-of-final-variable等规则的语义域),而不是@final。
给代码审查与教学场景的三点经验:
- 方法 ≠ 嵌套在函数里的函数。判断依据是"定义语句所在作用域是否为类作用域"(源码判据见 function.rs),而非函数长什么样。
@final的最佳位置紧贴被约束的方法/类定义。绕经lossy_decorator、多重恒等装饰器之后再识别 final 是类型检查器的实现负担(见 mdtest/final.md 的边界用例),从源头上把@final写在声明处最省心。- 不要把
@final与@abstractmethod混用:两者语义互斥(abstract-and-final-method),抽象的必须被覆写,final 的禁止被覆写,鱼与熊掌不可兼得(mdtest/final.md)。
总结
final-on-non-method 是 ruff 仓库中 ty 类型检查器针对 @final 误用场景的第一道防线。它以 Error 级别拦截一切施加在模块级函数与嵌套函数上的无效 @final,通过与 override-of-final-method、subclass-of-final-class 等规则的配合,共同保障 final 语义只在方法/类这两个真正有继承语义的对象上被表达。想要深入研究的读者,可以从三条线索继续探索本仓库:规则文档 lint_docs/final-on-non-method.md、注册声明 types/diagnostic.rs、实现与测试 types/infer/builder/function.rs 与 mdtest/final.md。
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