资讯详情

Ruff `ty` 类型检查器一元运算符推断实战:从 `__pos__`/`__neg__`/`__invert__` 到 `unsupported-operator` 诊断

📅 2026/9/12 11:56:54 | 华诺云谱 👁 阅读
Ruff `ty` 类型检查器一元运算符推断实战:从 `__pos__`/`__neg__`/`__invert__` 到 `unsupported-operator` 诊断
Ruffty类型检查器一元运算符推断实战从__pos__/__neg__/__invert__到unsupported-operator诊断【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffRuff 内置的ty类型检查器通过调用实例的 dunder 方法来推断一元运算符x、-x、~x的结果类型并在操作数不支持该运算符时给出unsupported-operator诊断。本文以 crates/ty_python_semantic/resources/mdtest/unary/custom.md 这份官方 mdtest 测试文档为骨架逐一剖析类实例、类本身、函数字面量、子类、联合类型与元类六大场景下的推断与报错行为并结合 builder.rs 与 types.rs 的源码实现讲清这套机制背后的完整调用链。读完本文你将掌握一元运算符类型推断的完整规则并能独立阅读和运行仓库中的 mdtest 测试用例。一、测试文档的背景mdtest 是什么custom.md并不是一篇散文式文档而是 Ruffty类型检查器测试套件中的一份mdtest 夹具fixture。仓库通过datatest_stable注册了一个测试 harness凡是位于 crates/ty_python_semantic/resources/mdtest/ 目录下、以.md结尾的文件都会被当作测试用例执行具体见 crates/ty_python_semantic/tests/mdtest.rsdatatest_stable::harness! { { test mdtest, root ./resources/mdtest, pattern r\.md$ }, { test lint_doc, root ./resources/lint_docs, pattern r\.md$ }, }每个 md 文件中的 Python 代码块都会被提取出来交给类型检查器分析然后与注释中的断言revealed:、# error: [rule]以及同目录 snapshots/ 下的快照进行比对。因此这份文档中的每一行断言都是可运行、可验证的规范精确刻画了当前版本ty检查器的真实行为。二、基础映射一元运算符对应的 dunder 方法一元运算符与 dunder 方法的对应关系在 builder.rs 的fallback_unary_expression_type闭包中一目了然let unary_dunder_method match op { ast::UnaryOp::Invert __invert__, ast::UnaryOp::UAdd __pos__, ast::UnaryOp::USub __neg__, ast::UnaryOp::Not { unreachable!(Not operator is handled in its own case); } };运算符语法dunder 方法正号x__pos__负号-x__neg__按位取反~x__invert__逻辑取反not x不走 dunder而是走真值truthiness逻辑需要特别注意的是not运算符不通过 dunder 方法处理它在infer_unary_expression_type中有独立分支见下文第五节通过try_bool求取操作数的真值并取反得到布尔结果同时会做取反冗余check_negation_redundancy等额外检查。三、场景一类实例Class instances当操作数是类的实例时ty检查器会在该实例的类上查找对应 dunder 方法并用其返回类型作为表达式结果类型class Yes: def __pos__(self) - bool: return False def __neg__(self) - str: return negative def __invert__(self) - int: return 17 class Sub(Yes): ... class No: ... reveal_type(Yes()) # revealed: bool reveal_type(-Yes()) # revealed: str reveal_type(~Yes()) # revealed: int reveal_type(Sub()) # revealed: bool reveal_type(-Sub()) # revealed: str reveal_type(~Sub()) # revealed: int # error: [unsupported-operator] Unary operator is not supported for object of type No reveal_type(No()) # revealed: Unknown # error: [unsupported-operator] Unary operator - is not supported for object of type No reveal_type(-No()) # revealed: Unknown # error: [unsupported-operator] Unary operator ~ is not supported for object of type No reveal_type(~No()) # revealed: Unknown要点如下Yes()的结果类型就是__pos__的返回类型bool-Yes()是str~Yes()是int与 dunder 方法声明一一对应继承有效Sub(Yes)的实例同样支持这三个运算符结果类型与基类一致即 dunder 方法沿 MRO 继承缺失即报错No没有定义任何相关 dunder 方法三个运算符全部触发unsupported-operator错误且reveal_type结果为Unknown错误后无法确定类型。从源码看这一行为由fallback_unary_expression_type中的operand_type.try_call_dunder(db, env, unary_dunder_method, CallArguments::none(), ...)驱动Ok时取outcome.return_type(db, env)Err时报告unsupported-operator并回退为e.fallback_return_type(db, env)见 builder.rs。四、场景二类本身Classesdunder 方法定义在类中只对该类的实例有效对类本身无效。要让运算符作用于类对象自身dunder 方法必须定义在类的类型上——即元类上class Yes: def __pos__(self) - bool: return False def __neg__(self) - str: return negative def __invert__(self) - int: return 17 class Sub(Yes): ... class No: ... # error: [unsupported-operator] Unary operator is not supported for object of type class Yes reveal_type(Yes) # revealed: Unknown # error: [unsupported-operator] Unary operator - is not supported for object of type class Yes reveal_type(-Yes) # revealed: Unknown # error: [unsupported-operator] Unary operator ~ is not supported for object of type class Yes reveal_type(~Yes) # revealed: Unknown这里Yes、-Yes、~Yes中的操作数类型是ClassLiteral表现为class Yes而不是Yes实例。尽管Yes内部定义了__pos__等三个方法但由于这些方法没有定义在type即Yes的元类上运算符仍然不被支持三个表达式全部报unsupported-operatorreveal_type均为Unknown。Sub和No两个类同理全部报错。这与 Python 运行时行为一致Yes实际会抛TypeError。从实现上印证在 builder.rs 的分支匹配中Type::ClassLiteral(_)与Type::SubclassOf(_)、Type::FunctionLiteral(_)等类型一样统一落入fallback_unary_expression_type()走 dunder 查找而 dunder 查找使用try_call_dunder其内部在 types.rs 强制加入了MemberLookupPolicy::NO_INSTANCE_FALLBACK即隐式 dunder 调用永不回退到实例成员因此类定义体中的方法不会被类对象借用。五、场景三函数字面量Function literals函数对象同样不支持一元运算符即使它是看起来可调用的东西def f(): pass # error: [unsupported-operator] Unary operator is not supported for object of type def f() - Unknown reveal_type(f) # revealed: Unknown # error: [unsupported-operator] Unary operator - is not supported for object of type def f() - Unknown reveal_type(-f) # revealed: Unknown # error: [unsupported-operator] Unary operator ~ is not supported for object of type def f() - Unknown reveal_type(~f) # revealed: Unknown注意错误信息中操作数类型被显示为def f() - Unknown函数字面量的显示格式reveal_type均为Unknown。在源码的匹配分支中Type::FunctionLiteral(_)与Type::Callable(..)等可调用类型被显式列出并全部走fallback_unary_expression_type()而function类型没有定义__pos__/__neg__/__invert__因此必然报错。六、场景四子类作为值Subclass本场景考察的是把类对象当作值传递的情形。函数返回type[Yes]、type[Sub]、type[No]对返回值施加一元运算符得到的是type[Yes]等子类对象类型SubclassOf同样不支持运算符class Yes: def __pos__(self) - bool: return False def __neg__(self) - str: return negative def __invert__(self) - int: return 17 class Sub(Yes): ... class No: ... def yes() - type[Yes]: return Yes def sub() - type[Sub]: return Sub def no() - type[No]: return No # error: [unsupported-operator] Unary operator is not supported for object of type type[Yes] reveal_type(yes()) # revealed: Unknown # error: [unsupported-operator] Unary operator - is not supported for object of type type[Yes] reveal_type(-yes()) # revealed: Unknown # error: [unsupported-operator] Unary operator ~ is not supported for object of type type[Yes] reveal_type(~yes()) # revealed: Unknown # ... type[Sub] 与 type[No] 同理全部报错 ...yes()的返回类型是type[Yes]在内部表示中为SubclassOf错误信息显示为type[Yes]。与场景二一致Type::SubclassOf(_)也被显式列入fallback_unary_expression_type()的分支。它印证了同一结论无论是字面量类对象还是type[X]类型的值只要操作数是类而非实例普通类体中的 dunder 方法都不会生效。七、场景五联合类型Union当操作数是联合类型且其中一个成员缺少 dunder 方法时行为有一个值得注意的细节reveal_type仍然给出 dunder 方法的返回类型但同时会报告unsupported-operator错误并附加一条info说明哪个成员缺失该方法class Yes: def __pos__(self) - bool: return False def __neg__(self) - str: return negative def __invert__(self) - int: return 17 class No: ... def _(x: Yes | No): # snapshot: unsupported-operator reveal_type(x) # revealed: bool # snapshot: unsupported-operator reveal_type(-x) # revealed: str # snapshot: unsupported-operator reveal_type(~x) # revealed: int对应的快照输出来自 mdtest/snapshots 目录如下error[unsupported-operator]: Unary operator is not supported for object of type Yes | No -- src/mdtest_snippet.py:15:17 | 15 | reveal_type(x) # revealed: bool | ^^ info: No does not implement __pos__三个运算符都遵循同样的模式主错误信息指出整个联合类型Yes | No不支持该运算符光标精确指向操作数表达式x随后的info级别提示精确到No不实现__pos__或__neg__/__invert__。这一提示缺失成员的能力来自 builder.rs 的report_unsupported_unary_operator当try_call_dunder返回CallDunderError::PossiblyUnbound { unbound_on: Some(...) }时遍历unbound_on中每个类型追加info诊断{ty}does not implement{unary_dunder_method}。而其上层联合类型在 types.rs 中被特殊处理try_call_dunder_with_policy遇到Type::Union会转交给union.try_call_dunder_with_policy由联合类型内部逐个成员尝试查找收集某成员未定义该方法的信息。八、场景六元类Metaclass元类场景是场景二的反面dunder 方法定义在元类上时运算符对类对象本身可用。这也是唯一能让Yes、-Yes、~Yes正常工作的途径class Meta(type): def __pos__(self) - bool: return False def __neg__(self) - str: return negative def __invert__(self) - int: return 17 class Yes(metaclassMeta): ... class Sub(Yes): ... class No: ... reveal_type(Yes) # revealed: bool reveal_type(-Yes) # revealed: str reveal_type(~Yes) # revealed: int reveal_type(Sub) # revealed: bool reveal_type(-Sub) # revealed: str reveal_type(~Sub) # revealed: int # error: [unsupported-operator] Unary operator is not supported for object of type class No reveal_type(No) # revealed: Unknown # error: [unsupported-operator] Unary operator - is not supported for object of type class No reveal_type(-No) # revealed: Unknown # error: [unsupported-operator] Unary operator ~ is not supported for object of type class No reveal_type(~No) # revealed: Unknown要点Yes的元类是MetaYes在Meta上找到__pos__结果为bool-Yes为str~Yes为int元类同样沿继承链生效Sub(Yes)虽未显式指定元类但继承了Yes的元类Meta因此三个运算符同样可用结果类型与Yes一致No使用默认元类typetype上没有这三个 dunder 方法因此全部报unsupported-operatorreveal_type为Unknown。这也呼应了文档在Classes一节中的注释要让运算符作用于类本身dunder 方法必须定义在类的类型即type上。从实现上看try_call_dunder的成员查找最终会落在元类链上从而在Meta中找到__pos__等绑定。九、底层实现剖析infer_unary_expression_type的完整分支逻辑把六大场景统一起来一元运算符推断的入口是 builder.rs 的infer_unary_expression先推断操作数类型再调用infer_unary_expression_type(op, operand_type, unary)。后者在 builder.rs 中按操作数类型分派Dynamic/Divergent/Never直接返回操作数类型本身Dynamic透传Never保持Never见 builder.rsTypeAlias解包别名后递归推断alias.value_type字面量快速路径int_literal原样返回-int_literal做checked_neg溢出时回退到int实例~int_literal/~bool_literal按位取反后得到新的整数字面量类型其中~bool会额外检查 typeshed 中对__invert__的废弃标记见 builder.rsConstraintSet上的~直接对约束集取反not分支通过try_bool求真值并取反同时做取反冗余检查见 builder.rs受限 TypeVarconstrained TypeVar对每个约束逐一调用 dunder若全部成功则保留约束映射后的类型任一失败则报告unsupported-operator并回退带 upper bound 的 TypeVar 则委托给 bound 类型无约束 TypeVar 走默认 dunder 查找见 builder.rs兜底分支FunctionLiteral、ClassLiteral、SubclassOf、Union、NominalInstance、ProtocolInstance、KnownInstance等所有其他类型统一走fallback_unary_expression_type()即执行第四节所述的try_call_dunder查找与错误回退。try_call_dunder本身定义在 types.rs它通过member_lookup_with_policy在操作数类型上查找 dunder 方法再对方法的bindings做参数匹配与类型检查最后返回BindingsOk或CallDunderErrorErr包括MethodNotAvailable、PossiblyUnbound、CallError三种变体。关键点是隐式 dunder 调用会强制叠加MemberLookupPolicy::NO_INSTANCE_FALLBACK确保查找严格走类/元类链而不回退到实例成员——这正是类实例可用、类对象不可用场景一 vs 场景二/四的根源。十、unsupported-operator诊断规则unsupported-operator是ty内置 lint 规则之一在 crates/ty_python_semantic/src/types/diagnostic.rs 中声明declare_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, } }它检测二元、一元、比较表达式中操作数不支持运算符的情况默认级别为Error。对应的规则文档位于 crates/ty_python_semantic/resources/lint_docs/unsupported-operator.md其中说明了判定逻辑operands dont support the operator、危害运行时将抛出TypeError并给出了示例class A: ... # TypeError: unsupported operand type(s) for : A and A A() A() # error诊断的格式化入口是report_unsupported_unary_operatorbuilder.rs它先通过self.context.report_lint(UNSUPPORTED_OPERATOR, unary)获取诊断构建器生成形如Unary operator{op}is not supported for object of type{operand_type} 的主错误再按需追加Xdoes not implement__pos__的info提示。注意主信息中的{op}是运算符本身的显示文本/-/~而{operand_type}使用Type::display格式化如class Yes、type[Yes]、Yes | No、def f() - Unknown这解释了各场景中错误信息措辞的差异。十一、如何运行与扩展这些测试mdtest 测试不需要手工准备环境直接使用 Cargo 即可。例如只跑一元运算符相关的夹具cargo test -p ty_python_semantic --test mdtest mdtest/unary或者按datatest_stable的命名规则精确匹配某个文件cargo test -p ty_python_semantic --test mdtest custom仓库中与custom.md同目录的 crates/ty_python_semantic/resources/mdtest/unary/ 还包含三份姊妹夹具覆盖不同侧重点integers.md整数字面量上/-/~的快速路径与字面量类型折叠invert_add_usub.md实例场景以及 TypeVar 带 bound如boundfloat会被视为int | float与受限 TypeVar每个约束都支持运算符时保留 TypeVar 类型的推断规则not.mdnot运算符的真值推断与冗余检查。阅读这些夹具时可以遵循的通用语法约定# revealed: type断言表达式推断出的类型# error: [rule] message断言某行会触发指定 lint 并匹配错误信息# snapshot: name与snapshot代码块组合用于断言多行诊断快照。所有快照最终会写入 crates/ty_python_semantic/resources/mdtest/snapshots/ 目录与夹具一一对应。若行为有变可通过INSTA_UPDATEalways等 insta 快照机制重新生成。结语custom.md虽然只是一份测试夹具却完整定义了 Ruffty类型检查器在自定义一元运算上的行为契约实例沿 MRO 查找 dunder 方法并用其返回类型作为结果类对象、子类对象与函数对象一律不支持联合类型在成员缺失时报错但仍能给出部分信息并附带缺失成员的具体提示元类上的 dunder 方法则让类对象本身重新获得运算符支持。结合 builder.rs、types.rs 与 diagnostic.rs 的实现可以完整还原这套从语法节点到类型结果再到诊断输出的调用链。理解这些规则既有助于把握ty检查器的语义边界也能为在类型检查器之上编写工具或扩展提供可靠的参考基线。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。