深入解析 ty 的 `zero-stepsize-in-slice` 诊断:从 mdtest 用例到源码实现
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 中,内置序列类型(list、tuple、str、bytes、bytearray、range、memoryview)在进行 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
两个反面用例各有代表性:
ZeroSafeList(list[int]):虽然是list的子类,但覆写了__getitem__并直接返回0,完全不执行真正的切片逻辑。因此ZeroSafeList()[0:1:0]在运行期不会崩溃,ty 也就不报错。这印证了「内置序列类型」的判断依据是名义类型是否恰好命中内置序列,而不是「是否为内置序列的子类」。MySequence:一个非序列的自定义类,自己实现了__getitem__(self, s: slice)。切片操作会转成对该方法的调用,而方法体返回0,所以同样不报错。
这两条用例共同说明:zero-stepsize-in-slice 只对已知会失败的内置序列报告,自定义类型与子类如何处理切片完全由 __getitem__ 决定,ty 无法也不试图去穷举所有运行期失败。
源码级实现:从 SliceLiteral 到 SliceStepSizeZero
诊断的触发逻辑位于 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.rs 与 subscript.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:0、0::0、::0)全部报 zero-stepsize-in-slice。注意 ::0 这种省略 start 与 stop 的写法,其 SliceLiteral 的 start/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:0、start::0、::0)。凡是 step 为字面量零的形态,ty 一律标记。
设计哲学:为什么「不穷尽」反而是优点
zero-stepsize-in-slice 的边界设计(“not exhaustive”)值得单独说明:
- 只报确定性错误:ty 只在「切片对象是内置序列」且「step 确定为字面量零」这两个条件同时满足时报告。这种保守策略保证了零误报——凡是报出的错误在 CPython 下 100% 会抛
ValueError。 - 尊重自定义语义:Python 的
__getitem__是任意可重写的协议,MySequence()[0:1:0]完全可以是合法操作(本例中返回0)。ty 若对自定义类也报错,就会误伤大量合法代码。 - 错误等级为 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.md、string.md、bytes.md 共同构成完整的覆盖矩阵。
理解这一类诊断的边界,不仅有助于正确使用 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 StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051