ty 类型检查器unsound-return-statement规则:把 fully static 返回类型变成类型安全边界
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
unsound-return-statement是 ty(本项目仓库中基于 Rust 实现的 Python 类型检查器)提供的一条进阶 soundness lint 规则,用于检测那些"类型上可赋值、却并非子类型"的不健全return语句——典型场景就是把一个被推断为Any的表达式直接返回给一个静态返回类型的函数。本文将完整讲解该规则的行为、触发边界、修复方式与底层实现,帮助你在 Python 类型检查体系中构建更严格的"类型化边界",阻断Any在函数之间悄然渗透。
规则概述:它检查什么
unsound-return-statement检测的是:return语句所返回的类型并不是函数注解返回类型的[子类型],从而构成"不健全(unsound)返回"。
这条 lint 是invalid-return-type的更严格版本:
invalid-return-type检查的是返回值根本无法赋值(assignable)给注解返回类型的情况;unsound-return-statement进一步要求返回值必须是注解类型的子类型,即使它能够通过赋值检查(例如Any对int是可赋值的),只要不是子类型,就会报错。
两者在 ty 的诊断体系中互为表里:invalid-return-type对应更宽松的"可赋值性"检查,unsound-return-statement对应更严格的"子类型 + 完全静态"检查。相关规则文档参见 invalid-return-type.md。
为什么需要这条规则:Any的静默渗透
默认情况下,类型检查器只要求返回值的推断类型**可赋值(assignable)**给函数注解的返回类型。问题在于,Any类型与任何类型都可赋值,于是只要代码里有一个表达式被推断为Any,错误类型就可能顺着函数调用链悄悄扩散,最终在运行时才暴露,而类型检查器完全不会察觉:
from typing import Any def returns_any() -> Any: return "foo" def returns_int() -> int: # error: "Unsound return statement: `Any` is not a subtype of `int`" return returns_any() # fails at runtime, even though the type checker infers both operands as being of type `int`! returns_int() + 42在这个例子中,returns_any()的返回类型被推断为Any,而Any可赋值给int,所以默认检查不会报错;但运行时returns_int()实际返回的是字符串"foo","foo" + 42直接抛出TypeError。
启用unsound-return-statement后,ty 会在returns_int中的return returns_any()处报错,因为Any不是int的子类型。这相当于把每个声明了**完全静态(fully static)**返回类型的函数,当作代码的"类型化边界":Any不允许无声无息地跨过这条边界,不健全性必须在离源头(本例中即returns_any的返回类型声明)最近的位置被拦截。
触发条件:仅作用于 fully static 返回类型
这条规则只应用于返回类型被注解为[完全静态(fully static)]的函数。如果返回类型中的任何位置出现Any或Unknown——无论是显式还是隐式——规则都不会触发:
from typing import Any def returns_any() -> Any: return "foo" # error: [missing-type-argument] def returns_unparameterized_tuple() -> tuple: # no error, since the return type is implicitly `tuple[Unknown, ...]` # (which is what the `missing-type-argument` error is complaining about on the line above!) return returns_any() def returns_list_of_any() -> list[Any]: # no error, since the return type is explicitly `list[Any]` return returns_any()注意上述第二个例子中的微妙之处:tuple(未参数化)的返回类型被隐式地视为tuple[Unknown, ...],其中含Unknown,因此unsound-return-statement不触发——此时应该由另一条规则missing-type-argument来指出"缺少类型参数"这一更根本的问题。
生成器函数的例外情形
从实现上看,ty 对生成器函数也应用同类检查:在 function.rs 中,若生成器的返回类型(如Generator[int])是 fully static 类型,则每个return语句同样要满足"子类型"要求,否则报告 unsound return。也就是说,"fully static 类型化边界"同样适用于生成器中的返回值。
与其他规则的协同
这条规则与 ty 的missing-type-argument、unsound-assignment规则配合得特别好,也与 Ruff 的ANN201、ANN202、ANN204、ANN205、ANN206五条"缺失返回类型注解"规则互补。同时启用这些规则,可以有效降低return语句把不健全性"泄漏"出函数的概率——除非该函数被显式地注解为动态类型(如-> Any或-> tuple[Any]),否则Any很难穿过返回类型边界:
unsound-assignment:检查赋值给变量时,值不是变量声明类型子类型的"不健全赋值",参见 unsound-assignment.md;unsound-yield:检查yield/yield from表达式的值不是生成器注解 yield 类型子类型的"不健全产出",参见 unsound-yield.md;ANN201等 Ruff 规则:强制公开函数、私有函数、特殊方法、静态方法、类方法显式写出返回类型注解——只有注解存在,才能形成可检查的"类型化边界"。
从概念上看,unsound-return-statement与 mypy 的no-any-return错误码是同类规则:mypy 的--strict模式会启用它,也可以单独通过--warn-return-any选项开启。
示例与修复:用类型收窄消除诊断
触发示例:
from typing import Any def returns_any() -> Any: return 42 def returns_int() -> int: # error: "Unsound return statement: `Any` is not a subtype of `int`" return returns_any()修复方式:在return之前把类型收窄(narrow)为int的子类型。通过assert isinstance(...)进行类型收窄后,my_int的推断类型变为交集类型Any & int,而Any & int是int的子类型,因此不再报错:
from typing import Any from typing_extensions import reveal_type def returns_any() -> Any: return 42 def returns_int() -> int: my_int = returns_any() assert isinstance(my_int, int) reveal_type(my_int) # revealed: Any & int return my_int # no error: `Any & int` is a subtype of `int`这也是 ty 在诊断中给出的修复建议:在返回语句之前使用assert对类型进行收窄。
源码实现:诊断的生成逻辑
规则的完整定义位于 diagnostic.rs,其中记录了:
summary:检测返回类型不是函数注解返回类型子类型的 return 语句;status:LintStatus::stable("0.0.70"),即自 ty 0.0.70 起作为稳定规则提供;default_level:Level::Ignore,默认不启用。
具体的诊断报告函数是report_unsound_return_statement,见 diagnostic.rs。它的关键行为包括:
- 特殊注解归一化:如果函数的返回注解是
TypeIs或TypeGuard类型,其实际返回类型归一化为bool(这类函数被预期返回bool),再参与后续比较与展示; - 诊断消息:简明消息为
Unsound return statement: `{actual}` is not a subtype of `{expected}`,并在返回类型位置附加辅助标注 "Expected a subtype of ... because of the return type"; - 解释信息:明确指出
{actual}` is assignable to `{expected}`, but not a subtype of `{expected}`,说明"可赋值但不健全"的本质; - 修复帮助:
help消息建议 "Consider using anassertto narrow the type prior to thereturnstatement"。
触发路径:先查赋值性,再查子类型
在函数体推断流程中,ty 对每个显式return语句执行两级检查,见 function.rs:
对每个 return 语句: 1. 若返回值 not assignable 到返回类型 -> 报告 invalid-return-type 2. 否则,若: - unsound-return-statement 规则已启用,且 - 公共返回类型是 fully static 类型,且 - 返回值不是返回类型的纯冗余(pure redundancy)类型 -> 报告 unsound-return-statement其中TypeRelation::Redundancy { pure: true }表示"纯冗余"关系:只有类型真正是返回类型的子类型时才算满足。这一实现同时处理了NotImplemented的特殊过滤(先从联合类型中剔除NotImplemented再参与检查)。代码注释还特别说明:UNSOUND_YIELD和UNSOUND_ASSIGNMENT的实现与之几乎相同,改动时需要同步更新——这印证了三条 unsound 规则在架构上的一致性。
默认级别与适用人群
unsound-return-statement默认是关闭的(default_level: Level::Ignore),需要用户显式启用。它的定位是:为希望从类型检查器获得额外健全性保障的进阶用户设计,而不是给刚刚开始对 Python 代码使用类型检查器的用户。理由很直接——它要求返回值是注解类型的子类型,比业界常见的"可赋值"标准严格得多,在大量存在Any交互的存量代码库中可能会产生较多噪音。
对新手团队,建议先用默认的invalid-return-type(默认级别为Error)保证基本的返回类型正确性;当代码库的类型注解覆盖率足够高后,再考虑启用unsound-return-statement与其配套规则,把 fully static 返回类型真正变成不可穿透的类型安全边界。完整的规则文档可从 crates/ty/docs/rules.md 中的unsound-return-statement一节查看。
相关规则速览
| 规则 | 检查对象 | 与unsound-return-statement的关系 |
|---|---|---|
invalid-return-type | 返回值无法赋值给注解返回类型 | 本规则的宽松版,默认启用(Error 级别) |
unsound-return-statement | 返回值不是注解返回类型的子类型 | 本文主题,默认禁用 |
unsound-yield | yield/yield from值不是注解 yield 类型的子类型 | 针对生成器的同类规则 |
unsound-assignment | 变量赋值不是声明类型的子类型 | 针对赋值的同类规则 |
missing-type-argument | 泛型容器缺少类型参数(隐式Unknown) | 与隐式Unknown场景互补 |
各规则的行为与边界可对照阅读 unsound-yield.md、unsound-assignment.md 与 invalid-return-type.md,三者在文档与实现上互相印证,共同构成 ty 的"子类型级"健全性检查家族。
【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考