详解:检测并自动改写 dict / list / str 子类化,并豁免 Stub 文件)
Ruff FURB189subclass-builtin详解检测并自动改写 dict / list / str 子类化并豁免 Stub 文件【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffRuff 的refurb规则组中包含一条FURB189subclass-builtin规则用于检测对dict、list、str三个内置类型的直接子类化并建议改用collections模块中的UserDict、UserList、UserString。该规则在 preview 模式下可用且明确豁免 stub 文件.pyi以保证类型存根能忠实表达运行时实现。本文以仓库中的 mdtest 文档 subclass-builtin.md 为核心结合 规则实现源码 与 测试夹具完整讲解该规则的启用方式、判定逻辑、自动修复行为以及为什么 stub 文件不受检查。规则背景为什么子类化内置类型是危险的内置类型并不一致地调用自身的 dunder 方法。以dict为例dict.__init__和dict.update()会绕过__setitem__导致继承行为不可靠。规则文档中给出的典型例子是class UppercaseDict(dict): def __setitem__(self, key, value): super().__setitem__(key.upper(), value) d UppercaseDict({a: 1, b: 2}) # Bypasses __setitem__ print(d) # {a: 1, b: 2}构造UppercaseDict({a: 1, b: 2})时__setitem__根本没有被触发键值并未被大写。改用UserDict后则符合预期from collections import UserDict class UppercaseDict(UserDict): def __setitem__(self, key, value): super().__setitem__(key.upper(), value) d UppercaseDict({a: 1, b: 2}) # Uses __setitem__ print(d) # {A: 1, B: 2}这正是FURB189的诊断动机。诊断消息与修复标题在源码 subclass_builtin.rs 中定义诊断消息Subclassing{subclass}can be error prone, usecollections.{replacement}instead修复标题Replace withcollections.{replacement}启用规则需要 preview 模式根据 mdtest 文档 给出的配置该规则目前处于 preview 阶段启用方式为[lint] preview true select [FURB189]这一点与源码中的元信息一致subclass_builtin.rs 通过宏标注了规则的预览起始版本与类别#[violation_metadata(preview_since 0.7.3, category Category::Suspicious)]即FURB189自 0.7.3 版本进入 preview分类为Suspicious可疑用法。规则代码与检查函数的绑定位于 codes.rs(Refurb, 189) rules::refurb::rules::SubclassBuiltin,检查入口挂在 AST 分析器处理类定义Stmt::ClassDef的位置见 statement.rs。判定逻辑单基类、下标表达式与内置符号解析从 检查函数源码 看判定流程为stub 文件直接返回详见后文专节取出类定义的基类参数Arguments要求恰好只有一个基类let [base] **bases否则返回通过map_subscript剥离基类上的下标表达式只检查名称部分。例如class SubscriptDict(dict[str, str])会识别出基类是dict且修复时只替换dict这一段保留下标结构用checker.semantic().resolve_builtin_symbol确认该名称确实解析到内置符号避免误伤同名的局部类名称命中str/dict/list三者之一时才报告诊断。SupportedBuiltins枚举与替换目标的映射关系为被检测的内置类型建议替换为dictcollections.UserDictlistcollections.UserListstrcollections.UserString对照 测试夹具 FURB189.py 可以验证这些边界行为# positives —— 全部命中 FURB189 class D(dict): pass class L(list): pass class S(str): pass class SubscriptDict(dict[str, str]): pass # 命中下标部分被保留 class SubscriptList(list[str]): pass # 命中 # currently not detected —— 当前不检测 class SetOnceDict(SetOnceMappingMixin, dict): pass # 多基类直接跳过 # negatives —— 不命中 class C: pass class I(int): pass # int 不在检测范围 class ActivityState(str, Enum, metaclassCaseInsensitiveEnumMeta): ... # 多基类跳过值得注意的是夹具中明确标注了# currently not detected由于源码要求“恰好一个基类”class SetOnceDict(SetOnceMappingMixin, dict)这种混合了 Mixin 的写法当前不会被报告这是该规则的一个已知检测边界。命中与未命中的诊断输出保存在快照 ruff_linter__rules__refurb__tests__subclass-builtin_FURB189.py.snap 中其中每条诊断都附带note: This is an unsafe fix and may change runtime behavior的提示。自动修复不安全的替换 自动导入SubclassBuiltin实现了AlwaysFixableViolation即始终附带修复方案。修复由两部分组成见 fix 构造代码通过checker.importer().get_or_import_symbol在文件中插入或复用from collections import UserDict / UserList / UserString导入用Edit::range_replacement将基类名称部分下标表达式内部替换为导入绑定名。例如class SubscriptDict(dict[str, str])的修复结果会变为from collections import UserDict class SubscriptDict(UserDict[str, str]): ...该修复被标记为unsafe原因是isinstance(x, dict)、isinstance(x, list)、isinstance(x, str)这类检查在使用对应User*类后会失效UserDict并非dict的子类。源码 docstring 中给出的建议是如果你无法控制下游代码可忽略该检查如果可以控制应把类型检查改为抽象基类例如dict-collections.abc.MutableMapping、list-collections.abc.MutableSequence对str则不存在等价转换。这也解释了为什么快照输出中每条修复都强调运行时行为可能改变。核心豁免Stub 文件.pyi中允许子类化内置类型这是 mdtest 文档 的主体内容在 stub 文件中子类化内置类型必须被允许因为 stub 的职责是忠实表达运行时实现包括第三方库或 CPython 自身的实现细节而这些实现往往不在编写 stub 的人控制范围内。mdtest 中给出的验证样例为一段.pyi代码期望不产生任何诊断class D(dict): ... class L(list): ... class S(str): ... class SubscriptDict(dict[str, str]): ... class SubscriptList(list[str]): ...对应实现非常直接检查函数在进入基类分析之前首先判断源文件类型stub 直接放行subclass_builtin.rs/// FURB189 pub(crate) fn subclass_builtin(checker: Checker, class: StmtClassDef) { if checker.source_type.is_stub() { return; } // ...后续基类判定... }规则 docstring 中也把这一行为写进了文档说明第 19-20 行This rule does not apply to stub files, which should faithfully represent the runtime implementation and may be out of the authors control.也就是说同一个class D(dict)在普通.py文件中会触发FURB189诊断而在.pyi文件中则完全静默。这种“同一代码、不同文件类型、不同 lint 结果”的行为正是 Ruff mdtest 机制存在的意义之一mdtest 目录 下的 Markdown 文件内嵌toml配置块和代码块作为文档与测试一体的回归用例确保“stub 文件豁免”这一行为不会在未来的重构中被破坏。如何在仓库中验证该规则的行为结合本仓库的组织方式可以从三个层面复现和核对FURB189的行为单元测试快照规则测试在 refurb/mod.rs 中注册Rule::SubclassBuiltin对应夹具FURB189.py运行cargo test -p ruff_linter refurb可重新生成/比对snapshots/下的诊断快照快照文件即为上文引用的.snap路径mdtest 文档subclass-builtin.md 以preview true加select [FURB189]的配置声明了测试环境其 pyi 代码块即“无诊断”断言本身手动运行在任意 Python 项目中配置相同的[lint]块后执行ruff check file或ruff check --isolated --preview --select FURB189 file临时启用观察诊断输出与ruff check --fix的自动改写结果即可得到与快照一致的效果。小结FURB189是 Ruff 在 preview 阶段提供的、针对dict/list/str直接子类化的可疑用法检测它利用内置类型绕过 dunder 方法的特性问题引导开发者改用UserDict/UserList/UserString并提供“自动导入 基类替换”的不安全修复。其两条关键边界都可在源码中直接验证一是仅处理单基类定义多基类如 Mixin 组合当前不检测二是stub 文件整体豁免checker.source_type.is_stub()提前返回后者由专门的 mdtest 文档作为回归用例固化。理解这两条边界能帮助你正确评估该规则在真实代码库中的适用范围与修复风险。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考