AI 生成的类型注解如何骗过 mypy
AI 助手生成的类型注解在语法上通过 mypy 检查,但语义错误会在运行时暴露。提出 Pyre/Pysa 等防护工具方案。
完整中文译文
为什么 AI 生成结构有效但语义有误的类型提示
AI 代码助手生成的类型提示满足 mypy 的结构验证器 —— 注解在句法上正确且能通过类型检查 —— 但其语义意义与代码的实际行为不匹配,原因是 mypy 验证的是注解的一致性,而不是注解者意图的正确性。BrassCoders 将 Pyre/Pysa 作为其 12 个扫描器之一运行,添加了污点分析,可以跟踪跨越类型注解边界的数据流,而不仅仅是检查注解的一致性。
根本原因在于 AI 助手学习注解模式的方式。一个在数百万 Python 文件上训练的助手已经在数千个上下文中见过 Optional[str]。它根据函数签名的形状进行模式匹配 —— 而不是根据每个调用方的行为。当你的其余代码库在没有 None 检查的情况下解引用返回值时,助手不会跟踪该调用图。mypy 按照其文档化设计,验证的是注解在结构上的一致性,而不是其行为正确性。
这种区别对大规模类型化代码库很重要。当注解撒谎时,mypy 的绿灯会训练审查者将类型检查输出视为行为证明。但事实并非如此。
欺骗 mypy 的四种类型提示模式
BrassCoders 的 Pyre/Pysa 扫描器对类型注解代码执行污点分析,可以标记类型不安全的数据流,但 AI 生成且 mypy 无声接受的四种模式是:可选字段上的 Optional[str](但下游代码从不处理 None)、折叠结构的 Dict[str, Any](mypy 本可以强制执行)、异步函数上的不正确返回类型注解,以及不进行转换而撒谎的 cast() 调用。
Optional[str] 最为常见。AI 助手看到一个理论上可能缺失的字段,将其注解为 Optional[str] —— 在隔离情况下技术上正确。问题在于每个下游调用方都直接解引用它:user.name.upper()。mypy 根据调用方自己的注解来检查调用方,而不是检查 None 分支是否被处理。AttributeError 就此埋下。
Dict[str, Any] 是完全折叠类型安全的注解。Python 的 typing 模块提供了 TypedDict 恰好用于这种情况 —— 一个具有已知键形状的字典,mypy 可以强制执行。AI 助手默认使用 Dict[str, Any],因为它从不会导致类型检查失败。它也不提供任何强制执行。
异步函数上的返回类型不匹配更微妙。一个注解为 -> str 的异步函数如果返回协程对象会编译并通过类型检查。在运行时,等待一个字符串的调用代码会收到异常,而不是字符串。只有当函数在类型检查的上下文中被显式调用时,mypy 才会捕获这种不一致。未注解的调用方会让它通过。
cast() 是最直接的谎言。它告诉 mypy"相信我,这个值是类型 T"而不在运行时修改该值。cast() 在运行时是纯无操作。AI 助手使用它来静音类型检查器无法解决的类型错误。底层的不一致仍然存在。
当注解撒谎时:运行时 vs 静态分析
mypy 的类型检查器在分析时操作注解:它从不执行代码。一个声称函数返回 str 的注解被 mypy 信任,即使该函数在 mypy 没有跟踪的代码路径中返回 None。BrassCoders 的 Pysa 扫描器跟踪这些流 —— 跟随 None 通过函数调用,直到它到达会导致崩溃或产生不正确行为的汇点。
范围上的差异很重要。mypy 执行局部类型推断:它检查每个函数的主体是否与函数声明的签名一致,以及调用站点是否与签名匹配。它不执行全局可达性分析来了解什么值在运行时实际通过调用图。当一个函数注解为 -> str 但有一个代码路径返回 None 时,mypy 仅在 None 返回在 mypy 能分析的分支中时才标记不一致。通过返回类型为 Any 的第三方 API 调用到达的 None 会无法检测地通过。
Pysa 的污点分析从源开始 —— 用户输入、外部 API 响应、数据库读取 —— 并跟踪这些值通过调用图进行的污点流向。它不仅仅检查注解。它跟踪可达性。当被污染的 None 跨越一个标记为 str 的类型注解边界时,那是一个发现,而不是绿灯。
BrassCoders 在类型化 Python 代码中标记什么
BrassCoders 将 Pyre/Pysa 作为其 12 个扫描器之一运行 —— Pysa 对类型化 Python 代码执行跨程序污点分析,标记污点跨越类型边界的数据流。它也标记 cast() 使用模式和与安全敏感操作相邻的未注解代码路径。
在同一代码库上与 mypy 一起运行 BrassCoders 会给你两种不同的分析。mypy 在类型不匹配的调用站点发现注解不一致。Pysa 发现可达性问题:危险值到达敏感汇点的代码路径,无论注解如何。这两个工具是互补的,而不是冗余的。
使用 --strict 和编辑器中的 Pyright 将 BrassCoders 添加到你的 CI 管道中以进行实时注解验证。在 macOS、Linux 和 Windows (WSL2) 上,使用以下命令安装:
pip install brasscoders
brasscoders scan .
OSS 核心在本地运行,没有出站网络调用。Apache 2.0。无需账户。Pyre/Pysa 作为 12 个扫描器之一被包含,所以污点分析在每次扫描时自动运行。
BrassCoders 付费版在 OSS 核心基础上添加基于嵌入的噪声消减,费用为每个开发者每月 $12 —— 一旦你的扫描输出超过你想要手动分类的数量就很有用。OSS 核心配合 Pysa 足以捕获这里描述的类型提示模式。
基准:BrassCoders 在已发布语料库中捕获了 12 个 AI 生成的 bug 中的 11 个。Bandit 捕获了其中 6 个。类型提示失败是 Bandit 根本看不到的类别。