首页
/ 深入解析 ty 的 `zero-stepsize-in-slice` 诊断:从 mdtest 用例到源码实现

深入解析 ty 的 `zero-stepsize-in-slice` 诊断:从 mdtest 用例到源码实现

2026-09-11 17:31:34作者:裴锟轩Denise

ty(Rust 编写的 Python 类型检查器,Astral 出品,代码位于当前仓库 crates/ty_python_semantic 等 crate 中)会在已知内置序列类型被以步长为零(step=0)的方式切片时,报告 zero-stepsize-in-slice 诊断。本文以仓库内 mdtest 用例 stepsize_zero.md 为核心,完整拆解该诊断的触发范围、非穷尽性设计、底层实现(subscript.rs / diagnostic.rs)以及同系列其他测试用例,帮助读者理解 ty 如何在类型检查阶段提前发现这类必然在运行期抛出的 ValueError

诊断是什么:检测切片步长为零

在 Python 中,内置序列类型(listtuplestrbytesbytearrayrangememoryview)在进行 seq[start:stop:0] 这类切片操作时,会抛出 ValueError: slice step cannot be zero。这是解释器层面的硬性限制,与序列内容无关——只要步长为零,就必然失败。

ty 提供的 zero-stepsize-in-slice 诊断会在类型检查阶段静态捕获这一错误,而不是等程序运行到该行才崩溃。该 lint 的官方说明(见 lint_docs/zero-stepsize-in-slice.md)明确指出:

  • What it does:当切片操作已知必然失败时,检查步长是否为零。
  • Why is this bad:Python 内置序列类型在步长为零时抛 ValueError
  • Known problems:该检查并非穷尽式(not exhaustive)。

diagnostic.rs 的 lint 注册代码可以看到它的完整元数据:

declare_lint! {
    #[doc = include_str!("../../resources/lint_docs/zero-stepsize-in-slice.md")]
    pub(crate) static ZERO_STEPSIZE_IN_SLICE = {
        summary: "detects a slice step size of zero",
        status: LintStatus::stable("0.0.1-alpha.1"),
        default_level: Level::Error,
    }
}

也就是说:该规则自 0.0.1-alpha.1 起即为稳定(stable)状态,且默认级别是 error(与 rules.md 中记录的 "Default level: error" 一致)。这意味着一处 values[1:10:0] 默认会让 ty 的检查直接失败,而不是给出警告。

逐行拆解 mdtest 用例:内置序列全覆盖

mdtest(markdown test)是 ty 系列 crate 用于把 Markdown 文档片段当作可执行测试的框架(入口见 crates/mdtest)。用例 stepsize_zero.md 给出了诊断的完整正面与反面样本,可以逐行解读:

from typing import Any

def builtins(
    values: list[int],
    mutable_bytes: bytearray,
    view: memoryview,
    numbers: range,
    immutable_bytes: bytes,
    text: str,
) -> None:
    values[1:10:0]  # error: [zero-stepsize-in-slice]
    mutable_bytes[1:10:0]  # error: [zero-stepsize-in-slice]
    view[1:10:0]  # error: [zero-stepsize-in-slice]
    numbers[1:10:0]  # error: [zero-stepsize-in-slice]
    immutable_bytes[1:10:0]  # error: [zero-stepsize-in-slice]
    text[1:10:0]  # error: [zero-stepsize-in-slice]

这 6 个用例覆盖了 ty 当前支持触发该诊断的全部内置序列类型

类型 示例 是否报错
list[int] values[1:10:0] error: [zero-stepsize-in-slice]
bytearray mutable_bytes[1:10:0] error: [zero-stepsize-in-slice]
memoryview view[1:10:0] error: [zero-stepsize-in-slice]
range numbers[1:10:0] error: [zero-stepsize-in-slice]
bytes immutable_bytes[1:10:0] error: [zero-stepsize-in-slice]
str text[1:10:0] error: [zero-stepsize-in-slice]

注意这里对序列参数采用了「值 + 类型注解」的写法:ty 依据静态类型list[int]str 等)而非运行值来判断,因此即便 values 为空列表、text 为空字符串,只要类型是内置序列,[1:10:0] 就会被标记。这与 step 必须是字面量零0)有关——ty 只有在能确定切片字面量的 step 部分为 Some(0) 时才报错(下文源码部分会展开)。

反面用例:自定义 __getitem__ 不报错

用例的第二部分展示了诊断的非穷尽性边界

class ZeroSafeList(list[int]):
    def __getitem__(self, key: Any) -> Any:
        return 0

ZeroSafeList()[0:1:0]  # No error

class MySequence:
    def __getitem__(self, s: slice) -> int:
        return 0

MySequence()[0:1:0]  # No error

两个反面用例各有代表性:

  1. ZeroSafeList(list[int]):虽然是 list 的子类,但覆写了 __getitem__ 并直接返回 0,完全不执行真正的切片逻辑。因此 ZeroSafeList()[0:1:0] 在运行期不会崩溃,ty 也就报错。这印证了「内置序列类型」的判断依据是名义类型是否恰好命中内置序列,而不是「是否为内置序列的子类」。
  2. MySequence:一个非序列的自定义类,自己实现了 __getitem__(self, s: slice)。切片操作会转成对该方法的调用,而方法体返回 0,所以同样不报错。

这两条用例共同说明:zero-stepsize-in-slice 只对已知会失败的内置序列报告,自定义类型与子类如何处理切片完全由 __getitem__ 决定,ty 无法也不试图去穷举所有运行期失败。

源码级实现:从 SliceLiteralSliceStepSizeZero

诊断的触发逻辑位于 crates/ty_python_semantic/src/types/subscript.rs。首先,错误种类在 SubscriptErrorKind 枚举中定义:

/// A slice literal used a step size of zero.
SliceStepSizeZero,

(见 subscript.rs

核心匹配逻辑(subscript.rs)可以概括为:当被下标对象的类型NominalInstance 且其 known_class 恰好命中 List | Tuple | Str | Bytes | Bytearray | Range | Memoryview 这 7 种内置类之一,同时切片类型也是 NominalInstance 且其 slice_literal 解析出的 SliceLiteral { step: Some(0), .. } 时,就构造 SliceStepSizeZero 错误:

(
    Type::NominalInstance(maybe_sequence_nominal),
    Type::NominalInstance(maybe_slice_nominal),
) if matches!(
    maybe_sequence_nominal.known_class(db),
    Some(
        KnownClass::List
            | KnownClass::Tuple
            | KnownClass::Str
            | KnownClass::Bytes
            | KnownClass::Bytearray
            | KnownClass::Range
            | KnownClass::Memoryview
    )
) && let Some(SliceLiteral { step: Some(0), .. }) =
    maybe_slice_nominal.slice_literal(db) =>
{
    Some(Err(SubscriptError::new(
        value_ty,
        SubscriptErrorKind::SliceStepSizeZero,
    )))
}

几个值得注意的实现细节:

  • 字面量 0 判定step: Some(0) 要求 step确定等于零的整数常量。因此 s[::0]s[0::0]s[:4:0] 这类写法的 step 都是常量 0,全部会被命中;而如果 step 是变量(例如 step = 0; s[::step]),ty 在无法确定其常量值时不会报错——除非值流分析能把它折叠成字面量。
  • 恰好匹配内置类matches!known_class 做精确匹配,意味着 list[int] 这类泛型实例化NominalInstance)同样命中,这与 mdtest 中 values: list[int] 的用例一致。
  • 另外两条路径:除了上面的主匹配,tuple 切片(subscript.rs)在调用 py_slice_type 失败时会映射到 SliceStepSizeZero;字符串/字节字面量切片(subscript.rssubscript.rs)在 py_slice 返回 Err 时同样映射到该错误。也就是说,字面量路径与内置类路径共用同一个错误码。

诊断的最终输出

错误最终通过 diagnostic.rs 中的 report_slice_step_size_zero 落成用户可见的诊断消息:

pub(super) fn report_slice_step_size_zero(context: &InferContext, node: AnyNodeRef) {
    let Some(builder) = context.report_lint(&ZERO_STEPSIZE_IN_SLICE, node) else {
        return;
    };
    builder.into_diagnostic("Slice step size cannot be zero");
}

报告位置是 node,即被下标表达式 seq<a href="https://link.gitcode.com/i/77a291144a7828bc0a4d4180e347739e" target="_blank">...]值部分(value node,见 [subscript.rs 中 report_slice_step_size_zero(context, value_node.into()))。输出消息为 Slice step size cannot be zero,错误码为 zero-stepsize-in-slice

同系列 mdtest:step=0 在更多切片形态中的覆盖

stepsize_zero.md 并不是孤立用例。在 subscript 目录下还有三份 mdtest 从不同容器类型的角度覆盖同一规则,可以交叉印证诊断的完整性:

tuple 切片tuple.md):

step = 2
reveal_type(t[start:stop:step])  # revealed: tuple[Literal["a"], Literal[b"b"]]
t[0:4:0]  # error: [zero-stepsize-in-slice]
t[:4:0]  # error: [zero-stepsize-in-slice]
t[0::0]  # error: [zero-stepsize-in-slice]
t[::0]  # error: [zero-stepsize-in-slice]

这里同时展示了两件事:合法的 step = 2 切片会被折叠求值(reveal 出精确的字面量元组类型),而四种 step=0 的变体(0:4:0:4:00::0::0)全部报 zero-stepsize-in-slice。注意 ::0 这种省略 startstop 的写法,其 SliceLiteralstart/stop 都是 None,但 step 仍是 Some(0),所以同样被命中。

字符串字面量切片string.md):

step = 2
reveal_type(s[start:stop:step])  # revealed: Literal["bd"]
s[0:4:0]  # error: [zero-stepsize-in-slice]
s[:4:0]  # error: [zero-stepsize-in-slice]
s[0::0]  # error: [zero-stepsize-in-slice]
s[::0]  # error: [zero-stepsize-in-slice]

字节字面量切片bytes.md):

b[0:4:0]  # error: [zero-stepsize-in-slice]
b[:4:0]  # error: [zero-stepsize-in-slice]
b[0::0]  # error: [zero-stepsize-in-slice]
b[::0]  # error: [zero-stepsize-in-slice]

综合这四份测试文件可以看出该规则的测试矩阵:容器维度list / bytearray / memoryview / range / bytes / str / tuple 字面量与泛型实例)+ 切片形态维度start:stop:0:stop:0start::0::0)。凡是 step 为字面量零的形态,ty 一律标记。

设计哲学:为什么「不穷尽」反而是优点

zero-stepsize-in-slice 的边界设计(“not exhaustive”)值得单独说明:

  1. 只报确定性错误:ty 只在「切片对象是内置序列」且「step 确定为字面量零」这两个条件同时满足时报告。这种保守策略保证了零误报——凡是报出的错误在 CPython 下 100% 会抛 ValueError
  2. 尊重自定义语义:Python 的 __getitem__ 是任意可重写的协议,MySequence()[0:1:0] 完全可以是合法操作(本例中返回 0)。ty 若对自定义类也报错,就会误伤大量合法代码。
  3. 错误等级为 error 的合理性:正因为只报确定性错误,该 lint 才敢把默认级别设为 error;它代表的是「必然的运行期崩溃」,而不是「可能的坏味道」。这一点与其他基于启发式的 lint(如 unused-awaitable,默认 Warn)形成对比。

如何在 ty 中启用与运行验证

该 lint 的默认级别是 error,因此开箱即用,无需额外配置。若需要调整其行为,可通过 ty 的 lint 级别配置覆盖(参考 ty 规则参考文档 中的 "Default level" 说明,将其降级为 warning 或关闭均可)。运行方式上,mdtest 框架会自动把上述 Markdown 文件中的 # error: <a href="https://link.gitcode.com/i/7612fa08e94d038ec6459ebe1714795c" target="_blank">code] 注释当作断言执行(框架源码见 [crates/mdtest/src),因此 stepsize_zero.md 本身就是一份可执行的行为规格说明——任何改动若破坏了该诊断的触发或抑制逻辑,测试都会失败。

小结

zero-stepsize-in-slice 是 ty 中一个「小而精确」的静态诊断:它在类型检查阶段捕获内置序列 step=0 切片这一必然的运行时 ValueError。通过 mdtest 用例 与源码 subscript.rs / diagnostic.rs 的对照,可以清晰看到其设计要点:

  • 触发条件严格限定为内置 7 类序列 + 切片字面量 step == 0
  • 自定义 __getitem__(含内置序列的子类覆写)一律放行,体现「只报确定性错误」的保守策略;
  • 默认级别 error,消息为 Slice step size cannot be zero
  • 同系列 tuple.mdstring.mdbytes.md 共同构成完整的覆盖矩阵。

理解这一类诊断的边界,不仅有助于正确使用 ty,也能作为设计类型检查器「确定性诊断」的参考范式:宁可少报,不可错报。

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

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
932
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23