首页
/ ty/ruff 类型检查器中的静态断言:深入解析 static-assert-error 检查

ty/ruff 类型检查器中的静态断言:深入解析 static-assert-error 检查

2026-09-08 21:32:01作者:平淮齐Percy

本篇技术指南以 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_ofis_assignable_to 等类型谓词一起构成类型系统属性的"可执行规格说明"。

static_assert 的 API 签名

static_assertty_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.rsKnownFunction::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`
真值性模糊(如 intbool 等) 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 自身的类型系统测试(如交集类型、否定类型 NotAlwaysTruthy/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-errorstatic_assert,就掌握了 ty 在"类型系统自身可测试性"上的设计哲学:类型检查器不仅能检查业务代码,还能对类型系统本身的规则做自检,而这一切都发生在程序运行之前。

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

项目优选

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