- 静态分析
- 代码质量
- Lint
- 开发工具
【免费下载链接】pylint
It's not just a linter that annoys you!
本文基于 pylint 仓库的 2.2 版本发布摘要(doc/whatsnew/2/2.2/summary.rst)展开,系统梳理该版本引入的 4 项新检查能力(字符串格式类型不匹配、重复格式化参数、日志格式化风格选项、序列内隐式字符串拼接)以及 8 项重要的行为修复。读完本文,你将能理解每项检查对应的消息符号、触发条件、命令行配置方式,并掌握从仓库源码与功能测试中验证这些行为的方法。文中源码路径均指向当前仓库,可用于进一步深入阅读。
版本概览
Pylint 2.2 于 2018-11-25 发布(Release 2.2,Date 2018-11-25),是 2.x 系列早期的一个重要功能版本。该版本在字符串格式校验、日志调用检查两个方向上补强明显,同时修复了一批围绕异常处理、变量作用域与导入分析的误报问题,其中多个修复以 GitHub issue 编号记录(如 #2484、#2431、#996、#2421)。
值得说明的是:当前仓库中的代码已经过多个大版本演进,2.2 时代新增的消息在后续版本中可能被重命名或扩展了触发条件。本文会同时标注「2.2 时的形态」与「当前仓库中的实现」,帮助读者在最新代码中定位这些功能的对应实现。
新增检查器与检查能力
字符串格式类型不匹配检查(bad-string-format-type)
2.2 的字符串检查器(String checker)开始报告格式字符串与实参类型不匹配的问题,对应消息bad-string-format-type(消息码 E1307,消息文本为 "Argument %r does not match format type %r")。其含义是:格式串要求的转换类型与实际传入参数的类型不兼容。
该检查覆盖两种格式化写法:
- 对
%风格格式化(如"%d" % x),实现位于 字符串检查器 的visit_binop中:通过utils.parse_format_string解析格式串得到每个占位符要求的类型(required_arg_types),再对实参逐一调用arg_matches_format_type判断兼容性(strings.py)。 - 对 PEP 3101 风格(
"{}".format(...)),在_check_new_format_specifiers中同样会执行类型匹配校验。
类型兼容规则在 strings.py 的arg_matches_format_type中有细致定义:
%s、%r、%a可接受任意类型;str子类只能用于%c;int子类(含bool、IntEnum成员)可用于所有整数类转换符;float子类可用于diueEfFgGn%等浮点类转换符。
这意味着"%.2f" % "not_a_float"这类运行时必然出错的写法,在静态分析阶段就能被发现。
重复字符串格式化参数检查(duplicate-string-formatting-argument)
新增消息duplicate-string-formatting-argument(W1308,消息文本 "Duplicate string formatting argument %r, consider passing as named argument"),用于检测同一个变量被作为位置参数多次传入str.format(),此时更合理的写法是改用命名参数。
实现细节位于 strings.py 的_detect_vacuous_formatting:它用collections.Counter统计str.format()调用中位置参数的名字,凡出现次数大于 1 的名字都会触发一条消息。该函数由_check_new_format在完成参数数量校验后调用(strings.py)。
例如以下代码(摘自已存在的功能测试 tests/functional/d/duplicate/duplicate_string_formatting_argument.py):
NAME = 42 OTHER_NAME = 24 OTHER_OTHER_NAME = 2 CONST = "some value {} some other value {} {} {} {} {}".format( NAME, NAME, OTHER_NAME, OTHER_NAME, OTHER_NAME, OTHER_OTHER_NAME, )由于NAME出现 2 次、OTHER_NAME出现 3 次,会同时触发两条duplicate-string-formatting-argument消息(预期输出见 duplicate_string_formatting_argument.txt),提示应将它们改写为"...{NAME}...{OTHER_NAME}...".format(NAME=NAME, OTHER_NAME=OTHER_NAME)形式的命名参数写法。
日志格式化风格选项(--logging-format-style)
这是 2.2 为日志检查器(LoggingChecker)新增的一个命令行选项:--logging-format-style,用于声明项目中日志调用采用的格式化风格,使与%风格耦合的检查能够适配{}风格。
选项定义位于 pylint/checkers/logging.py:
- 类型为
choice,仅接受两个取值:old:%风格格式化(logging.error("%s", x)),默认值;new:{}风格格式化(logging.error("{}", x),即str.format()风格)。
- 命令行写法为
--logging-format-style=old或--logging-format-style=new;配置文件写法为在[LOGGING]小节中设置logging-format-style=new(参考 tests/functional/l/logging/logging_format_interpolation_style.rc)。
该选项的实际作用在 logging.py 的_check_format_string中体现:检查器在visit_module时读取self.linter.config.logging_format_style(logging.py),随后根据风格选择不同的格式串解析路径——old走parse_format_string(%语法),new走parse_format_method_string(PEP 3101 语法),从而正确统计日志调用所需的参数个数,支撑logging-too-many-args、logging-too-few-args、logging-unsupported-format等消息的判定。
仓库测试覆盖了两种风格的行为差异:
- tests/functional/l/logging/logging_format_interpolation_style.py 配套
logging-format-style=new,验证logging.error("{}", 1)、logging.error("{named}", {"named": 1})等调用均不误报; logging_too_few_args_new_style.rc/logging_too_few_args_old_style.rc/logging_too_many_args_new_style.rc等文件分别锁定new/old两种风格下的参数数量检查;- tests/config/test_argparse_config.py 直接以
--logging-format-style=new作为命令行参数运行 pylint,验证选项解析链路。
序列内的隐式字符串拼接检查(implicit-str-concat-in-sequence)
2.2 新增对列表、集合、元组中隐式字符串拼接的检测。所谓「隐式拼接」是指相邻字符串字面量之间缺少逗号,被 Python 编译器自动合并为一个字符串,例如原文档给出的触发示例:
woops = ('a', 'b' 'c')这里'b' 'c'实际是'bc',而作者本意可能是('a', 'b', 'c')这样的三元素元组——正是这种「差一个逗号」的笔误,很容易在配置项、常量表中埋下隐蔽 bug。
该消息在 2.2 时期的符号名为implicit-str-concat-in-sequence(W1403)。在后续版本演进中,检测范围被扩展到了函数调用参数、赋值等更多场景,消息也更名为implicit-str-concat(W1404,消息文本 "Implicit string concatenation found in %s"),并保留了旧符号的别名映射:{"old_names": [("W1403", "implicit-str-concat-in-sequence")]}(见 strings.py)。
当前实现由StringConstantChecker负责,它基于 token 级处理(process_tokens)识别字符串字面量,并针对不同容器分别触发检查:visit_list/visit_set/visit_tuple/visit_call都会调用check_for_concatenated_strings(strings.py)。
功能测试 tests/functional/i/implicit/implicit_str_concat.py 完整刻画了触发与豁免边界:
TEST_LIST1 = ['a' 'b'] # 触发:list 内拼接 TEST_LIST2 = (u"a" u"b", u"c") # 触发:tuple 内拼接 TEST_LIST3 = {r'''a''', r'''b''' r'''c'''} # 触发:set 内拼接 TEST_LIST5 = ('a', 'b' 'c') # 不触发:跨行相邻 TEST_LIST6 = ('...\ ccc') # 不触发:反斜杠续行 TEST_LIST8 = [b'A' b'B'] # 不触发:bytes 字面量其中「跨行相邻不触发」是默认行为,可通过选项--check-str-concat-over-line-jumps(默认False)打开跨行场景的告警(strings.py)。
其他重要改进与修复
try-except-raise 正确处理多级异常继承层次
try-except-raise检查器此前在「except 子句中只有裸raise,且后续存在其异常父类的处理分支」这类场景下判断不准确。2.2 修复了它对多级继承层次(multilevel inheritance hierarchy)的处理,关闭 issue #2484。
当前实现位于 pylint/checkers/exceptions.py 的_check_try_except_raise:当一个 except 分支体首语句为裸raise时记下标记,随后检查后续 handler中是否出现了该裸 raise 分支所捕获异常的父类——如果存在父类处理分支,说明裸 raise 会被外层父类分支兜住,try-except-raise不再误报;反之才发出告警。子类关系的判定借助utils.is_subclass_of(见下文修复)。
useless-import-alias 不再报告import x.y.z as z
此前import x.y.z as z(别名与模块末段同名)会被useless-import-alias误报。2.2 起,检查器对这类「点分导入 + 与末段同名的别名」不再告警。
当前实现位于 pylint/checkers/imports.py 的_check_import_as_rename:仅当未拆分包路径(len(splitted_packages) == 1,即形如import foo as foo)且不满足 re-export 豁免条件时才报告useless-import-alias;而对import x.y.z as z这类两级包名的情况,转而在别名与末段一致时提示改写成from x.y import z(消息consider-using-from-import)。
unnecessary-pass 覆盖「仅含 docstring 与 pass」的定义
unnecessary-pass(W0107)在 2.2 中得到扩展:当函数或类体中只有 docstring 加一个pass语句时,同样会告警——因为此时 docstring 本身已构成合法的空定义体,pass是多余写法。
实现位于 pylint/checkers/base/pass_checker.py:visit_pass在两个条件下触发告警:一是该pass不是父节点唯一的子语句(len(node.parent.child_sequence(node)) > 1);二是其父节点是类或函数定义且存在 docstring(node.parent.doc_node非空)。第二条正是 2.2 扩展的新场景。
修复 nonlocal 引发的 undefined-variable / used-before-assignment 误报
在闭包场景中,内层函数通过nonlocal声明变量后,pylint 此前可能错误报告undefined-variable或used-before-assignment。2.2 修复了这两类误报。
当前仓库中与该修复相关的作用域分析逻辑分散在 pylint/checkers/variables.py 中:包括_is_nonlocal_name(判断名字节点是否为所在 frame 中的 nonlocal 声明,variables.py)、未消费名字过滤时对 nonlocal 的特判(variables.py)、以及_has_nonlocal_in_enclosing_frame对外层 frame 中 nonlocal 声明的向上追溯(variables.py)。总体思路是:凡被 nonlocal 声明接管的名字,其「定义」发生在闭包外层,不应再以内层视角去判定未定义或先使用后赋值。
修复 is_subclass_of 参数非 ClassDef 时的崩溃
pylint.checkers.utils.is_subclass_of此前在传入参数不是ClassDef节点时会抛出异常(例如异常检查过程中对无法推断的节点调用时)。2.2 加入类型守卫:只要 child 或 parent 任一不是ClassDef,函数直接返回False而非崩溃。
当前实现见 pylint/checkers/utils.py:
def is_subclass_of(child: nodes.ClassDef, parent: nodes.ClassDef) -> bool: if not all(isinstance(node, nodes.ClassDef) for node in (child, parent)): return False for ancestor in child.ancestors(): ...这一守卫正是上文try-except-raise多级继承修复所依赖的稳定基础,两者在 2.2 中互为配合。
raising-non-exception 取推断值的最后一个
raising-non-exception用于检查raise语句抛出的对象是否确实继承自BaseException。此前当同一个变量名在不同代码路径被推断为多种类型时,pylint 只取第一个推断结果,容易产生虚假告警。2.2 改为采用推断值的最后一个,从而在「变量复用为多种类型」的写法下显著降低误报,关闭 issue #2431。
当前visit_raise的实现通过utils.safe_infer(expr)对raise表达式做推断,再交由ExceptionRaiseLeafVisitor判定是否为标准异常(pylint/checkers/exceptions.py);对无法确认继承关系的类,则保持静默(has_known_bases为假时不报告,exceptions.py)。
asyncio.coroutine 协程不再误报 not-an-iterable
此前对使用@asyncio.coroutine装饰器构建的协程执行yield from时,pylint 会错误报告not-an-iterable(E1133)。2.2 起显式识别这类协程并跳过迭代性检查,关闭 issue #996。
当前实现位于 pylint/checkers/typecheck.py:_is_asyncio_coroutine通过推断调用目标函数的装饰器,判断其限定名是否为ASYNCIO_COROUTINE;随后visit_yieldfrom在识别到协程时直接返回,不再走_check_iterable(typecheck.py)。
函数内未使用的导入报告 unused-import 而非 unused-variable
此前若未使用的import出现在函数体内,pylint 会错误报告为unused-variable。2.2 起统一改为报告unused-import,关闭 issue #2421。
当前实现见 pylint/checkers/variables.py:在未消费名字的最终处理中,对Import/ImportFrom语句分支单独构造消息文本(如"import foo"、"foo imported from bar as baz"),并以unused-import消息上报;只有非导入语句才落入unused-variable分支。这一归类修正让消息语义与语句类型严格对应,便于在 CI 配置中分别 disable/ignore。
小结:如何在自己的项目中应用 2.2 的检查能力
从 2.2 版本开始,pylint 对「字符串格式」「日志调用」这两类高频易错点的静态检查已经相当完整,其核心能力在当前版本中依然可用(部分消息符号有所演进):
| 能力 | 2.2 消息符号 | 当前仓库消息符号 | 启用方式 |
|---|---|---|---|
| 格式类型不匹配 | bad-string-format-type | bad-string-format-type | 默认开启 |
| 重复格式化参数 | duplicate-string-formatting-argument | duplicate-string-formatting-argument | 默认开启 |
| 日志格式化风格 | logging-format-style选项 | logging-format-style选项 | --logging-format-style=old/new |
| 序列内隐式拼接 | implicit-str-concat-in-sequence | implicit-str-concat(旧名映射保留) | 默认开启 |
| 无必要 pass | unnecessary-pass | unnecessary-pass | 默认开启 |
实用建议:
- 团队统一采用
str.format()/ f-string 风格记录日志时,在 pylint 配置的[LOGGING]小节写入logging-format-style=new,可避免基于%语法的参数个数检查产生噪音; - 序列(列表/集合/元组)字面量中相邻字符串字面量容易漏写逗号,可在 CI 中保持
implicit-str-concat开启,并视需求用check-str-concat-over-line-jumps决定是否覆盖跨行场景; - 若某条消息在新代码中已无意义,可在配置中按符号 disable,例如
disable=duplicate-string-formatting-argument,但建议优先以命名参数改写源码消除告警。
如需在本地复现 2.2 的这些行为,可结合上述 功能测试用例(如tests/functional/d/duplicate/、tests/functional/l/logging/、tests/functional/i/implicit/)逐一运行验证,测试文件中的.rc配置与.txt预期输出是最可靠的「检查行为契约」。
- 静态分析
- 代码质量
- Lint
- 开发工具
【免费下载链接】pylint
It's not just a linter that annoys you!
相关推荐
Pylint 的 bad-format-string-key(W1300):% 风格格式化字符串字典键检查详解
Pylint 的 bad format string key(W1300):% 风格格式化字符串字典键检查详解 导读 bad format string key
静态分析代码质量Lint开发工具Pylint 2.2 版本全解析:新增字符串与日志格式化检查器、配置选项及关键修复
Pylint 2.2 版本全解析:新增字符串与日志格式化检查器、配置选项及关键修复 导读 Pylint 2.2 是 pylint 在 2018 年 11 月发布
静态分析代码质量Lint开发工具pylint bad-format-character(E1300)检查详解:识别与修复 % 旧式字符串格式化中的非法转换字符
pylint bad format character(E1300)检查详解:识别与修复 % 旧式字符串格式化中的非法转换字符 导读 本文聚焦 Pylint 消
静态分析代码质量Lint开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考