在源代码中查找对布尔值的隐式强制转换
Finding implicit coercion to boolean in source code
如何在源代码中找到所有对布尔值的隐式转换?这包括条件语句 if x
、循环 while x
、运算符 x or y
等;但不是 if x == 0
或 if len(x) == 0
等。我不介意使用静态分析器,或 IDE,或正则表达式,或为此目的设计的 python 库.当然会有一些误报,当 x
实际上是布尔值时;没关系。
用例:我发现了由强制转换为布尔值引起的错误。例如,一个变量 x
应该是一个整数或 None
并且被错误地测试为 if not x
暗示 if x is None
。我想使所有布尔转换显式(例如,将 if not x
替换为 if x is None
或 if 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)。
如何在源代码中找到所有对布尔值的隐式转换?这包括条件语句 if x
、循环 while x
、运算符 x or y
等;但不是 if x == 0
或 if len(x) == 0
等。我不介意使用静态分析器,或 IDE,或正则表达式,或为此目的设计的 python 库.当然会有一些误报,当 x
实际上是布尔值时;没关系。
用例:我发现了由强制转换为布尔值引起的错误。例如,一个变量 x
应该是一个整数或 None
并且被错误地测试为 if not x
暗示 if x is None
。我想使所有布尔转换显式(例如,将 if not x
替换为 if x is None
或 if 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)。