Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion rope/base/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ def parse(source, filename="<string>", *args, **kwargs): # type: ignore


def call_for_nodes(node, callback):
"""If callback returns `True` the child nodes are skipped"""
"""
Pre-order depth-first traversal of AST nodes, calling `callback(node)` for
each node visited.

When each node is visited, `callback(node)` will be called with the visited
`node`, then its children node will be visited.

If `callback(node)` returns `True` for a node, then the descendants of that
node will not be visited.

See _ResultChecker._find_node for an example.
"""
result = callback(node)
if not result:
for child in ast.iter_child_nodes(node):
Expand Down
2 changes: 2 additions & 0 deletions rope/refactor/similarfinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ def __init__(self, body, pattern, does_match):
def find_matches(self):
if self.matches is None:
self.matches = []
# _check_nodes always returns None, so
# call_for_nodes traverses self.body's entire tree.
ast.call_for_nodes(self.body, self._check_node)
return self.matches

Expand Down
26 changes: 26 additions & 0 deletions ropetest/refactor/patchedasttest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,32 @@ def check_region(self, text, start, end):
self.test_case.assertEqual((start, end), node.region)

def _find_node(self, text):
"""
Find the node in `self.ast` whose type is named in `text`.

:param text: ast node name

Generally, the test should only have a single matching node, as it make
the test much harder to understand when there may be multiple matches.

If `self.ast` contains more than one nodes that matches `text`, then
the **outer-most last match** takes precedence.

For example, given that we are looking for `ast.Call` node:

checker._find_node("Call")

and given that `self.ast` is the AST for this code:

func_a(1, func_b(2, 3)) + func_c(4, func_d(5, 6))

the outer-most last match would be the ast node representing this bit:

func_c(4, func_d(5, 6))

Note that the order of traversal is based on the order of ast nodes,
which usually, but not always, match textual order.
"""
goal = text
if not isinstance(text, (tuple, list)):
goal = [text]
Expand Down