深入解析 Ruff Ty 类型检查器如何诊断联合类型Union函数调用【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff导读当 Python 变量因条件分支而持有不同类型时例如f f1或f f2对它的调用会同时面对多个函数签名静态检查器需要逐一校验每个分支并给出精确报错。本篇以 Ruff 仓库中 Ty 类型检查器位于crates/ty_python_semantic的官方测试文档 union_call.md 为骨架完整还原其针对联合函数类型调用的诊断设计从最基础的双分支场景到仅单个变体错误时的上下文增强、八类常见错误原因的覆盖、长联合类型的截断显示再到带重载方法与不兼容变体的复杂组合。读完本文你将理解 Ty 如何在一次调用中对联合的每个成员逐一绑定参数、合并报告并掌握这些诊断在源码中的实现位置与真实输出格式。背景联合类型调用为何需要专门诊断在 Python 中一个简单的if/else分支就足以让同一个名字在不同路径上指向不同函数def f1() - int: return 0 def f2(name: str) - int: return 0 def _(flag: bool): if flag: f f1 else: f f2 x f(3)此时f的静态类型是(def f1() - int) | (def f2(name: str) - int)这样一个函数联合类型。调用f(3)是否合法取决于每一个联合成员是否都能接受参数3f1不接收任何参数传 1 个位置参数 →too-many-positional-argumentsf2要求name: str而3是Literal[3]→invalid-argument-type。若把联合当成单个可调用对象做一次总绑定就很难指出错误究竟出在哪个变体上。Ty 的解决思路是在 crates/ty_python_semantic/src/types/call.rs 与 crates/ty_python_semantic/src/types/call/bind.rs 中引入Bindings结构它为联合的每个变体分别建立参数绑定binding逐成员做参数匹配与类型检查最后把失败变体的错误合并成对用户友好的诊断。从源码看Bindings的设计注释bind.rs 第 596-628 行明确说明了其层级关系union 层调用点的实参必须与联合中所有元素兼容调用才算合法返回值用联合合并intersection 层每个 union 元素内部依次尝试每个绑定只要有一个绑定成功该元素即成功返回值用交集合并。当所有元素都不合法时Bindings::as_resultbind.rs会根据失败模式收敛出三类错误对应 call.rs 中的CallErrorKind错误类别含义NotCallable联合中没有任何元素可调用例如成员是5这种字面量BindingError部分元素可调用但至少一个可调用元素的参数绑定失败PossiblyNotCallable并非所有元素都可调用但调用参数与所有可调用元素都兼容其中BindingError优先级最高即便联合里混有不可调用的成员只要实参与某个可调用变体不匹配就仍按绑定错误报告call.rs 第 399-415 行的注释明确记载了这一取舍。基准输出小规模双分支示例的完整诊断文档的第一个用例直接展示了联合调用失败时 Ty 输出的两份独立诊断。对应快照见 A smaller scale example 快照.snap)实际输出为error[invalid-argument-type]: Argument to function f2 is incorrect -- src/mdtest_snippet.py:14:11 | 14 | x f(3) | ^ Expected str, found Literal[3] info: Function defined here -- src/mdtest_snippet.py:4:5 | 4 | def f2(name: str) - int: | ^^ --------- Parameter declared here info: Union variant def f2(name: str) - int is incompatible with this call site info: Attempted to call union type (def f1() - int) | (def f2(name: str) - int)error[too-many-positional-arguments]: Too many positional arguments to function f1: expected 0, got 1 -- src/mdtest_snippet.py:14:11 | 14 | x f(3) | ^ info: Union variant def f1() - int is incompatible with this call site info: Attempted to call union type (def f1() - int) | (def f2(name: str) - int)注意两条诊断信息的共同结构——每条错误都附带两条info子诊断Union variant ... is incompatible with this call site明确指出是联合中的哪一个变体无法匹配本次调用Attempted to call union type ...展示被调用的完整联合类型。这种先逐变体报具体错误、再说明变体归属的两级结构正是 Ty 联合诊断的核心设计。在源码中它由 bind.rs 的UnionDiagnostic结构承载该结构记录了触发错误的callable_type整个联合类型与variant_type出错的特定变体类型并在report_element_diagnostics中为每个失败的联合元素挂载这两条信息见 bind.rs 第 1580-1641 行与第 9777-9788 行的结构定义。单变体错误时的上下文增强文档第二个用例探讨了一个重要细节当联合中只有一个变体非法时诊断会额外补充该变体特有的上下文信息当多个变体都非法时这些额外上下文会被省略以免信息过载淹没用户。对应快照见 Multiple variants but only one is invalid 快照.snap)def f1(a: int) - int: return 0 def f2(name: str) - int: return 0 def _(flag: bool): if flag: f f1 else: f f2 # error: [invalid-argument-type] x f(3)此时f1接受a: int参数3与之完全匹配只有f2因Expected str, found Literal[3]而失败。Ty 输出如下error[invalid-argument-type]: Argument to function f2 is incorrect -- src/mdtest_snippet.py:13:11 | 13 | x f(3) | ^ Expected str, found Literal[3] info: Function defined here -- src/mdtest_snippet.py:4:5 | 4 | def f2(name: str) - int: | ^^ --------- Parameter declared here info: Union variant def f2(name: str) - int is incompatible with this call site info: Attempted to call union type (def f1(a: int) - int) | (def f2(name: str) - int)与第一个用例对比可以发现因为只有f2一个变体失败诊断保留了Function defined here函数定义位置、参数声明位置这类仅与该变体相关的上下文帮助用户直达问题源头而第一个用例中f1、f2同时失败输出中就没有为每个错误都展开函数定义位置f2的invalid-argument-type仍保留但f1的too-many-positional-arguments只给出基本错误行。文档原文明确说明如果多于一个变体非法则省略这些附加上下文以避免让最终用户不知所措。这一行为是 Ty 诊断精准定位 控制噪音平衡的体现联合越大、失败的变体越多输出越收敛为变体清单式的概览失败变体越少越倾向于把该变体的完整上下文挖出来。全覆盖测试穷举八类非关键字相关错误文档作者自述这些测试在联合专属诊断最初创建时加入并很可能随时间过时其目的就是在一次调用中尽量覆盖所有可能的诊断消息。该用例Cover non-keyword related reasons构造了一个 8 路联合对应快照见 Cover non-keyword related reasons 快照.snap)from inspect import getattr_static from typing import overload def f1() - int: return 0 def f2(name: str) - int: return 0 def f3(a: int, b: int) - int: return 0 def f4T: str - int: return 0 overload def f5() - None: ... overload def f5(x: str) - str: ... def f5(x: str | None None) - str | None: return x overload def f6() - None: ... overload def f6(x: str, y: str) - str: ... def f6(x: str | None None, y: str | None None) - str | None: return x y if x and y else None def _(n: int): class PossiblyNotCallable: if n 0: def __call__(self) - int: return 0 if n 0: f f1 elif n 1: f f2 elif n 2: f f3 elif n 3: f f4 elif n 4: f 5 elif n 5: f f5 elif n 6: f f6 else: f PossiblyNotCallable() # error: [too-many-positional-arguments] # error: [invalid-argument-type] Argument to function f2 is incorrect: Expected str, found Literal[3] # error: [missing-argument] # error: [invalid-argument-type] Argument to function f4 is incorrect: Argument type Literal[3] does not satisfy upper bound str of type variable T # error: [invalid-argument-type] Argument to function f5 is incorrect: Expected str, found Literal[3] # error: [no-matching-overload] No overload of function f6 matches arguments # error: [call-non-callable] Object of type Literal[5] is not callable # error: [call-non-callable] Object of type PossiblyNotCallable is not callable (possibly missing __call__ method) x f(3)对f(3)这一处调用Ty 依据每个变体的签名分别给出诊断变体签名特点诊断消息要点f1无参数too-many-positional-argumentsexpected 0, got 1f2name: strinvalid-argument-typeExpectedstr, foundLiteral[3]f3a: int, b: intmissing-argumentNo argument provided for required parameterbf4泛型[T: str]invalid-argument-typeLiteral[3]不满足类型变量T的上界strf5重载函数invalid-argument-type匹配到f5(x: str) - str但实参为Literal[3]f6双签名重载no-matching-overloadNo overload of functionf6matches argumentsLiteral[5]非可调用字面量call-non-callableObject of typeLiteral[5]is not callablePossiblyNotCallable()条件定义的__call__call-non-callablepossibly missing__call__method快照中的实际输出片段节选展示了联合类型被截断显示的效果error[call-non-callable]: Object of type Literal[5] is not callable -- src/mdtest_snippet.py:60:9 | 60 | x f(3) | ^^^^ info: Union variant Literal[5] is incompatible with this call site info: Attempted to call union type (def f1() - int) | (def f2(name: str) - int) | (def f3(a: int, b: int) - int) | ... omitted 5 union elements关键细节一类型变量上界upper bound校验f4的报错Argument type Literal[3] does not satisfy upper bound str of type variable T表明Ty 对泛型函数PEP 695 语法def f4T: str不仅做参数类型匹配还会校验实参是否满足类型变量的约束上界。快照中还附带Type variable defined here子诊断精确指向[T: str]的声明位置Cover non-keyword related reasons 快照.snap) 第 155-168 行。关键细节二重载函数的两种失败形态f5与f6同为overload函数但失败形态不同f5存在一个可匹配的f5(x: str) - str重载只是实参类型不对因此报invalid-argument-type并附带Matching overload defined here与Non-matching overloads for function f5:两条上下文列出() - None作为未匹配的重载f6的所有重载() - None与(x: str, y: str) - str都与调用f(3)不兼容因此报no-matching-overload并列出First overload defined here与全部Possible overloads for function f6:。这两类诊断与 Ty 的类型检查 Lint 注册表一一对应可在 crates/ty_python_semantic/src/types/diagnostic.rs 中找到NO_MATCHING_OVERLOADdetects calls that do not match any overload、INVALID_ARGUMENT_TYPEdetects call arguments whose type is not assignable to the corresponding typed parameter、TOO_MANY_POSITIONAL_ARGUMENTS、MISSING_ARGUMENT、CALL_NON_CALLABLE等 Lint 的正式定义。关键细节三PossiblyNotCallable与条件的__call__PossiblyNotCallable类在n 0时才定义__call__方法属于可能不可调用的对象。Ty 对其报Object of type PossiblyNotCallable is not callable (possibly missing __call__ method)括号后缀 possibly missing 正是CallErrorKind::PossiblyNotCallable语义的体现——并非确定不可调用而是存在不可调用的可能。关键字参数相关的联合诊断联合诊断不仅覆盖位置参数也覆盖关键字参数匹配问题。文档对应的关键字用例Cover keyword argument related reasons如下快照见 Cover keyword argument related reasons 快照.snap)def any(*args, **kwargs) - int: return 0 def f1(name: str) - int: return 0 def _(n: int): if n 0: f f1 else: f any # error: [parameter-already-assigned] # error: [unknown-argument] y f(foo, namebar, unknownquux)调用f(foo, namebar, unknownquux)在f1变体上产生两个错误error[parameter-already-assigned]: Multiple values provided for parameter name of function f1 -- src/mdtest_snippet.py:14:18 | 14 | y f(foo, namebar, unknownquux) | ^^^^^^^^^^ info: Union variant def f1(name: str) - int is incompatible with this call site info: Attempted to call union type (def f1(name: str) - int) | (def any(...) - int)error[unknown-argument]: Argument unknown does not match any known parameter of function f1 -- src/mdtest_snippet.py:14:30 | 14 | y f(foo, namebar, unknownquux) | ^^^^^^^^^^^^^^ info: Union variant def f1(name: str) - int is incompatible with this call site info: Attempted to call union type (def f1(name: str) - int) | (def any(...) - int)foo与namebar同时向参数name提供值触发parameter-already-assigned关键字unknown在f1中不存在触发unknown-argument。而any(*args, **kwargs)变体对任意实参都兼容因此不产生任何错误。这再次验证了联合语义只要任一变体失败就报告失败变体各自独立成条诊断。对应的 Lint 定义同样位于 diagnostic.rsPARAMETER_ALREADY_ASSIGNEDdetects multiple arguments for the same parameter与UNKNOWN_ARGUMENTdetects unknown keyword arguments in calls。长联合与字面量的截断显示当被调函数的期望类型是大而混合的联合时完整打印会把输出撑得难以阅读。文档专设截断用例Truncation for long unions and literals快照见 Truncation for long unions and literals 快照.snap)from typing import Literal, Union class A: ... class B: ... class C: ... class D: ... class E: ... class F: ... def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) - int: return 0 def _(n: int): x n # error: [invalid-argument-type] f1(x)当int无法赋给该联合时Ty 的期望类型输出被截断为error[invalid-argument-type]: Argument to function f1 is incorrect -- src/mdtest_snippet.py:16:8 | 16 | f1(x) | ^ Expected Literal[1, 2, 3, 4, 5, ... omitted 3 literals] | A | B | ... omitted 4 union elements, found int info: Function defined here -- src/mdtest_snippet.py:10:5 | 10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) - int: | ^^ ----------------------------------------------------------- Parameter declared here截断遵循两条规则字面量联合截断Literal[1, 2, 3, 4, 5, 6, 7, 8]只显示前 5 个成员剩余以... omitted 3 literals收尾类型联合截断A | B | C | D | E | F只显示前 2 个成员剩余以... omitted 4 union elements收尾。对比前文的Attempted to call union type ... omitted 5 union elements调用方联合被截断与本处的Expected ... omitted 4 union elements参数期望类型被截断说明 Ty 在展示联合类型本身与展示作为参数期望类型的联合两个场景都实现了统一的截断策略从根源上避免诊断信息爆炸。多上界类型变量上的方法调用联合诊断同样作用于类型变量的方法调用场景。文档用例Attribute access on a typevar with multiple bounds如下快照见 Attribute access on a typevar with multiple bounds 快照.snap)from typing import TypeVar, Self class A: def foo(self, x: int) - Self: return self class B: def foo(self, x: str) - Self: return self T TypeVar(T, A, B) def _(x: T, y: int) - T: # error: [invalid-argument-type] return x.foo(y)T TypeVar(T, A, B)表示x可能是A或B的实例因此x.foo的静态类型是两个绑定方法的联合bound method T_ when A.foo(x: int) - T_ | bound method T_ when B.foo(x: str) - T_。对x.foo(y)y: intA.foo接受int兼容B.foo要求str报invalid-argument-type。实际输出error[invalid-argument-type]: Argument to bound method B.foo is incorrect -- src/mdtest_snippet.py:15:18 | 15 | return x.foo(y) | ^ Expected str, found int info: Method defined here -- src/mdtest_snippet.py:8:9 | 8 | def foo(self, x: str) - Self: | ^^^ ------ Parameter declared here info: Union variant bound method T_ when B.foo(x: str) - T_ is incompatible with this call site info: Attempted to call union type (bound method T_ when A.foo(x: int) - T_) | (bound method T_ when B.foo(x: str) - T_)注意x.foo是方法调用绑定方法invalid-argument-type报在实参y上而非整个调用表达式上且绑定方法 泛型类型变量的联合类型也能被正确展示。这说明联合调用诊断不限于顶层函数调用也完整覆盖了类型变量约束出的多上界方法这一常见泛型场景。复杂组合重载方法联合 不兼容变体最后一个用例Union with overloaded method and incompatible variant是全文最复杂也最能体现诊断精确性的场景对应快照见 Union with overloaded method and incompatible variant 快照.snap)def _(x: bytes | str | int): # error: [invalid-argument-type] # error: [unresolved-attribute] x.split( )对x.split( )这一调用三个变体各不相同str.split接受sep: str | None实参 兼容bytes.split接受sep: Buffer | None实参 不是Buffer不兼容int根本没有split属性无法解析。Ty 的输出同时包含两条诊断且各司其职error[unresolved-attribute]: Attribute split is not defined on int in union bytes | str | int -- src/mdtest_snippet.py:4:5 | 4 | x.split( ) | ^^^^^^^error[invalid-argument-type]: Argument to bound method bytes.split is incorrect -- src/mdtest_snippet.py:4:13 | 4 | x.split( ) | ^^^ Expected Buffer | None, found Literal[ ] info: type Literal[ ] is not assignable to any element of the union Buffer | None info: ├── type Literal[ ] is not assignable to protocol Buffer info: │ └── protocol member __buffer__ is not defined on type Literal[ ] info: └── ... omitted 1 union element without additional context info: Method defined here -- stdlib/builtins.pyi:1843:9 | 1843 | def split(self, sep: ReadableBuffer | None None, maxsplit: SupportsIndex -1) - list[bytes]: | ^^^^^ --------------------------------- Parameter declared here info: Union variant bound method bytes.split(...) - list[bytes] is incompatible with this call site info: Attempted to call union type (bound method bytes.split(...) - list[bytes]) | (bound method str.split(...) - list[str])文档原文着重强调了这个设计意图当一个联合变体的方法参数兼容str.split、另一个变体的方法参数不兼容bytes.split期望Buffer而非str、其余变体根本没有该方法为可调用联合贡献Unknown时我们只应报告不兼容变体的具体错误对bytes.split报invalid-argument-type而不应为兼容变体误报no-matching-overload。这里有两个值得注意的实现事实int变体没有split属性Ty 在属性解析阶段就将其排除在可调用联合之外以Unknown参与因此最终的调用诊断联合是bytes.split | str.split两个绑定方法而不是bytes.split | str.split | int.split——从Attempted to call union type一行可以看出int已被剔除缺失属性与错误实参是两条独立诊断unresolved-attribute属性不存在由属性解析路径报告错误范围为x.splitinvalid-argument-type实参不匹配由调用绑定路径报告错误范围为实参 。UNRESOLVED_ATTRIBUTE在 diagnostic.rs 中定义为 detects references to unresolved attributes。同时Expected Buffer | None的输出还展示了 Ty 对协议类型的解释能力info层级逐行展开Literal[ ]为何不可赋给Buffer因为协议成员__buffer__未定义并以... omitted 1 union element without additional context对期望联合的其余成员进行省略。如何在本地复现与验证上述所有诊断输出均来自仓库内的快照测试你可以在本地完整复现。查看源码联合调用诊断的实现集中在 crates/ty_python_semantic/src/types/call/bind.rs参数绑定与诊断报告与 crates/ty_python_semantic/src/types/call.rsCallErrorKind分类Lint 定义见 crates/ty_python_semantic/src/types/diagnostic.rs阅读测试文档测试用例位于 union_call.md每个用例的期望输出见 mdtest 快照目录 下union_call.md_*开头的 7 个.snap文件运行测试这些用例属于 mdtest 体系crates/mdtest/src/lib.rs可通过 Ty 测试套件运行例如在仓库根目录执行cargo test -p ty_python_semantic具体运行方式以仓库 CONTRIBUTING 说明为准。修改用例代码后运行测试即可对比新的诊断输出与.snap快照这正是 Ty 开发者维护诊断行为的工作流。小结通过 union_call.md 及其快照我们可以把 Ty 的联合调用诊断能力归纳为四条设计原则逐变体绑定、逐变体报告调用联合时对每个成员独立做参数绑定与类型检查失败变体各自产生独立诊断并通过Union variant ... is incompatible with this call site标注归属上下文按需增强仅单个变体非法时附带该变体的函数定义、参数声明等定位信息多个变体同时非法时收敛为变体概览避免信息过载错误原因全覆盖位置参数过多、缺失、类型不符、关键字参数重复赋值、未知参数、泛型上界、重载匹配no-matching-overload、不可调用对象call-non-callable等都在一次调用中准确分类输出可读性优先长联合与长字面量统一截断... omitted N union elements/... omitted N literals属性解析与参数匹配各司其职不误报、不重复报。这些行为在 crates/ty_python_semantic/src/types/call/bind.rs 的Bindings/BindingsElement/UnionDiagnostic结构中有清晰实现union 层要求所有元素兼容、intersection 层尝试任一成功错误优先级BindingError PossiblyNotCallable NotCallable保证诊断总是落在最能说明问题的那一类上。对于任何需要在 Rust 类型检查器中处理多候选调用诊断的开发者来说这份文档与其快照都是值得研读的参考样本。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考