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

Python 在源代码中查找对布尔值的隐式强制,python,python-3.x,static-analysis,Python,Python 3.x,Static Analysis,如何在源代码中找到所有到布尔值的隐式转换?这包括条件语句,如if x,循环,如while x,运算符,如x或y,等等。;但不是如果x==0或如果len(x)==0等等。我不介意使用静态分析器、IDE、正则表达式或为此目的设计的python库。当然,当x实际上是布尔值时,会出现一些误报;那很好 用例:我发现了由强制布尔值引起的bug。例如,变量x被假定为整数或None,并且被错误地测试为if not x,这意味着if x为None。我希望所有的布尔转换都是显式的(例如,如果x不是x,则将替换为,如

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


用例:我发现了由强制布尔值引起的bug。例如,变量
x
被假定为整数或
None
,并且被错误地测试为
if not x
,这意味着
if x为None
。我希望所有的布尔转换都是显式的(例如,如果x不是x,则将
替换为
,如果x是None,则将
替换为
,如果x==0,则将
替换为
,等等)。当然,这必须手动完成,但至少识别隐式转换发生的位置会有所帮助。

我的第一个想法是修饰内置的
bool
函数,但由于某些原因,这在Python 3.4中不起作用

因此,我提出了一个解决方案,当已知所有可能使用的类时:基本上装饰每个类的
\uu bool\uu
方法

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
是一个包含目标类的iterable。您可能可以动态地填充它

如果在启动时执行此代码,则每个布尔强制将打印
“强制为布尔值”

只是一个简短的测试:

>>> 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))
以下是输出:

Eli Bendersky写了一篇关于如何使用AST的文章,其中包括一些访问AST节点的示例代码。你可能想去一个你寻找特殊建筑的地方。在上面的示例中,您将查找
If
节点下的(子)表达式,其中操作数被直接视为布尔值,或者被视为
Not()
节点的唯一操作数

找到每一个可能的案例可能相当复杂。但是我认为你可以很容易地用一两页代码找到“简单”的情况(如果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)
导入ast
source=''第1行
x=1
y=2
如果不是x:
打印('不是x')
如果y为无:
打印('y为无')
当y或非x或(x<1且非y和x<10)时:
打印('x<10')
x+=1
'''
tree=ast.parse(源代码)
类FindNameAsBoolean(ast.NodeVisitor):
定义初始化(自身,行):
self.source\u行=行
def报告查找(自身、种类、位置、大小=3):
打印(“\n发现%s位于%s%”(种类、地点))
打印(自源线[locn[0]-1])
打印(''*locn[1],'^'*大小,sep='')
def visit_UnaryOp(自身,节点):
如果isinstance(node.op,ast.Not)和isinstance(node.operand,ast.Name):
self.report\u find('NOT-NAME',(node.lineno,node.col\u offset),size=4+len(node.operand.id))
self.generic_访问(节点)
def visit_BoolOp(自身,节点):
opname=type(node.op)。\uuuuu name\uuuuuu.upper()
对于node.values中的kid:
如果是实例(kid,ast.Name):
self.report\u find(“%s-NAME”%opname,(node.lineno,node.col\u offset),size=len(kid.id))
self.generic_访问(节点)
类查找测试(ast.NodeVisitor):
定义初始化(自身,行):
self.source\u行=行
def_fnab(自身,节点):
cond=node.test
FindNameAsBoolean(self.source_行)。访问(cond)
def访问_如果(自身、节点):
自组织(节点)
self.generic_访问(节点)
def访问时(自身、节点):
自组织(节点)
self.generic_访问(节点)
FindTests(source.splitlines())。访问(树)
以下是输出:

$python test.py
在(5,3)处找到非名称
如果不是x:
^^^^^
在(12,6)处找到OR-NAME
当y或非x或(x<1且非y和x<10)时:
^
在(12,11)处找到非姓名
当y或非x或(x<1且非y和x<10)时:
^^^^^
在(12,31)处找到非姓名
当y或非x或(x<1且非y和x<10)时:
^^^^^

事实上,有一个类型库正是这样做的。它适用于Python2和Python3

请参阅,使用命令
--strict boolean


我把公认的答案移到了这个问题上,尽管@AustinHastings有一个非常有用的答案,关于如何使用
ast
,因为我想让人们知道
mypy
——它是一个很棒的工具(不太可能很快被放弃,有100多个贡献者,包括Guido)。

我希望静态地这样做,不是动态的——主要是因为无法确保在测试期间每个强制都在执行路径上。此外,没有办法装饰内置类型的
\uuu bool\uuu
,这是大多数隐式转换发生的地方。@max我理解。好的,我将在这里给出我的答案,因为它可能有用,但这肯定不能解决您的问题……作为补充,您应该阅读并确定您想要分离的确切语句。您需要的可能是
test
。您知道
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):
                               ^^^^^