在源代码中查找对布尔值的隐式强制转换

Finding implicit coercion to boolean in source code

如何在源代码中找到所有对布尔值的隐式转换?这包括条件语句 if x、循环 while x、运算符 x or y 等;但不是 if x == 0if len(x) == 0 等。我不介意使用静态分析器,或 IDE,或正则表达式,或为此目的设计的 python 库.当然会有一些误报,当 x 实际上是布尔值时;没关系。

用例:我发现了由强制转换为布尔值引起的错误。例如,一个变量 x 应该是一个整数或 None 并且被错误地测试为 if not x 暗示 if x is None。我想使所有布尔转换显式(例如,将 if not x 替换为 if x is Noneif x == 0,等等)。当然,它必须手动完成,但至少识别隐式转换发生的位置会有所帮助。

我的第一个想法是修饰 built-in bool 函数,但由于某些原因,这不适用于 Python 3.4.

因此,当已知可能使用的 classes 的完整集合时,我提出了一个解决方案:基本上装饰每个 class.[=16= 的 __bool__ 方法]

def bool_highlighter(f):
    def _f(*args, **kwargs):
        print("Coercion to boolean")
        return f(*args, **kwargs)
    return _f

for c in classes:
    try:
        c.__bool__ = bool_highlighter(c.__bool__)
    except AttributeError:
        pass

我只是假设 classes 是一个包含目标 classes 的可迭代对象。您可能可以动态填充它。

如果您在启动时执行此代码,每个布尔强制转换都会打印 "Coercion to boolean".

只是一个简短的测试:

>>> class Foo:
...     def __init__(self, v):
...         self.v = v
...
...     def __bool__(self):
...         return self.v == 12
...
>>> foo = Foo(15)
>>> if not foo:
...     print("hello")
...
Coercion to boolean
hello

我建议您看一下标准 ast 模块。这是一些简单的代码:

import ast
source = '''
x=1
if not x:
    print('not x')
'''

tree = ast.parse(source)
print(ast.dump(tree))

这是输出:

$ python test.py
Module(body=[Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=1)), If(test=UnaryOp(op=Not(), operand=Name(id='x', ctx=Load())), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='not x')], keywords=[]))], orelse=[])])

Eli Bendersky 写了一篇关于使用 AST 的 article,他包含了一些用于访问 AST 节点的示例代码。你会想去你寻找特定建筑的地方参观。在上面的示例中,您将在 If 节点下寻找(子)表达式,其中操作数被直接视为布尔值,或被视为 Not() 节点的唯一操作数。

找出所有可能的案例可能非常复杂。但我认为您可以使用一两页代码轻松找到 "simple" 个案例(如果 x,如果不是 x,如果 x 或 y)。

编辑: 这里有一些代码(我认为)可以满足您的需求。

import ast
source = '''#Line 1
x=1
y=2

if not x:
    print('not x')

if y is None:
    print('y is none')


while y or not x or (x < 1 and not y and x < 10):
    print('x < 10')
    x += 1

'''

tree = ast.parse(source)

class FindNameAsBoolean(ast.NodeVisitor):
    def __init__(self, lines):
        self.source_lines = lines

    def report_find(self, kind, locn, size=3):
        print("\nFound %s at %s" % (kind, locn))
        print(self.source_lines[locn[0]-1])
        print(' ' * locn[1], '^' * size, sep='')

    def visit_UnaryOp(self, node):
        if isinstance(node.op, ast.Not) and isinstance(node.operand, ast.Name):
            self.report_find('NOT-NAME', (node.lineno, node.col_offset), size=4 + len(node.operand.id))
        self.generic_visit(node)

    def visit_BoolOp(self, node):
        opname = type(node.op).__name__.upper()
        for kid in node.values:
            if isinstance(kid, ast.Name):
                self.report_find('%s-NAME' % opname, (node.lineno, node.col_offset), size=len(kid.id))

        self.generic_visit(node)

class FindTests(ast.NodeVisitor):
    def __init__(self, lines):
        self.source_lines = lines

    def _fnab(self, node):
        cond = node.test
        FindNameAsBoolean(self.source_lines).visit(cond)

    def visit_If(self, node):
        self._fnab(node)
        self.generic_visit(node)

    def visit_While(self, node):
        self._fnab(node)
        self.generic_visit(node)

FindTests(source.splitlines()).visit(tree)

这是输出:

$ python test.py

Found NOT-NAME at (5, 3)
if not x:
   ^^^^^

Found OR-NAME at (12, 6)
while y or not x or (x < 1 and not y and x < 10):
      ^

Found NOT-NAME at (12, 11)
while y or not x or (x < 1 and not y and x < 10):
           ^^^^^

Found NOT-NAME at (12, 31)
while y or not x or (x < 1 and not y and x < 10):
                               ^^^^^

实际上,有一个打字库可以做到这一点。它适用于 python 2 和 python 3.

参见 mypy,使用命令 --strict-boolean

尽管@AustinHastings 对如何使用 ast 做了一个非常有用的答案,但我将接受的答案移到了这个问题上,因为我希望人们知道 mypy -这是一个很棒的工具(不太可能很快被放弃,有 100 多个贡献者,包括 Guido)。