Skip to content

Commit d9dacc2

Browse files
committed
Consolidate exit detection and add tests
Move the bit of exit detection logic that was in _AnnotateNode to _MarkExitNodes(). Even if it's a bit less efficient, it's easier to follow. Also, fix comments in _MarkExitNodes() and add a unit test.
1 parent 596704a commit d9dacc2

File tree

2 files changed

+33
-10
lines changed

2 files changed

+33
-10
lines changed

callgraph/core.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,22 @@ def GetOrCreateNode(self, name):
8383
return node
8484

8585
def _MarkExitNodes(self):
86-
# Algorithm:
87-
# visit all nodes via goto or nested connections, starting from __begin__
88-
# mark the node as exit nodes if it contains
86+
# A node is an exit node if:
87+
# 1. it contains an "exit" command with no target
88+
# or
89+
# 2. it's reached from the starting node via "goto" or "nested" connections
90+
# and it contains an exit command or a "goto eof" command.
91+
92+
# Identify all nodes with an exit command with no targets.
93+
for node in self.nodes.values():
94+
all_commands = set(itertools.chain.from_iterable(line.commands for line in node.code))
95+
exit_cmd = Command("exit", "")
96+
if exit_cmd in all_commands:
97+
node.is_exit_node = True
98+
99+
# Visit the call graph to find nodes satisfying condition #2.
89100
q = [self.first_node]
90-
visited = set()
101+
visited = set() # Used to avoid loops, since the call graph is not acyclic.
91102

92103
while q:
93104
cur = q.pop()
@@ -99,7 +110,7 @@ def _MarkExitNodes(self):
99110
else:
100111
all_commands = itertools.chain.from_iterable(line.commands for line in cur.code)
101112
for command in all_commands:
102-
if command[0] == "exit" and command[1] == "" or command[0] == "goto" and command[1] == "eof":
113+
if command[0] == "exit" or (command[0] == "goto" and command[1] == "eof"):
103114
cur.is_exit_node = True
104115
break
105116

@@ -166,11 +177,6 @@ def _AnnotateNode(self, node):
166177

167178
if command == "exit" and target == "":
168179
line.terminating = True
169-
170-
# Note that exit node detection is implemented in _MarkExitNodes, but that algorithm only
171-
# follows "nested" and "goto" nodes, therefore we need to mark nodes that contain naked
172-
# "exit" statements here, since they are terminating even when reached via "call" statements.
173-
node.is_exit_node = True
174180

175181
@staticmethod
176182
def Build(input_file, log_file=sys.stderr):

tests/test_core.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,23 @@ def test_call_nested_eof(self):
284284
self.assertTrue(eof.is_exit_node)
285285
self.assertTrue(eof.is_last_node)
286286

287+
def test_multiple_exit_nodes(self):
288+
code = """
289+
exit
290+
:foo
291+
exit
292+
""".split("\n")
293+
call_graph = CallGraph.Build(code, self.devnull)
294+
self.assertEqual(2, len(call_graph.nodes))
295+
296+
begin = call_graph.nodes["__begin__"]
297+
self.assertTrue(begin.is_exit_node)
298+
self.assertFalse(begin.is_last_node)
299+
300+
foo = call_graph.nodes["foo"]
301+
self.assertTrue(foo.is_exit_node)
302+
self.assertTrue(foo.is_last_node)
303+
287304

288305
if __name__ == "__main__":
289306
unittest.main()

0 commit comments

Comments
 (0)