Skip to content

Commit

Permalink
Fix line range using Python 3.8 end_lineno (#821)
Browse files Browse the repository at this point in the history
Python 3.8 and above have new ast node attributes to identify the
end line number and end column offset [1].

Python 3.8 also fixes line numbers for multiline strings [2].

This fixes the issue mentioned in #820, but only for Python 3.8+.

[1] https://docs.python.org/3.8/library/ast.html#ast.AST.end_lineno
[2] https://bugs.python.org/issue31241

Signed-off-by: Eric Brown <browne@vmware.com>
  • Loading branch information
ericwb committed Mar 6, 2022
1 parent 29bc186 commit 1c0fc80
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 60 deletions.
10 changes: 3 additions & 7 deletions bandit/core/node_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,7 @@ def visit_Str(self, node):
"""
self.context["str"] = node.s
if not isinstance(node._bandit_parent, ast.Expr): # docstring
self.context["linerange"] = b_utils.linerange_fix(
node._bandit_parent
)
self.context["linerange"] = b_utils.linerange(node._bandit_parent)
self.update_scores(self.tester.run_tests(self.context, "Str"))

def visit_Bytes(self, node):
Expand All @@ -187,9 +185,7 @@ def visit_Bytes(self, node):
"""
self.context["bytes"] = node.s
if not isinstance(node._bandit_parent, ast.Expr): # docstring
self.context["linerange"] = b_utils.linerange_fix(
node._bandit_parent
)
self.context["linerange"] = b_utils.linerange(node._bandit_parent)
self.update_scores(self.tester.run_tests(self.context, "Bytes"))

def pre_visit(self, node):
Expand All @@ -215,7 +211,7 @@ def pre_visit(self, node):
self.context["col_offset"] = node.col_offset

self.context["node"] = node
self.context["linerange"] = b_utils.linerange_fix(node)
self.context["linerange"] = b_utils.linerange(node)
self.context["filename"] = self.fname
self.context["file_data"] = self.fdata

Expand Down
100 changes: 50 additions & 50 deletions bandit/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,56 +220,56 @@ def calc_linerange(node):

def linerange(node):
"""Get line number range from a node."""
if hasattr(node, "_bandit_linerange_stripped"):
lines_minmax = node._bandit_linerange_stripped
return list(range(lines_minmax[0], lines_minmax[1] + 1))

strip = {
"body": None,
"orelse": None,
"handlers": None,
"finalbody": None,
}
for key in strip.keys():
if hasattr(node, key):
strip[key] = getattr(node, key)
setattr(node, key, [])

lines_min = 9999999999
lines_max = -1
if hasattr(node, "lineno"):
lines_min = node.lineno
lines_max = node.lineno
for n in ast.iter_child_nodes(node):
lines_minmax = calc_linerange(n)
lines_min = min(lines_min, lines_minmax[0])
lines_max = max(lines_max, lines_minmax[1])

for key in strip.keys():
if strip[key] is not None:
setattr(node, key, strip[key])

if lines_max == -1:
lines_min = 0
lines_max = 1

node._bandit_linerange_stripped = (lines_min, lines_max)

return list(range(lines_min, lines_max + 1))


def linerange_fix(node):
"""Try and work around a known Python bug with multi-line strings."""
# deal with multiline strings lineno behavior (Python issue #16806)
lines = linerange(node)
if hasattr(node, "_bandit_sibling") and hasattr(
node._bandit_sibling, "lineno"
):
start = min(lines)
delta = node._bandit_sibling.lineno - start
if delta > 1:
return list(range(start, node._bandit_sibling.lineno))
return lines
if sys.version_info >= (3, 8) and hasattr(node, "lineno"):
return list(range(node.lineno, node.end_lineno + 1))
else:
if hasattr(node, "_bandit_linerange_stripped"):
lines_minmax = node._bandit_linerange_stripped
return list(range(lines_minmax[0], lines_minmax[1] + 1))

strip = {
"body": None,
"orelse": None,
"handlers": None,
"finalbody": None,
}
for key in strip.keys():
if hasattr(node, key):
strip[key] = getattr(node, key)
setattr(node, key, [])

lines_min = 9999999999
lines_max = -1
if hasattr(node, "lineno"):
lines_min = node.lineno
lines_max = node.lineno
for n in ast.iter_child_nodes(node):
lines_minmax = calc_linerange(n)
lines_min = min(lines_min, lines_minmax[0])
lines_max = max(lines_max, lines_minmax[1])

for key in strip.keys():
if strip[key] is not None:
setattr(node, key, strip[key])

if lines_max == -1:
lines_min = 0
lines_max = 1

node._bandit_linerange_stripped = (lines_min, lines_max)

lines = list(range(lines_min, lines_max + 1))

"""Try and work around a known Python bug with multi-line strings."""
# deal with multiline strings lineno behavior (Python issue #16806)
if hasattr(node, "_bandit_sibling") and hasattr(
node._bandit_sibling, "lineno"
):
start = min(lines)
delta = node._bandit_sibling.lineno - start
if delta > 1:
return list(range(start, node._bandit_sibling.lineno))
return lines


def concat_string(node, stop=None):
Expand Down
7 changes: 7 additions & 0 deletions examples/multiline_statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@
"args",
shell=True,
universal_newlines=True)

subprocess.check_output(
"/some_command",
"args",
shell=True,
universal_newlines=True
)
14 changes: 11 additions & 3 deletions tests/functional/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,17 +738,25 @@ def test_multiline_code(self):
)

issues = self.b_mgr.get_issue_list()
self.assertEqual(2, len(issues))
self.assertEqual(3, len(issues))
self.assertTrue(
issues[0].fname.endswith("examples/multiline_statement.py")
)

self.assertEqual(1, issues[0].lineno)
self.assertEqual(list(range(1, 3)), issues[0].linerange)
if sys.version_info >= (3, 8):
self.assertEqual(list(range(1, 2)), issues[0].linerange)
else:
self.assertEqual(list(range(1, 3)), issues[0].linerange)
self.assertIn("subprocess", issues[0].get_code())
self.assertEqual(5, issues[1].lineno)
self.assertEqual(list(range(3, 6 + 1)), issues[1].linerange)
self.assertIn("shell=True", issues[1].get_code())
self.assertEqual(11, issues[2].lineno)
if sys.version_info >= (3, 8):
self.assertEqual(list(range(8, 13 + 1)), issues[2].linerange)
else:
self.assertEqual(list(range(8, 12 + 1)), issues[2].linerange)
self.assertIn("shell=True", issues[2].get_code())

def test_code_line_numbers(self):
self.run_example("binding.py")
Expand Down

0 comments on commit 1c0fc80

Please sign in to comment.