ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Ruff Ty 类型检查器 unsupported-operator 诊断深度解析:mdtest 对比测试与实现原理

Ruff Ty 类型检查器 unsupported-operator 诊断深度解析:mdtest 对比测试与实现原理 Ruff Ty 类型检查器 unsupported-operator 诊断深度解析mdtest 对比测试与实现原理【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读本文以仓库 crates/ty_python_semantic/resources/mdtest/comparison/unsupported.md 为核心骨架完整剖析 Ruff 项目内建类型检查器ty在遇到不支持的运算符如in、not in、时如何产生unsupported-operator诊断。你将掌握mdtest 文档驱动测试的书写格式与 snapshot 输出结构、unsupported-operator诊断消息的完整构成主标注、次标注、info 信息、以及诊断在 types/diagnostic.rs 中的底层实现与元组/联合类型场景下的精确定位逻辑帮助你理解 Ruff 类型推断系统如何在不支持的操作上做到报错即定位、原因可见。一、背景什么是 mdtest 与 unsupported.mdRuff 的类型检查器ty采用文档驱动测试doc-driven testing的方式维护行为规范测试即文档文档即测试。这些测试文件位于 crates/ty_python_semantic/resources/mdtest 目录下comparison/子目录集中存放与比较运算comparison相关的用例其中 unsupported.md 专门覆盖操作数不支持该运算符的场景。一个 mdtest 文件由多个 Python 代码块py与对应的期望输出快照snapshot交替组成代码块内用# snapshot注释标记需要生成快照的表达式# snapshot: unsupported-operator表示该快照仅针对unsupported-operator这一条规则生成规则过滤代码块中的reveal_type(...)调用用于揭示推断出的类型是类型检查器测试的惯用断言方式。这种文件既可以直接被 mdtest 驱动执行把每个代码块拼装成独立的src/mdtest_snippet.py进行类型检查也可以作为人工阅读的行为规范文档。测试运行框架位于 crates/ruff_mdtest/src/lib.rs 与 crates/mdtest/src/lib.rs。unsupported-operator 是什么unsupported-operator是 ty 内置的 lint定义于 crates/ty_python_semantic/src/types/diagnostic.rsdeclare_lint! { #[doc include_str!(../../resources/lint_docs/unsupported-operator.md)] pub(crate) static UNSUPPORTED_OPERATOR { summary: detects binary, unary, or comparison expressions where the operands dont support the operator, status: LintStatus::stable(0.0.1-alpha.1), default_level: Level::Error, } }其摘要为检测操作数不支持该运算符的二元、一元或比较表达式自0.0.1-alpha.1起即标记为stable默认级别为Error。它的 lint 文档resources/lint_docs/unsupported-operator.md明确指出Attempting to use an unsupported operator will raise aTypeErrorat runtime.即此类代码必然在运行时抛出TypeError因此静态阶段报错是有充分依据的class A: ... # TypeError: unsupported operand type(s) for : A and A A() A() # error本文所述诊断正是 ty 对in/not in/等运算符在不可比较类型间使用的静态拦截。二、核心用例逐条剖析in/not in/的静态拦截以下按 unsupported.md 的原始顺序逐条还原每个用例的代码、快照输出与含义。所有用例共享如下前置环境def _(flag: bool, flag1: bool, flag2: bool): class A: ...即函数体内定义了本地类A三个布尔参数用于构造条件类型。2.1 数值字面量与in1 in 7a 1 in 7 reveal_type(a) # revealed: boolerror[unsupported-operator]: Unsupported in operation -- src/mdtest_snippet.py:4:9 | 4 | a 1 in 7 | -^^^^- | | | | | Has type Literal[7] | Has type Literal[1]关键点左操作数1的类型为Literal[1]右操作数7的类型为Literal[7]in运算符对整数右侧操作数没有定义int不是容器因此被判定为不支持虽然报错控制流仍然继续reveal_type(a)揭示的结果是bool。也就是说即使表达式本身非法类型推断仍以结果类型未知但可归约为 bool的方式继续传播避免级联误报快照中-^^^^-与Has type ...的对应关系展示了诊断的标注annotation布局主标注覆盖整个运算符表达式两个次标注分别覆盖左右操作数并给出其类型。2.2 数值字面量与not in0 not in 10b 0 not in 10 reveal_type(b) # revealed: boolerror[unsupported-operator]: Unsupported not in operation -- src/mdtest_snippet.py:7:9 | 7 | b 0 not in 10 | -^^^^^^^^-- | | | | | Has type Literal[10] | Has type Literal[0]与 2.1 完全对称not in同样不被支持消息文本为Unsupported \not in operation。这证实诊断消息会原样复现源码中使用的运算符符号in/not in/ 等。2.3object()与object() 5# snapshot: unsupported-operator c object() 5 reveal_type(c) # revealed: Unknownerror[unsupported-operator]: Unsupported operation -- src/mdtest_snippet.py:10:9 | 10 | c object() 5 | --------^^^- | | | | | Has type Literal[5] | Has type object注意与 2.1 / 2.2 的两点差异快照头部标注了# snapshot: unsupported-operator——当代码块中同时存在多条规则可能产生快照时用规则名精确过滤避免其他诊断干扰object() 5中object没有实现__lt__比较方法因此结果类型被推断为Unknown而不再是bool。这说明当比较结果无法确定时ty 不会强行归约为bool而是以Unknown兜底——这正是类型未知语义在比较运算上的体现。2.4 反向5 object()# snapshot: unsupported-operator d 5 object() reveal_type(d) # revealed: Unknownerror[unsupported-operator]: Unsupported operation -- src/mdtest_snippet.py:13:9 | 13 | d 5 object() | -^^^-------- | | | | | Has type object | Has type Literal[5]操作数左右互换后诊断结构保持一致左操作数5标注Has type \Literal[5]右操作数object()标注Has type object。证明该诊断与操作数方向无关只要任一侧缺乏对应 dunder 方法即报错。2.5 联合字面量与in42 in int_literal_or_str_literalint_literal_or_str_literal 1 if flag else foo e 42 in int_literal_or_str_literal reveal_type(e) # revealed: boolerror[unsupported-operator]: Unsupported in operation -- src/mdtest_snippet.py:17:9 | 17 | e 42 in int_literal_or_str_literal | --^^^^-------------------------- | | | | | Has type Literal[1, foo] | Has type Literal[42] info: Operation fails because operator in is not supported between objects of type Literal[42] and Literal[1]这是全文最值得深读的用例右侧类型为联合字面量Literal[1, foo]来自1 if flag else foo的条件表达式诊断的主/次标注给出的是整体联合类型Literal[1, foo]而底部的info:行则精确指出失败的具体分支in运算符不支持Literal[42]与Literal[1]之间的操作——即联合中1这个成员与左侧42不兼容而foo字符串作为容器理论上可行。这体现了 ty 在非原子类型联合、元组诊断上的精化策略主消息给出外层类型info信息下钻到导致失败的原子成员。三、元组元素级定位在异构元组中的失败unsupported.md 的后半部分转向元组比较展示了诊断系统在元组元素粒度上的定位能力。这与同目录下 tuples.md 中 Comparison Unsupported 一节的结论一致当两个元组含不支持比较的类型时结果为Unknown并针对不兼容元素发出诊断。3.1 同型但元素类型不同(1, 2) (1, hello)# snapshot: unsupported-operator f (1, 2) (1, hello) reveal_type(f) # revealed: Unknownerror[unsupported-operator]: Unsupported operation -- src/mdtest_snippet.py:20:9 | 20 | f (1, 2) (1, hello) | ------^^^------------ | | | | | Has type tuple[Literal[1], Literal[hello]] | Has type tuple[Literal[1], Literal[2]] info: Operation fails because operator is not supported between the tuple elements at index 2 (of type Literal[2] and Literal[hello])三个信息层次层层递进主标注Unsupported \ operation次标注左元组类型tuple[Literal[1], Literal[2]]、右元组类型tuple[Literal[1], Literal[hello]]info 下钻失败发生在索引 2 的元素对Literal[2]与Literal[hello]之间——元组比较按元素从左到右进行1 1成立后继续比较第 2 个元素2 hello无意义因此整个比较失败。注意index 2是从 1 开始计数的人性化序号对应源码中的position 1而非 Python 的 0 基索引。3.2 两侧类型完全相同(flag1, A()) (flag2, A())# snapshot: unsupported-operator g (flag1, A()) (flag2, A()) reveal_type(g) # revealed: Unknownerror[unsupported-operator]: Unsupported operation -- src/mdtest_snippet.py:23:9 | 23 | g (flag1, A()) (flag2, A()) | ------------^^^------------ | | | Both operands have type tuple[bool, A] info: Operation fails because operator is not supported between the tuple elements at index 2 (both of type A)与前例的关键区别左右操作数类型完全相同tuple[bool, A]flag1/flag2均为boolA()均为类A的实例因此不再分别标注左右类型而是合并为一句主标注Both operands have type \tuple[bool, A]并以^^^ 标注整个比较表达式失败根源是bool bool后继续比较A A而本地类A未实现__lt__故info行指出索引 2 的元素两侧均为A不支持。这种两侧类型等价时合并标注的呈现方式在源码中对应left_ty.is_equivalent_to(db, env, right_ty)的分支判断。四、诊断的源码级实现report_unsupported_comparison快照中的每一条信息都不是凭空生成的其背后是 crates/ty_python_semantic/src/types/diagnostic.rs 中的report_unsupported_comparison函数。将其与快照逐行对照4.1 主消息与标注分派let mut diagnostic diagnostic_builder.into_diagnostic(format_args!(Unsupported {} operation, error.op)); if left_ty.is_equivalent_to(db, env, right_ty) { diagnostic.set_primary_annotation_message(format_args!( Both operands have type {}, left_ty.display_with(db, env, display_settings.clone()) )); // ... 两个操作数都做 secondary 标注 diagnostic.set_concise_message(format_args!( Operator {} is not supported between two objects of type {}, ... )); } else { for (ty, expr) in [(left_ty, left), (right_ty, right)] { diagnostic.annotate(context.secondary(expr).message(format_args!( Has type {}, ... ))); } diagnostic.set_concise_message(format_args!( Operator {} is not supported between objects of type {} and {}, ... )); }对应关系一目了然快照现象源码逻辑Unsupported \in operation|error.op 渲染进主消息Has type \Literal[7]|else 分支对每个操作数做 secondary 标注Both operands have type \tuple[bool, A]|is_equivalent_to 为真时的合并标注错误码error[unsupported-operator]context.report_lint(UNSUPPORTED_OPERATOR, range)产生的 lint 构建器诊断还会附带 concise 消息如Operator \ is not supported between objects of type ...供编辑器等只显示单行摘要的场景使用。4.2 info 信息非原子类型的元素下钻快照中info:行的来源是本函数的后段。其核心注释写得很清楚diagnostic.rs// For non-atomic types like unions and tuples, we now provide context // on the underlying elements that caused the error. // If were emitting a diagnostic for something like (1, foo) (2, 3): // // - left_ty is tuple[Literal[1], Literal[foo]] // - right_ty is tuple[Literal[2], Literal[3]] // - error.left_ty is Literal[foo] // - error.right_ty is Literal[3]即left_ty/right_ty是外层整体类型error.left_ty/error.right_ty是内层触发失败的原子元素类型。当两者不相等时说明错误发生在子元素上于是若两侧都是定长元组TupleSpec::Fixed且长度相同则用position()找出第一个失败的配对位置输出Operation fails because operator \ is not supported between the tuple elements at index N (of type X and Y)3.1 的场景若两侧等价则输出(both of type \X)3.2 的场景否则联合类型等场景退化为原子级信息如 2.5 中的between objects of type \Literal[42] and Literal[1]。4.3 二元运算与增强赋值同一规则的三条路径除了比较运算UNSUPPORTED_OPERATOR规则还覆盖二元运算与增强赋值report_unsupported_binary_operationdiagnostic.rs处理a b、a | b等一般二元表达式report_unsupported_augmented_assignmentdiagnostic.rs处理a b等增强赋值通过OperatorDisplay在消息中渲染为形式二者共用report_unsupported_binary_operation_impldiagnostic.rs同样执行类型等价合并标注 / 否则分别标注的分派。lint 文档中A() A()触发TypeError的例子正是这条二元路径的运行时佐证。此外类型表达式层面如X | Y中类型值不支持|也会复用该规则见 builder/type_expression.rs 中Unsupported \| operation的构造。而 [builder.rs](https://link.gitcode.com/i/b3892101f2aab136f05c3589cf20c525) 的注释表明即使某方法在联合部分成员上可能未绑定导致调用失败ty 仍会保留相关绑定让unsupported-operator 诊断与 deprecation 等提示同时呈现而不互相遮蔽。五、如何复现与运行这些用例5.1 通过 mdtest 驱动执行unsupported.md 是标准的 mdtest 用例可在仓库中通过 mdtest 测试框架运行。mdtest 会把每个py代码块抽取为独立的src/mdtest_snippet.py片段进行类型检查再与snapshot块做快照比对——快照头部显示的src/mdtest_snippet.py:4:9正是该拼接片段的文件路径与行列号。相关的测试装配代码位于 [crates/ty_python_semantic/src/... ] 对应的 mdtest 集成测试crates/ruff_mdtest 与 crates/mdtest。执行方式参考仓库 mdtest 文档驱动测试的通用流程在项目根目录运行相应的 cargo 测试命令如cargo test中与 mdtest 相关的目标即可对resources/mdtest/comparison/下所有用例进行断言。同目录其他文件integers.md、tuples.md 等展示了同类机制在支持场景下的表现可与本文的不支持场景对照阅读。5.2 在真实代码中触发诊断在支持 ty 检查的工程中直接编写如下代码即可在编辑器/CI 中看到unsupported-operator1 in 7 # error[unsupported-operator]: Unsupported in operation 0 not in 10 # error[unsupported-operator]: Unsupported not in operation object() 5 # error[unsupported-operator]: Unsupported operation (1, 2) (1, hello) # info: ... tuple elements at index 2 ...诊断默认级别为Error可在 ty 的配置对应 ty.schema.json 及 lint 配置体系中按规则调整级别或忽略。想了解规则全貌可查看 ty/docs/rules.md其中收录了unsupported-operator在内的完整规则清单。六、小结unsupported-operator 的设计要点报错有依据不支持的操作符必然在运行时抛TypeError见 lint_docs/unsupported-operator.md因此静态报错是确定性事实而非猜测报错不阻断即使表达式非法reveal_type仍会给出bool或Unknown的结果类型in场景归约bool无 dunder 时归约Unknown避免错误级联类型等价合并标注两侧类型相同时输出Both operands have type ...单条标注否则左右分别标注Has type ...非原子类型下钻联合类型与元组的失败会通过info:行精确定位到具体成员/索引元组场景直接给出第 N 个元素的人性化序号一规则三场景比较运算、一般二元运算、增强赋值共用UNSUPPORTED_OPERATOR分别由 diagnostic.rs 中的三个报告函数驱动。通过 unsupported.md 这份文档驱动测试既能作为类型检查器行为规范人工审阅也能作为可执行用例持续回归——这正是 Ruff 项目中文档即测试工程实践的生动样本。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表