Ruff 项目 ty 类型检查器 `unused-awaitable` 诊断深度解析:从 mdtest 测试用例到源码实现
Ruff 项目 ty 类型检查器unused-awaitable诊断深度解析从 mdtest 测试用例到源码实现【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本文围绕 Ruff 仓库中 ty 类型检查器的unused-awaitable诊断lint展开以 crates/ty_python_semantic/resources/mdtest/diagnostics/unused_awaitable.md 这份 mdtest 测试文档为骨架系统梳理该诊断的触发条件、豁免场景、类型层面的判定规则并结合ty_python_semanticcrate 的源码实现解释其底层原理。读完本文你将掌握ty 如何识别未被 await 的协程这类静默错误、Union/Intersection/动态类型分别如何处理、reveal_type与assert_type为何被豁免以及如何阅读与运行这类以 Markdown 为载体的可执行测试文档。一、什么是unused-awaitable诊断unused-awaitable是 tyRuff 仓库内以 Rust 实现的 Python 类型检查器提供的一条稳定 lint 规则其职责是检测那些以表达式语句expression statement形式出现、却从未被await的可等待awaitable对象。在源码中该 lint 的声明位于 crates/ty_python_semantic/src/types/diagnostic.rsdeclare_lint! { #[doc include_str!(../../resources/lint_docs/unused-awaitable.md)] pub(crate) static UNUSED_AWAITABLE { summary: detects awaitable objects that are used as expression statements without being awaited, status: LintStatus::stable(0.0.21), default_level: Level::Warn, } }从中可以确认三个关键事实默认级别为warn该诊断默认以警告级别报告而非错误Level::Error自 0.0.21 起标记为稳定LintStatus::stable(0.0.21)表明它不是预览特性文档与代码同源lint 的官方说明通过include_str!直接嵌入自 crates/ty_python_semantic/resources/lint_docs/unused-awaitable.md保证规则文档与实现始终同步。规则索引页 crates/ty/docs/rules.md 中也收录了该规则标注默认级别为warn、自 0.0.21 加入。二、为什么未 await 的协程是一种缺陷要理解这条诊断的价值需要先回顾 Python 的异步语义调用一个async def函数并不会执行函数体而是返回一个协程对象coroutine。如果这个协程对象从未被await函数体内的代码永远不会执行——这几乎总是一个编程错误。Python 解释器在运行时遇到这种情况会发出RuntimeWarning: coroutine was never awaited但这类警告极易被忽略。ty 的优势在于它在静态检查阶段就能发现这类问题无需等到运行时。lint 文档 crates/ty_python_semantic/resources/lint_docs/unused-awaitable.md 给出了最直观的示例async def fetch_data() - str: return data async def main() - None: # Warning: coroutine is not awaited fetch_data() # error await fetch_data() # OK三、源码层面诊断如何在类型推断阶段触发unused-awaitable的实现在类型推断器infer_body中位于 crates/ty_python_semantic/src/types/infer/builder.rs。其核心逻辑非常简洁fn infer_body(mut self, suite: [ast::Stmt]) { let db self.db(); for statement in suite { self.infer_maybe_standalone_statement(statement); if let ast::Stmt::Expr(ast::StmtExpr { range: _, node_index: _, value, }) statement { let ty self.expression_type(value); if ty.is_awaitable(self.db()) !self.is_known_function_call(value) { if let Some(builder) self.context.report_lint(UNUSED_AWAITABLE, value.as_ref()) { builder.into_diagnostic(format_args!( Object of type {} is not awaited, ty.display(db, self.program_environment()), )); } } } } self.check_suite_for_redundant_conditions(suite); }从实现可以提炼出诊断触发的三个必要条件语句形态必须是表达式语句ast::Stmt::Expr即该调用单独成行、其结果被丢弃表达式的推断类型必须可等待ty.is_awaitable(db)该调用不是已知的诊断辅助函数!self.is_known_function_call(value)。满足以上条件时ty 会报告形如Object of type \{类型} is not awaited 的诊断信息。3.1 已知函数豁免reveal_type与assert_type第三个条件的实现是is_known_function_call同样位于 builder.rs/// Returns true if expr is a call to a known diagnostic function /// (e.g., reveal_type or assert_type) whose return value should not /// trigger the unused-awaitable lint. fn is_known_function_call(self, expr: ast::Expr) - bool { let ast::Expr::Call(call) expr else { return false; }; matches!( self.expression_type(call.func), Type::FunctionLiteral(f) if matches!( f.known(self.db()), Some(KnownFunction::RevealType | KnownFunction::AssertType) ) ) }reveal_type和assert_type是类型检查阶段的辅助函数它们的返回值只服务于类型展示与断言并不代表忘记了 await。若不对它们豁免类型检查器在帮助开发者排查类型问题时反而会制造噪音。四、类型级判定is_awaitable的递归规则诊断的第二块基石是Type::is_awaitable定义于 crates/ty_python_semantic/src/types.rs/// Returns true if this type is an awaitable that should be awaited before being discarded. /// /// Currently checks for instances of types.CoroutineType (returned by async def calls). /// Unions are considered awaitable only if every element is awaitable. /// Intersections are considered awaitable if any positive element is awaitable. fn is_awaitable(self, db: db dyn Db) - bool { match self { Type::NominalInstance(instance) { matches!(instance.known_class(db), Some(KnownClass::CoroutineType)) } Type::Union(union) { let elements union.elements(db); // Guard against empty unions (Never), since all() on an empty // iterator returns true. !elements.is_empty() elements.iter().all(|ty| ty.is_awaitable(db)) } Type::Intersection(intersection) intersection .positive(db) .iter() .any(|ty| ty.is_awaitable(db)), _ false, } }这段代码揭示了三条重要的判定策略与测试文档中的用例一一对应类型形态判定策略语义解释普通实例NominalInstance仅当已知类是types.CoroutineTypeasync def调用返回的正是CoroutineType实例联合类型Union所有元素都可等待才触发只要联合中存在非可等待分支该表达式就可能不是协程不应报警交集类型Intersection任一正向元素可等待即触发交集蕴含所有成员约束只要其中一个是协程丢弃它就是错误的动态类型Any / Unknown及所有其他类型不触发动态类型信息不足避免误报注意Never空联合的特殊处理all()对空迭代器返回true因此代码先用!elements.is_empty()守卫避免对Never类型误报。五、mdtest 测试文档逐例解读unused_awaitable.md本质上是一份mdtest 测试文档它以 Markdown 为承载、以 Python 代码块为可执行用例通过行尾注释声明预期结果# error: [unused-awaitable]表示该行应触发此诊断# revealed: 类型表示reveal_type的输出。这类文档同时充当规范说明与回归测试由仓库中的 mdtest 测试框架crates/mdtest驱动执行。下面逐例解析文档中的 11 个场景。5.1 基础场景未 await 的协程调用async def fetch() - int: return 42 async def fetch_complex(x) - int: return 42 async def main(): fetch() # error: [unused-awaitable] fetch_complex(lambda: None) # error: [unused-awaitable]调用async def函数会产生必须被 await 的协程。即使参数形式不同此处第二个调用携带了 lambda 参数只要结果是CoroutineType且作为表达式语句被丢弃就会触发诊断。5.2 已 await 的协程不触发async def fetch() - int: return 42 async def main(): await fetch()这是正确写法await fetch()真正执行了异步函数体协程被消费因此没有任何诊断。5.3 赋值给变量的协程当前不触发async def fetch() - int: return 42 async def main(): # TODO: ty should eventually warn about unused coroutines assigned to variables coro fetch()协程被赋值给变量coro后不再属于表达式语句被丢弃的形态因此当前不会触发诊断。文档中的 TODO 注释表明ty 未来计划对赋值后从未使用的协程变量也发出警告——这正是当前实现的已知边界值得关注后续演进。5.4 作为参数传入函数不触发async def fetch() - int: return 42 async def main(): print(fetch())当协程作为实参被传递而不是单独成行的表达式语句时它仍有机会被消费因此不应报警。这与infer_body只检查ast::Stmt::Expr形态的实现是一致的。5.5 模块顶层调用同样触发async def fetch() - int: return 42 fetch() # error: [unused-awaitable]lint 在async def之外同样生效——因为协程依然被丢弃无论它出现在哪里这都是一处缺陷。这也印证了infer_body对任意语句块的统一处理逻辑。5.6 联合类型全部可等待才触发from types import CoroutineType from typing import Any def get_coroutine() - CoroutineType[Any, Any, int] | CoroutineType[Any, Any, str]: raise NotImplementedError async def main(): get_coroutine() # error: [unused-awaitable]CoroutineType[Any, Any, int] | CoroutineType[Any, Any, str]的每一个分支都是协程类型无论实际返回哪种丢弃它都是错误因此触发诊断。5.7 含非可等待分支的联合不触发from types import CoroutineType from typing import Any def get_maybe_coroutine() - CoroutineType[Any, Any, int] | int: raise NotImplementedError async def main(): get_maybe_coroutine()联合中混入了int这一非可等待分支。该表达式可能返回普通整数直接丢弃是合法的常见写法例如某些回调模式因此诊断不应触发。这正对应is_awaitable中联合须全部元素可等待的规则。5.8 交集类型含可等待元素即触发from collections.abc import Coroutine from types import CoroutineType from ty_extensions import Intersection class Foo: ... class Bar: ... def get_coroutine() - Intersection[Coroutine[Foo, Foo, Foo], CoroutineType[Bar, Bar, Bar]]: raise NotImplementedError async def main(): get_coroutine() # error: [unused-awaitable]交集类型Intersection[...]表示对象同时满足所有成员约束。只要其中一个正向成员是可等待的该对象就必然是可等待的丢弃它同样是缺陷。此例还展示了 ty 扩展模块ty_extensions.Intersection的用法见 crates/ty_python_semantic/resources/mdtest/intersection_types.md。5.9reveal_type与assert_type明确豁免from typing_extensions import assert_type from types import CoroutineType from typing import Any async def fetch() - int: return 42 async def main(): reveal_type(fetch()) # revealed: CoroutineType[Any, Any, int] assert_type(fetch(), CoroutineType[Any, Any, int])reveal_type(fetch())在类型检查器中显示fetch()的类型为CoroutineType[Any, Any, int]这正是async def调用的返回类型assert_type则断言该类型与声明一致。二者都是类型检查辅助手段其参数中的协程并不会被丢弃因此不触发诊断——这由前面分析的is_known_function_call机制保证。5.10 普通函数调用不触发def compute() - int: return 42 def main(): compute()compute()返回普通int本身不可等待丢弃它是完全合法的表达式语句不触发诊断。5.11 动态类型不触发from typing import Any def get_any() - Any: return None async def main(): get_any()Any以及Unknown属于动态类型类型信息不足ty 采取保守策略不报警避免对动态代码产生误报。六、诊断信息的实际形态与报告级别当上述任一场景触发时ty 会输出类似如下的诊断Object of type CoroutineType[Any, Any, int] is not awaited消息中的类型通过ty.display(db, env)格式化直接呈现协程的具体类型参数。由于UNUSED_AWAITABLE的默认级别是warn它在 ty 的默认输出中作为警告报告不会阻断类型检查流程。与unused-awaitable同族的还有unused-ignore-comment诊断见 rules.md用于检查不再适用的ty: ignore抑制注释——这提示 ty 的诊断体系对抑制机制本身也有自检能力。如需针对单行抑制可以参考仓库中ty: ignore[...]的注释语法例如# ty: ignore[unused-awaitable]抑制机制的实现见 crates/ty_python_semantic/src/suppression.rs。七、如何阅读与运行 mdtest 测试文档unused_awaitable.md位于ty_python_semantic的测试资源目录 crates/ty_python_semantic/resources/mdtest/diagnostics/ 下。这类文档的阅读约定是每个##小节代表一个独立测试场景标题即场景语义代码块中的# error: [unused-awaitable]声明该行期望触发此诊断# revealed: 类型声明reveal_type的期望输出无行尾注释的代码块表示不应触发任何诊断。mdtest 框架支持分层配置文档根部的 TOML 配置块如[environment] python-version 3.10会被子章节继承或覆盖具体规则见 crates/ty_python_semantic/resources/mdtest/mdtest_config.md。这些 Markdown 测试由 mdtest harnesscrates/mdtest 与 crates/ruff_mdtest解析并驱动执行既是类型检查器行为的权威文档也是持续集成中自动运行的回归测试。八、总结从测试文档到实现的一条完整链路通过unused-awaitable这一个诊断可以看到 ty 项目文档即测试、测试即规范的设计风格行为规范由 mdtest 文档 unused_awaitable.md 以 11 个可执行用例精确定义规则声明由 diagnostic.rs 中的declare_lint!宏登记默认级别warn、稳定版 0.0.21触发逻辑由 builder.rs 在infer_body中完成表达式语句 可等待类型 非已知函数的三重判定类型判定由 types.rs 的is_awaitable递归处理普通实例、Union全元素、Intersection任元素与动态类型。掌握了这条链路你不仅可以准确预测 ty 在何种代码上会报告unused-awaitable还能举一反三地理解 ty 其他基于类型推断的 lint如redundant-condition、division-by-zero等在 crates/ty_python_semantic/resources/mdtest/diagnostics/ 目录下的组织方式与阅读方法。对于在代码中排查协程忘加 await类问题unused-awaitable是把运行时警告前移到编译期的最佳实践。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考