Ruff RUF065:logging 调用中“提前字符串转换”的静态检查与星号参数边界解析
2026/9/8 20:56:45 网站建设 项目流程

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

【免费下载链接】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-conversion

RUF065属于 Ruff 内置规则集(Ruff前缀,性能类Performance),自0.13.2起进入 Preview,注册于 codes.rs:

(Ruff, "065") => rules::ruff::rules::LoggingEagerConversion,

其动机来自规则文档注释(logging_eager_conversion.rs):logging调用的参数在记录时会被自动格式化为字符串,因此在传参前就主动执行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.rs):

let 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 %s
ascii(x)%sUnnecessary ascii() conversion when formatting with %s. Use %a instead of %s
oct(x)%sUnnecessary oct() conversion when formatting with %s. Use %#o instead of %s
hex(x)%sUnnecessary hex() conversion when formatting with %s. Use %#x instead of %s

两点实现细节值得注意:

  1. str()的保守判定:仅当str()恰好有一个位置参数或关键字参数、且参数不是可变展开(variadic)时才报,例如str(object="!")会被标记(见 RUF065_1 快照),而str(*args)这类形式则不会误报。
  2. 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 arguments)

logging-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. 星号在前(用例 1)*"1234"是第一个位置参数,后面repr("5")到底对应第几个占位符取决于*"1234"展开后有几个元素,静态无法确定,不报
  2. 提前转换在星号前(用例 2)repr("1")稳定地映射到第一个%s,映射可靠,
  3. 多个星号参数(用例 3):第一个参数就是星号展开,后续映射完全不可知,任何位置都不报
  4. 混合占位符(用例 4)repr("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::Str+match_builtin_expr(func, "repr")分支而报错;而logging.warning("%s%s%s%s %s", *"1234", repr("5"))中首个实参即*"1234"take_while第一轮就停止,循环体一次都不执行。从源码结构看,该截断策略是有意为之的“宁可漏报、不可错报”设计:mdtest 用四个正反用例把它固化成了回归保障。

运行与验证方式

在只读查看本仓库的前提下,可以在自己的项目中复现该行为:

  1. 准备配置文件(ruff.tomlpyproject.toml[tool.ruff.lint]段):

    [lint] preview = true select = ["RUF065"]
  2. 运行检查:

    ruff check --select RUF065 --preview your_project
  3. 若要复现本仓库的 mdtest 断言,测试体系位于 ruff_mdtest 工具包,它按文件内 TOML 配置抽取代码块、执行 lint 并与snapshot块比对;规则单元级快照则存放在 crates/ruff_linter/src/rules/ruff/snapshots,其中与 RUF065 相关的两份是 RUF065_0.py.snap 与 RUF065_1.py.snap。

适用前提与限制:该规则依赖msg字符串字面量才能解析占位符;logging调用的识别是启发式的,若项目使用其他日志门面(如logurustructlog),可通过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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询