ruff ty 类型检查器规则解析:@final不得用于非方法函数(final-on-non-method)
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
@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。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考