算符推斷實(shí)戰(zhàn):從 `__pos__`/`__neg__`/`__invert__` 到 `unsupported-operator` 診斷)
Ruffty類型檢查器一元運(yùn)算符推斷實(shí)戰(zhàn)從__pos__/__neg__/__invert__到unsupported-operator診斷【免費(fèi)下載鏈接】ruffAn extremely fast Python linter and code formatter, written in Rust.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ru/ruffRuff 內(nèi)置的ty類型檢查器通過調(diào)用實(shí)例的 dunder 方法來推斷一元運(yùn)算符x、-x、~x的結(jié)果類型并在操作數(shù)不支持該運(yùn)算符時(shí)給出unsupported-operator診斷。本文以 crates/ty_python_semantic/resources/mdtest/unary/custom.md 這份官方 mdtest 測(cè)試文檔為骨架逐一剖析類實(shí)例、類本身、函數(shù)字面量、子類、聯(lián)合類型與元類六大場(chǎng)景下的推斷與報(bào)錯(cuò)行為并結(jié)合 builder.rs 與 types.rs 的源碼實(shí)現(xiàn)講清這套機(jī)制背后的完整調(diào)用鏈。讀完本文你將掌握一元運(yùn)算符類型推斷的完整規(guī)則并能獨(dú)立閱讀和運(yùn)行倉庫中的 mdtest 測(cè)試用例。一、測(cè)試文檔的背景mdtest 是什么custom.md并不是一篇散文式文檔而是 Ruffty類型檢查器測(cè)試套件中的一份mdtest 夾具fixture。倉庫通過datatest_stable注冊(cè)了一個(gè)測(cè)試 harness凡是位于 crates/ty_python_semantic/resources/mdtest/ 目錄下、以.md結(jié)尾的文件都會(huì)被當(dāng)作測(cè)試用例執(zhí)行具體見 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$ }, }每個(gè) md 文件中的 Python 代碼塊都會(huì)被提取出來交給類型檢查器分析然后與注釋中的斷言revealed:、# error: [rule]以及同目錄 snapshots/ 下的快照進(jìn)行比對(duì)。因此這份文檔中的每一行斷言都是可運(yùn)行、可驗(yàn)證的規(guī)范精確刻畫了當(dāng)前版本ty檢查器的真實(shí)行為。二、基礎(chǔ)映射一元運(yùn)算符對(duì)應(yīng)的 dunder 方法一元運(yùn)算符與 dunder 方法的對(duì)應(yīng)關(guān)系在 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); } };運(yùn)算符語法dunder 方法正號(hào)x__pos__負(fù)號(hào)-x__neg__按位取反~x__invert__邏輯取反not x不走 dunder而是走真值truthiness邏輯需要特別注意的是not運(yùn)算符不通過 dunder 方法處理它在infer_unary_expression_type中有獨(dú)立分支見下文第五節(jié)通過try_bool求取操作數(shù)的真值并取反得到布爾結(jié)果同時(shí)會(huì)做取反冗余check_negation_redundancy等額外檢查。三、場(chǎng)景一類實(shí)例Class instances當(dāng)操作數(shù)是類的實(shí)例時(shí)ty檢查器會(huì)在該實(shí)例的類上查找對(duì)應(yīng) dunder 方法并用其返回類型作為表達(dá)式結(jié)果類型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要點(diǎn)如下Yes()的結(jié)果類型就是__pos__的返回類型bool-Yes()是str~Yes()是int與 dunder 方法聲明一一對(duì)應(yīng)繼承有效Sub(Yes)的實(shí)例同樣支持這三個(gè)運(yùn)算符結(jié)果類型與基類一致即 dunder 方法沿 MRO 繼承缺失即報(bào)錯(cuò)No沒有定義任何相關(guān) dunder 方法三個(gè)運(yùn)算符全部觸發(fā)unsupported-operator錯(cuò)誤且reveal_type結(jié)果為Unknown錯(cuò)誤后無法確定類型。從源碼看這一行為由fallback_unary_expression_type中的operand_type.try_call_dunder(db, env, unary_dunder_method, CallArguments::none(), ...)驅(qū)動(dòng)Ok時(shí)取outcome.return_type(db, env)Err時(shí)報(bào)告unsupported-operator并回退為e.fallback_return_type(db, env)見 builder.rs。四、場(chǎng)景二類本身Classesdunder 方法定義在類中只對(duì)該類的實(shí)例有效對(duì)類本身無效。要讓運(yùn)算符作用于類對(duì)象自身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中的操作數(shù)類型是ClassLiteral表現(xiàn)為class Yes而不是Yes實(shí)例。盡管Yes內(nèi)部定義了__pos__等三個(gè)方法但由于這些方法沒有定義在type即Yes的元類上運(yùn)算符仍然不被支持三個(gè)表達(dá)式全部報(bào)unsupported-operatorreveal_type均為Unknown。Sub和No兩個(gè)類同理全部報(bào)錯(cuò)。這與 Python 運(yùn)行時(shí)行為一致Yes實(shí)際會(huì)拋TypeError。從實(shí)現(xiàn)上印證在 builder.rs 的分支匹配中Type::ClassLiteral(_)與Type::SubclassOf(_)、Type::FunctionLiteral(_)等類型一樣統(tǒng)一落入fallback_unary_expression_type()走 dunder 查找而 dunder 查找使用try_call_dunder其內(nèi)部在 types.rs 強(qiáng)制加入了MemberLookupPolicy::NO_INSTANCE_FALLBACK即隱式 dunder 調(diào)用永不回退到實(shí)例成員因此類定義體中的方法不會(huì)被類對(duì)象借用。五、場(chǎng)景三函數(shù)字面量Function literals函數(shù)對(duì)象同樣不支持一元運(yùn)算符即使它是看起來可調(diào)用的東西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注意錯(cuò)誤信息中操作數(shù)類型被顯示為def f() - Unknown函數(shù)字面量的顯示格式reveal_type均為Unknown。在源碼的匹配分支中Type::FunctionLiteral(_)與Type::Callable(..)等可調(diào)用類型被顯式列出并全部走fallback_unary_expression_type()而function類型沒有定義__pos__/__neg__/__invert__因此必然報(bào)錯(cuò)。六、場(chǎng)景四子類作為值Subclass本場(chǎng)景考察的是把類對(duì)象當(dāng)作值傳遞的情形。函數(shù)返回type[Yes]、type[Sub]、type[No]對(duì)返回值施加一元運(yùn)算符得到的是type[Yes]等子類對(duì)象類型SubclassOf同樣不支持運(yùn)算符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] 同理全部報(bào)錯(cuò) ...yes()的返回類型是type[Yes]在內(nèi)部表示中為SubclassOf錯(cuò)誤信息顯示為type[Yes]。與場(chǎng)景二一致Type::SubclassOf(_)也被顯式列入fallback_unary_expression_type()的分支。它印證了同一結(jié)論無論是字面量類對(duì)象還是type[X]類型的值只要操作數(shù)是類而非實(shí)例普通類體中的 dunder 方法都不會(huì)生效。七、場(chǎng)景五聯(lián)合類型Union當(dāng)操作數(shù)是聯(lián)合類型且其中一個(gè)成員缺少 dunder 方法時(shí)行為有一個(gè)值得注意的細(xì)節(jié)reveal_type仍然給出 dunder 方法的返回類型但同時(shí)會(huì)報(bào)告unsupported-operator錯(cuò)誤并附加一條info說明哪個(gè)成員缺失該方法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對(duì)應(yīng)的快照輸出來自 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__三個(gè)運(yùn)算符都遵循同樣的模式主錯(cuò)誤信息指出整個(gè)聯(lián)合類型Yes | No不支持該運(yùn)算符光標(biāo)精確指向操作數(shù)表達(dá)式x隨后的info級(jí)別提示精確到No不實(shí)現(xiàn)__pos__或__neg__/__invert__。這一提示缺失成員的能力來自 builder.rs 的report_unsupported_unary_operator當(dāng)try_call_dunder返回CallDunderError::PossiblyUnbound { unbound_on: Some(...) }時(shí)遍歷unbound_on中每個(gè)類型追加info診斷{ty}does not implement{unary_dunder_method}。而其上層聯(lián)合類型在 types.rs 中被特殊處理try_call_dunder_with_policy遇到Type::Union會(huì)轉(zhuǎn)交給union.try_call_dunder_with_policy由聯(lián)合類型內(nèi)部逐個(gè)成員嘗試查找收集某成員未定義該方法的信息。八、場(chǎng)景六元類Metaclass元類場(chǎng)景是場(chǎng)景二的反面dunder 方法定義在元類上時(shí)運(yùn)算符對(duì)類對(duì)象本身可用。這也是唯一能讓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要點(diǎn)Yes的元類是MetaYes在Meta上找到__pos__結(jié)果為bool-Yes為str~Yes為int元類同樣沿繼承鏈生效Sub(Yes)雖未顯式指定元類但繼承了Yes的元類Meta因此三個(gè)運(yùn)算符同樣可用結(jié)果類型與Yes一致No使用默認(rèn)元類typetype上沒有這三個(gè) dunder 方法因此全部報(bào)unsupported-operatorreveal_type為Unknown。這也呼應(yīng)了文檔在Classes一節(jié)中的注釋要讓運(yùn)算符作用于類本身dunder 方法必須定義在類的類型即type上。從實(shí)現(xiàn)上看try_call_dunder的成員查找最終會(huì)落在元類鏈上從而在Meta中找到__pos__等綁定。九、底層實(shí)現(xiàn)剖析infer_unary_expression_type的完整分支邏輯把六大場(chǎng)景統(tǒng)一起來一元運(yùn)算符推斷的入口是 builder.rs 的infer_unary_expression先推斷操作數(shù)類型再調(diào)用infer_unary_expression_type(op, operand_type, unary)。后者在 builder.rs 中按操作數(shù)類型分派Dynamic/Divergent/Never直接返回操作數(shù)類型本身Dynamic透?jìng)鱊ever保持Never見 builder.rsTypeAlias解包別名后遞歸推斷alias.value_type字面量快速路徑int_literal原樣返回-int_literal做checked_neg溢出時(shí)回退到int實(shí)例~int_literal/~bool_literal按位取反后得到新的整數(shù)字面量類型其中~bool會(huì)額外檢查 typeshed 中對(duì)__invert__的廢棄標(biāo)記見 builder.rsConstraintSet上的~直接對(duì)約束集取反not分支通過try_bool求真值并取反同時(shí)做取反冗余檢查見 builder.rs受限 TypeVarconstrained TypeVar對(duì)每個(gè)約束逐一調(diào)用 dunder若全部成功則保留約束映射后的類型任一失敗則報(bào)告unsupported-operator并回退帶 upper bound 的 TypeVar 則委托給 bound 類型無約束 TypeVar 走默認(rèn) dunder 查找見 builder.rs兜底分支FunctionLiteral、ClassLiteral、SubclassOf、Union、NominalInstance、ProtocolInstance、KnownInstance等所有其他類型統(tǒng)一走fallback_unary_expression_type()即執(zhí)行第四節(jié)所述的try_call_dunder查找與錯(cuò)誤回退。try_call_dunder本身定義在 types.rs它通過member_lookup_with_policy在操作數(shù)類型上查找 dunder 方法再對(duì)方法的bindings做參數(shù)匹配與類型檢查最后返回BindingsOk或CallDunderErrorErr包括MethodNotAvailable、PossiblyUnbound、CallError三種變體。關(guān)鍵點(diǎn)是隱式 dunder 調(diào)用會(huì)強(qiáng)制疊加MemberLookupPolicy::NO_INSTANCE_FALLBACK確保查找嚴(yán)格走類/元類鏈而不回退到實(shí)例成員——這正是類實(shí)例可用、類對(duì)象不可用場(chǎng)景一 vs 場(chǎng)景二/四的根源。十、unsupported-operator診斷規(guī)則unsupported-operator是ty內(nèi)置 lint 規(guī)則之一在 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, } }它檢測(cè)二元、一元、比較表達(dá)式中操作數(shù)不支持運(yùn)算符的情況默認(rèn)級(jí)別為Error。對(duì)應(yīng)的規(guī)則文檔位于 crates/ty_python_semantic/resources/lint_docs/unsupported-operator.md其中說明了判定邏輯operands dont support the operator、危害運(yùn)行時(shí)將拋出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)獲取診斷構(gòu)建器生成形如Unary operator{op}is not supported for object of type{operand_type} 的主錯(cuò)誤再按需追加Xdoes not implement__pos__的info提示。注意主信息中的{op}是運(yùn)算符本身的顯示文本/-/~而{operand_type}使用Type::display格式化如class Yes、type[Yes]、Yes | No、def f() - Unknown這解釋了各場(chǎng)景中錯(cuò)誤信息措辭的差異。十一、如何運(yùn)行與擴(kuò)展這些測(cè)試mdtest 測(cè)試不需要手工準(zhǔn)備環(huán)境直接使用 Cargo 即可。例如只跑一元運(yùn)算符相關(guān)的夾具cargo test -p ty_python_semantic --test mdtest mdtest/unary或者按datatest_stable的命名規(guī)則精確匹配某個(gè)文件cargo test -p ty_python_semantic --test mdtest custom倉庫中與custom.md同目錄的 crates/ty_python_semantic/resources/mdtest/unary/ 還包含三份姊妹夾具覆蓋不同側(cè)重點(diǎn)integers.md整數(shù)字面量上/-/~的快速路徑與字面量類型折疊invert_add_usub.md實(shí)例場(chǎng)景以及 TypeVar 帶 bound如boundfloat會(huì)被視為int | float與受限 TypeVar每個(gè)約束都支持運(yùn)算符時(shí)保留 TypeVar 類型的推斷規(guī)則not.mdnot運(yùn)算符的真值推斷與冗余檢查。閱讀這些夾具時(shí)可以遵循的通用語法約定# revealed: type斷言表達(dá)式推斷出的類型# error: [rule] message斷言某行會(huì)觸發(fā)指定 lint 并匹配錯(cuò)誤信息# snapshot: name與snapshot代碼塊組合用于斷言多行診斷快照。所有快照最終會(huì)寫入 crates/ty_python_semantic/resources/mdtest/snapshots/ 目錄與夾具一一對(duì)應(yīng)。若行為有變可通過INSTA_UPDATEalways等 insta 快照機(jī)制重新生成。結(jié)語custom.md雖然只是一份測(cè)試夾具卻完整定義了 Ruffty類型檢查器在自定義一元運(yùn)算上的行為契約實(shí)例沿 MRO 查找 dunder 方法并用其返回類型作為結(jié)果類對(duì)象、子類對(duì)象與函數(shù)對(duì)象一律不支持聯(lián)合類型在成員缺失時(shí)報(bào)錯(cuò)但仍能給出部分信息并附帶缺失成員的具體提示元類上的 dunder 方法則讓類對(duì)象本身重新獲得運(yùn)算符支持。結(jié)合 builder.rs、types.rs 與 diagnostic.rs 的實(shí)現(xiàn)可以完整還原這套從語法節(jié)點(diǎn)到類型結(jié)果再到診斷輸出的調(diào)用鏈。理解這些規(guī)則既有助于把握ty檢查器的語義邊界也能為在類型檢查器之上編寫工具或擴(kuò)展提供可靠的參考基線。【免費(fèi)下載鏈接】ruffAn extremely fast Python linter and code formatter, written in Rust.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ru/ruff創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考