ty/ruff 类型检查器中的静态断言:深入解析 static-assert-error 检查
本篇技术指南以 Ruff 仓库中 ty(Python 类型检查器)的 static-assert-error lint 文档为核心,系统讲解 ty_extensions.static_assert 这一编译期断言机制的语义、使用方式、错误信息体系与底层实现原理。读完本文,你将掌握如何在类型检查阶段对表达式真值性做静态断言,理解 static_assert 判定"静态已知真值"的规则边界,并能结合源码定位到该检查在 ty_python_semantic crate 中的完整实现路径。
static-assert-error 检查的作用
static-assert-error 是 ty 类型检查器内置的一条 lint 规则,其唯一职责是:确保 static_assert 调用的参数在静态层面被证明为真。该规则的说明文档位于 crates/ty_python_semantic/resources/lint_docs/static-assert-error.md,并通过 include_str! 宏直接嵌入到 lint 声明中作为其内置文档:
declare_lint! {
#[doc = include_str!("../../resources/lint_docs/static-assert-error.md")]
pub(crate) static STATIC_ASSERT_ERROR = {
summary: "Failed static assertion",
status: LintStatus::stable("0.0.1-alpha.1"),
default_level: Level::Error,
}
}
该声明位于 crates/ty_python_semantic/src/types/diagnostic.rs,从中可以看到三个关键事实:
- lint 代码:
static-assert-error(在诊断输出与快照测试中以error[static-assert-error]形式出现); - 稳定性:自
0.0.1-alpha.1起即为稳定(stable)状态,属于类型检查器的长期基础能力; - 默认级别:
Level::Error,即静态断言失败默认直接报错。
在注册层面,该 lint 通过 crates/ty_python_semantic/src/types/diagnostic.rs 中的 registry.register_lint(&STATIC_ASSERT_ERROR) 被注册进 lint 注册表,作为 ty 语义分析阶段可报告的错误之一。
为什么需要静态断言:把类型检查当"可执行测试"
static_assert 调用代表用户向类型检查器发出的显式请求:如果参数无法在布尔上下文中被验证为 True,就让类型检查器发出错误。这与运行时 assert 形成鲜明对比:
- 运行时
assert在程序执行到该行时才校验,且依赖测试用例真正覆盖到这条路径; - 静态
static_assert在类型检查阶段(即代码尚未运行、甚至无法运行的环境中)就完成校验,失败会直接阻断类型检查。
因此它非常适合用于:
- 在类型层面编写"编译期测试"(type-level test),例如验证类型系统自身的行为是否符合预期;
- 表达类型层面的不变式(invariant),例如"这个类型别名必须能赋值给某类型";
- 在引入类型系统特性(交集类型、否定类型等)时,把约束固化为可被 CI 捕获的检查。
在 ty 的测试体系中,static_assert 大量出现在 crates/ty_python_semantic/resources/mdtest/ty_extensions.md 等 mdtest 文档中,配合 is_subtype_of、is_assignable_to 等类型谓词一起构成类型系统属性的"可执行规格说明"。
static_assert 的 API 签名
static_assert 从 ty_extensions 模块导入,其类型存根定义位于 crates/ty_vendored/ty_extensions/init.pyi:
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
两个参数的语义:
| 参数 | 类型 | 说明 |
|---|---|---|
condition |
object |
待校验的表达式,任何 Python 表达式均可传入,类型检查器会尝试静态求值其真值性 |
msg |
LiteralString | None |
可选的失败提示信息;必须是字符串字面量(或由字符串字面量拼接而成的表达式),传入非字面量字符串时检查器会回退到默认错误信息 |
该函数本身不参与运行,类型检查器会识别其 KnownFunction::StaticAssert 身份并在语义分析阶段特殊处理(详见下文源码剖析)。
基本用法与判定规则
static_assert 的参数会按 Python 的真值测试语义(truthiness)进行静态判定。根据 crates/ty_python_semantic/resources/mdtest/ty_extensions.md 中的测试用例,可以归纳出"静态已知真值"的判定边界:
可被静态验证为真的表达式
from ty_extensions import static_assert
from typing import TYPE_CHECKING
import sys
# 布尔字面量 / 布尔常量运算
static_assert(True)
static_assert(False or True)
static_assert(True and True)
# 比较表达式
static_assert(1 + 1 == 2)
static_assert("a" in "abc")
static_assert(n is None) # 在 n = None 之后
# 类型检查器已知的编译期常量
static_assert(TYPE_CHECKING)
static_assert(sys.version_info >= (3, 6))
必然失败的表达式(触发错误)
static_assert(False) # error: argument evaluates to `False`
static_assert(False or False) # error: argument evaluates to `False`
static_assert(False and True) # error: argument evaluates to `False`
static_assert(1 + 1 == 3) # error: argument evaluates to `False`
static_assert("d" in "abc") # error: argument evaluates to `False`
这正是原文档 static-assert-error.md 中第一个示例所对应的场景:static_assert(1 + 1 == 3) 的比较结果被推断为 Literal[False],因此报错。
真值恒定的字面量类型
Python 的真值测试规则同样适用于静态断言,以下类型在静态层面即被判定为"恒假":
static_assert(None) # error: argument of type `None` is always falsy
static_assert(0) # error: argument of type `Literal[0]` is always falsy
static_assert(()) # error: argument of type `tuple[()]` is always falsy
static_assert("") # error: argument of type `Literal[""]` is always falsy
static_assert(b"") # error: argument of type `Literal[b""]` is always falsy
而 1、(0,)、"a"、b"a" 等对应字面量则恒真,可以正常通过。
真值性模糊的表达式(同样报错)
当类型检查器无法确定表达式必然为真时,静态断言同样失败。原文档中的第二个示例正是这一情况:
# int(2.0 * 3.0) 被推断为 int 类型,int 的真值性不唯一
static_assert(int(2.0 * 3.0) == 6) # error: argument of type `bool` has an ambiguous static truthiness
值得注意:这里的表达式在运行时确实等于 6(即运行时为真),但类型检查器无法静态证明这一点,因此仍然报错——这正是"静态已知"四个字的严格含义。
此外,若参数类型本身不支持布尔转换,会触发 unsupported-bool-conversion 错误:
class InvalidBoolDunder:
def __bool__(self) -> int: # __bool__ 必须返回 bool,返回 int 不合法
return 1
static_assert(InvalidBoolDunder()) # error: [unsupported-bool-conversion]
错误信息体系:三条消息分支
static_assert 的失败诊断并非只有一种格式,而是依据推断类型与真值性结果生成三类针对性信息。这一逻辑完整实现在 crates/ty_python_semantic/src/types/function.rs 的 KnownFunction::StaticAssert 分支中:
KnownFunction::StaticAssert => {
let [Some(parameter_ty), message] = parameter_types else { return; };
let env = context.program_environment();
let truthiness = match parameter_ty.try_bool(db, env) {
Ok(truthiness) => truthiness,
Err(err) => {
err.report_diagnostic(...); // 如 unsupported-bool-conversion
return;
}
};
if let Some(builder) = context.report_lint(&STATIC_ASSERT_ERROR, call_expression) {
if truthiness.is_always_true() {
return; // 恒真:直接通过
}
let mut diagnostic = if let Some(message) = message
.and_then(Type::as_string_literal)
.map(|s| s.value(db))
{
// 分支一:自定义字符串字面量消息
builder.into_diagnostic(format_args!("Static assertion error: {message}"))
} else if *parameter_ty == Type::bool_literal(false) {
// 分支二:类型恰为 Literal[False]
builder.into_diagnostic("Static assertion error: argument evaluates to `False`")
} else if truthiness.is_always_false() {
// 分支三:类型恒假
builder.into_diagnostic(format_args!(
"Static assertion error: argument of type `{parameter_ty}` is always falsy", ...))
} else {
// 分支四:真值性模糊
builder.into_diagnostic(format_args!(
"Static assertion error: argument of type `{parameter_ty}` \
has an ambiguous static truthiness", ...))
};
// 附注:标注推断出的参数类型
diagnostic.annotate(Annotation::secondary(context.span(condition)).message(
format_args!("Inferred type of argument is `{}`", parameter_ty.display(db, env))));
...
}
}
从源码结构可以提炼出完整的错误信息路由表:
| 触发条件 | 错误信息 |
|---|---|
提供了字符串字面量 msg |
Static assertion error: <自定义消息> |
推断类型为 Literal[False] |
Static assertion error: argument evaluates to \False`` |
推断类型恒假(None、空字面量等) |
Static assertion error: argument of type \X` is always falsy` |
真值性模糊(如 int、bool 等) |
Static assertion error: argument of type \X` has an ambiguous static truthiness` |
| 类型不支持布尔转换 | [unsupported-bool-conversion] Boolean conversion is not supported... |
诊断输出还始终附带一条 Annotation 二级标注,显示 Inferred type of argument is ...,帮助开发者直接看到类型检查器推断出的参数类型,便于定位问题根源。
自定义错误消息的使用与限制
用户可以传入 msg 参数定制失败信息:
from ty_extensions import static_assert
# error: "Static assertion error: I really want this to be true"
static_assert(1 + 1 == 3, "I really want this to be true")
error_message = "A custom message "
error_message += "constructed from multiple string literals"
# error: "Static assertion error: A custom message constructed from multiple string literals"
static_assert(False, error_message)
msg 的类型是 LiteralString,因此由多个字符串字面量拼接而成的表达式仍可被识别。但一旦无法静态推断出字符串字面量值,检查器就会回退到默认消息:
shouted_message = "A custom message".upper() # .upper() 结果无法静态确定
# error: "Static assertion error: argument evaluates to `False`"
static_assert(False, shouted_message)
这一回退行为对应源码中的 message.and_then(Type::as_string_literal) 逻辑:只有能被规约为字符串字面量的参数才会走自定义消息分支。
诊断输出快照
以下是 static-assert-error 在四种典型场景下的真实诊断输出(来自 crates/ty_python_semantic/resources/mdtest/ty_extensions.md 的快照测试):
1. 参数求值为 False(含自定义消息):
error[static-assert-error]: Static assertion error: argument evaluates to `False`
--> src/mdtest_snippet.py:7:1
|
7 | static_assert(1 > 2)
| ^^^^^^^^^^^^^^-----^
| |
| Inferred type of argument is `Literal[False]`
2. 参数恒假:
error[static-assert-error]: Static assertion error: argument of type `Literal[""]` is always falsy
--> src/mdtest_snippet.py:11:1
|
11 | static_assert("")
| ^^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `Literal[""]`
3. 真值性模糊:
error[static-assert-error]: Static assertion error: argument of type `int` has an ambiguous static truthiness
--> src/mdtest_snippet.py:13:1
|
13 | static_assert(secrets.randbelow(2))
| ^^^^^^^^^^^^^^--------------------^
| |
| Inferred type of argument is `int`
这些快照由 mdtest 框架自动比对校验,确保诊断输出与实现始终一致,任何消息格式的改动都会在测试中被捕获。
典型实战:结合类型谓词做"类型级单元测试"
static_assert 最强大的用法是与 ty_extensions 提供的类型谓词配合,把类型系统属性固化为编译期可验证的断言。这些谓词返回 Literal[True] 或 Literal[False],恰好满足 static_assert 对"静态已知真值"的要求。以下用例均来自 ty_extensions.md:
from ty_extensions import static_assert
from ty_extensions._internal import (
is_equivalent_to, is_subtype_of, is_assignable_to,
is_disjoint_from, is_singleton,
)
from typing import Any, Literal, Union, Never
# 类型等价性
static_assert(is_equivalent_to(int | str, Union[int, str]))
static_assert(not is_equivalent_to(int, str))
# 子类型关系
static_assert(is_subtype_of(bool, int))
static_assert(not is_subtype_of(str, int))
class Base: ...
class Derived(Base): ...
static_assert(is_subtype_of(Derived, Base))
static_assert(not is_subtype_of(Base, Derived))
# 可赋值性
static_assert(is_assignable_to(int, Any))
static_assert(is_assignable_to(Any, str))
# 不相交性
static_assert(is_disjoint_from(None, int))
static_assert(not is_disjoint_from(Literal[2] | str, int))
# 单例类型
static_assert(is_singleton(None))
static_assert(is_singleton(Literal[True]))
static_assert(not is_singleton(int))
这种组合相当于在类型层面编写单元测试:一旦类型推断逻辑改变导致某个性质不再成立,类型检查就会以 static-assert-error 报错,从而在回归发生的第一时间暴露问题。ty 自身的类型系统测试(如交集类型、否定类型 Not、AlwaysTruthy/AlwaysFalsy 等特性的 mdtest 用例)正是依赖这一机制来锁定预期行为的。
一个容易被忽略的陷阱:类字面量与 type[...]
static_assert 校验的是表达式的推断类型,这一语义在涉及类字面量时容易踩坑:
from ty_extensions._internal import TypeOf, is_subtype_of
# 错误:这里测试的是"类型 str 是否是 type[str] 的子类型",恒为假
# error: "Static assertion error: argument of type `ConstraintSet[Literal[False]]` is always falsy"
static_assert(is_subtype_of(str, type[str]))
# 正确:TypeOf[str] 取表达式 str 的推断类型(类字面量类型),再测试其子类型关系
static_assert(is_subtype_of(TypeOf[str], type[str]))
这也再次印证了原文档"参数必须被静态验证为真"的严格要求:任何无法静态证明的表达式——无论是恒假、真值模糊还是推导链上某个环节不可判定——都会触发 static-assert-error。
实现要点回顾
- 入口:
static_assert被识别为KnownFunction::StaticAssert,类型检查器在函数调用语义分析阶段进入专门分支(crates/ty_python_semantic/src/types/function.rs); - 真值判定:通过
Type::try_bool尝试对参数类型做布尔化求值,返回Truthiness;无法求值(如__bool__返回类型非法)时上报unsupported-bool-conversion; - 错误路由:恒真直接通过;否则依据"字符串字面量消息 →
Literal[False]→ 恒假 → 模糊"的优先级生成诊断(见 crates/ty_python_semantic/src/types/function.rs); - lint 元数据:
static-assert-error,稳定版本0.0.1-alpha.1,默认Error级别(crates/ty_python_semantic/src/types/diagnostic.rs); - 类型存根:API 签名定义于 crates/ty_vendored/ty_extensions/init.pyi;
- 行为规格:全部判定规则与输出快照由 crates/ty_python_semantic/resources/mdtest/ty_extensions.md 中的 mdtest 用例锁定。
理解 static-assert-error 与 static_assert,就掌握了 ty 在"类型系统自身可测试性"上的设计哲学:类型检查器不仅能检查业务代码,还能对类型系统本身的规则做自检,而这一切都发生在程序运行之前。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
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