toxcore/other/analysis/check_recursion
iphydf 1859d0f44a
cleanup: Ensure we limit the system headers included in .h files.
Most system headers contain functions (e.g. `memcpy` in `string.h`)
which aren't needed in our own header files. For the most part, our own
headers should only include types needed to declare our own types and
functions. We now enforce this so we think twice about which headers we
really need in the .h files.
2022-02-04 20:54:37 +00:00

116 lines
3.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Tool to check for recursive calls in toxcore C code.
Usage:
cat toxav/*.c toxcore/*.c toxencryptsave/*.c \
| clang `pkg-config --cflags libsodium opus vpx` \
-Itoxav -Itoxcore -Itoxencryptsave -S -emit-llvm -xc - -o- \
| opt -analyze -print-callgraph 2>&1 \
| other/analysis/check_recursion
"""
import collections
import fileinput
import re
import sys
import time
from typing import Dict
from typing import List
from typing import Set
def load_callgraph() -> Dict[str, List[str]]:
"""
Parses the output from opt -print-callgraph from stdin or argv.
Returns graph as dict[str, list[str]] containing nodes with their outgoing
edges.
"""
graph: Dict[str, Set[str]] = collections.defaultdict(set)
cur = None
for line in fileinput.input():
found = re.search("Call graph node for function: '(.*)'", line)
if found:
cur = found.group(1)
if cur:
found = re.search("calls function '(.*)'", line)
if found:
graph[cur].add(found.group(1))
return {k: sorted(v) for k, v in graph.items()}
def walk(
visited: Set[str],
callgraph: Dict[str, List[str]],
cycles: Set[str],
stack: List[str],
cur: str,
) -> None:
"""
Detects cycles in the callgraph and adds them to the cycles parameter.
"""
if cur in visited:
return
stack.append(cur)
for callee in callgraph.get(cur, ()):
try:
cycles.add(" -> ".join(stack[stack.index(callee):] + [callee]))
except ValueError:
walk(visited, callgraph, cycles, stack, callee)
visited.add(callee)
stack.pop()
def get_time() -> int:
"""
Return the current time in milliseconds.
"""
return int(round(time.time() * 1000))
def find_recursion(expected: Set[str]) -> None:
"""
Main function: detects cycles and prints them.
Takes a set of expected cycles. If any of the expected cycles was not found,
or any unexpected cycle was found, the program exits with an error.
"""
start = prev = get_time()
print("[+0000=0000] Generating callgraph")
callgraph = load_callgraph()
now = get_time()
print("[+%04d=%04d] Finding recursion" % (now - prev, now - start))
prev = now
cycles: Set[str] = set()
visited: Set[str] = set()
for func in sorted(callgraph.keys()):
walk(visited, callgraph, cycles, [], func)
now = get_time()
if cycles:
print("[+%04d=%04d] Recursion detected:" % (now - prev, now - start))
for cycle in sorted(cycles):
if cycle in expected:
print(" - " + cycle + " (expected)")
expected.remove(cycle)
cycles.remove(cycle)
else:
print(" - " + cycle)
else:
print("[+%04d=%04d] No recursion detected" % (now - prev, now - start))
if expected:
print("Expected recursion no longer present: " + str(list(expected)))
if expected or cycles:
sys.exit(1)
find_recursion(expected={
"add_to_closest -> add_to_closest",
"add_to_list -> add_to_list",
})