ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ruff RUF065:logging 调用中“提前字符串转换”的静态检查与星号参数边界解析

Ruff RUF065:logging 调用中“提前字符串转换”的静态检查与星号参数边界解析 Ruff RUF065logging 调用中“提前字符串转换”的静态检查与星号参数边界解析【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff本篇围绕 Ruff 的RUF065logging-eager-conversion规则展开说明它如何识别logging调用中对参数做str()/repr()等“提前格式化”的反模式、如何在*args星号展开参数出现时安全地终止位置映射并结合 规则实现源码 与 mdtest 行为测试 给出可复现的配置与验证方式。规则概览什么是logging-eager-conversionRUF065属于 Ruff 内置规则集Ruff前缀性能类Performance自0.13.2起进入 Preview注册于 codes.rs(Ruff, 065) rules::ruff::rules::LoggingEagerConversion,其动机来自规则文档注释logging_eager_conversion.rslogging调用的参数在记录时会被自动格式化为字符串因此在传参前就主动执行str(x)、repr(x)之类的转换既多余又低效。典型反模式与修复方式如下# 反模式提前把 user 转成字符串 logging.info(%s - Something happened, str(user)) # 推荐写法直接把原始值交给 logging logging.info(%s - Something happened, user)由于该规则仍处于 Preview 状态启用时需要同时打开 Preview 并显式选择该规则。mdtest 用例文件 顶部给出的 TOML 配置即为标准写法lint.preview true lint.select [RUF065]在 ruff.md 文档 中该规则关联的唯一配置项是lint.logger-objects见下文“日志调用识别”一节。日志调用的识别启发式匹配与参数定位RUF065 的第一步是判断一个函数调用是否算“日志调用”。实现上复用了flake8-logging-format规则包的公共逻辑导入自 flake8_logging_format 模块let Some((logging_call_type, _range)) find_logging_call(checker, call) else { return; };从 规则文档注释 的 “Known problems” 一节可以看到该启发式的具体匹配面logging模块本身的用法如import logging; logging.info(...)flask.current_app.logger的用法在定义所在文件内使用、且名称以log开头或以logger/logging结尾的对象如logger logging.getLogger(); logger.info(...)通过lint.logger-objects显式声明的共享 logger 对象例如配置lint.logger-objects [module.logger]后from module import logger; logger.info(...)也会进入检查范围。识别之后还需定位格式字符串参数msg的位置。源码按调用类型区分logging_eager_conversion.rslet msg_pos match logging_call_type { LoggingCallType::LevelCall(_) 0, // logging.info(msg, ...) LoggingCallType::LogCall 1, // logging.log(logging.INFO, msg, ...) }; let Some(Expr::StringLiteral(string_literal)) call.arguments.find_argument_value(msg, msg_pos) else { return; // msg 不是字符串字面量则直接退出 };也就是说logging.log(LEVEL, msg, *args)这种形式中msg占第 1 个位置msg_pos 1而logging.info(msg, *args)中msg占第 0 个位置。若msg不是字符串字面量如 f-string、变量规则无法解析占位符会静默返回。随后用 cformat 解析器 把msg解析成CFormatString提取其中的%占位符序列。检测的转换模式与诊断消息规则将解析出的占位符序列与msg之后的位置参数逐一zip并对每个参数检查其是否为“提前转换调用”。不同转换函数对应不同的诊断消息message 实现模式占位符诊断消息str(x)%sUnnecessary str() conversion when formatting with %s直接去掉str()repr(x)%sUnnecessary repr() conversion when formatting with %s. Use %r instead of %sascii(x)%sUnnecessary ascii() conversion when formatting with %s. Use %a instead of %soct(x)%sUnnecessary oct() conversion when formatting with %s. Use %#o instead of %shex(x)%sUnnecessary hex() conversion when formatting with %s. Use %#x instead of %s两点实现细节值得注意str()的保守判定仅当str()恰好有一个位置参数或关键字参数、且参数不是可变展开variadic时才报例如str(object!)会被标记见 RUF065_1 快照而str(*args)这类形式则不会误报。oct()/hex()的复杂规格豁免oct()/hex()建议改用%#o/%#x但仅当占位符没有“复杂转换规格”时成立。has_complex_conversion_specifier 函数在以下任一情况返回true从而豁免使用了0零填充标志、未使用-左对齐标志且指定了最小宽度使用了空格或符号标志指定了精度precision。这些情况下%#o/%#x无法等价表达格式提前转换反而是必要手段。完整的功能面快照可以查看 RUF065_0 的测试快照其中覆盖了logging.info/logging.log、从logging直接导入的info/log、%r repr()这类“恰好等价”的边界logging.warning(Value: %r, repr(42))不报而logging.info(Debug info: %s, repr(test\nstring))报。mdtest 行为测试星号展开参数Unpacked argumentslogging-eager-conversion.md 是 Ruff 的 mdtest 行为测试以.md文件承载规则级测试用例文件头部的 TOML 块声明 lint 配置Python 代码块会被抽取成片段文件执行 lint再用内联的# snapshot:注释与文末的snapshot块做逐字断言。该文件针对的核心场景是星号表达式*args破坏了“占位符 ↔ 位置参数”的一一映射时的判定策略星号表达式*args会打断格式串占位符与变长日志参数之间的位置映射。保证星号参数之前的提前转换仍被标记但在它之后的模糊情形上直接放弃bail out。完整测试代码mdtest 原文import logging # 1. Starred before eager conversion (should not trigger for repr(5) because the mapping is broken) logging.warning(%s%s%s%s %s, *1234, repr(5)) # 2. Eager conversion before starred (should trigger for repr(1) because it maps reliably) logging.warning(%s %s, repr(1), *[1234]) # snapshot: logging-eager-conversion # 3. Multiple starred arguments (should not trigger anywhere) logging.warning(%s %s %s, *[1], *[2], repr(3)) # 4. Mixed specifiers and eager conversion before starred (should trigger for repr(1)) logging.warning(%s %s %s, repr(1), *[2, 3]) # snapshot: logging-eager-conversion预期的诊断输出mdtest 文末 snapshot 块error[RUF065]: Unnecessary repr() conversion when formatting with %s. Use %r instead of %s -- src/mdtest_snippet.py:7:26 | 7 | logging.warning(%s %s, repr(1), *[1234]) # snapshot: logging-eager-conversion | ^^^^^^^^^ error[RUF065]: Unnecessary repr() conversion when formatting with %s. Use %r instead of %s -- src/mdtest_snippet.py:13:29 | 13 | logging.warning(%s %s %s, repr(1), *[2, 3]) # snapshot: logging-eager-conversion | ^^^^^^^^^四个用例的行为解读星号在前用例 1*1234是第一个位置参数后面repr(5)到底对应第几个占位符取决于*1234展开后有几个元素静态无法确定不报提前转换在星号前用例 2repr(1)稳定地映射到第一个%s映射可靠报多个星号参数用例 3第一个参数就是星号展开后续映射完全不可知任何位置都不报混合占位符用例 4repr(1)仍在第一个%s上可靠映射报且不影响对*[2, 3]展开后剩余占位符的不确定处理。这套“报能可靠映射的、放过映射被破坏的”策略正是为了避免误报一旦星号展开插入其后参数的实际运行时取值个数未知继续按序配对占位符就可能把repr(x)错配到非%s的占位符上。源码机制take_while在首个星号表达式处截断映射上述 mdtest 语义在实现中对应一个非常简洁的控制点logging_eager_conversion.rs// Iterate over % placeholders in format string and zip with logging statement arguments for (spec, arg) in format_string .iter() .filter_map(|(_, part)| { if let CFormatPart::Spec(spec) part { Some(spec) } else { None } }) .zip( call.arguments .args .iter() .skip(msg_pos 1) .take_while(|arg| !arg.is_starred_expr()), ) { // ... 对 (spec, arg) 逐对检查提前转换 }关键点在于参数侧迭代器上的take_while(|arg| !arg.is_starred_expr())从msg之后的第一个实参开始按序取用迭代器在遇到第一个星号表达式时立即结束其后的实参无论是否含repr()等调用都不再与占位符配对占位符序列比实参序列长时zip本身就会在较短的一侧结束时停止因此占位符多于参数的情况同样安全。这正是用例 1/3 “映射被破坏即放弃”、用例 2/4 “星号前仍可可靠映射”的直接来源logging.warning(%s %s, repr(1), *[1234])中repr(1)是首个实参先于take_while终止被取走与第一个%s配对命中FormatConversion::Strmatch_builtin_expr(func, repr)分支而报错而logging.warning(%s%s%s%s %s, *1234, repr(5))中首个实参即*1234take_while第一轮就停止循环体一次都不执行。从源码结构看该截断策略是有意为之的“宁可漏报、不可错报”设计mdtest 用四个正反用例把它固化成了回归保障。运行与验证方式在只读查看本仓库的前提下可以在自己的项目中复现该行为准备配置文件ruff.toml或pyproject.toml的[tool.ruff.lint]段[lint] preview true select [RUF065]运行检查ruff check --select RUF065 --preview your_project若要复现本仓库的 mdtest 断言测试体系位于 ruff_mdtest 工具包它按文件内 TOML 配置抽取代码块、执行 lint 并与snapshot块比对规则单元级快照则存放在 crates/ruff_linter/src/rules/ruff/snapshots其中与 RUF065 相关的两份是 RUF065_0.py.snap 与 RUF065_1.py.snap。适用前提与限制该规则依赖msg为字符串字面量才能解析占位符logging调用的识别是启发式的若项目使用其他日志门面如loguru、structlog可通过lint.logger-objects显式声明其模块路径以纳入检查仅限文档中列出的名称启发式与显式配置两条途径。小结RUF065用“CFormatString解析占位符 实参逐位配对”的方式捕捉logging参数上的冗余str()/repr()/ascii()/oct()/hex()调用并给出%r、%a、%#o、%#x等等价替代其工程价值的关键在于对*args星号展开的边界处理——通过take_while在首个星号表达式处终止映射在“映射可靠”与“映射破坏”之间划出一条可测试、可回归的判据线而 mdtest 用例文件 恰好把这条例线用四个正反用例完整固化了下来。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进