Skip to content

Commit a7b295e

Browse files
committed
Add circular dependencies script
1 parent ffa86af commit a7b295e

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

contrib/devtools/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,14 @@ It will do the following automatically:
194194
- add missing translations to the build system (TODO)
195195

196196
See doc/translation-process.md for more information.
197+
198+
circular-dependencies.py
199+
========================
200+
201+
Run this script from the root of the source tree (`src/`) to find circular dependencies in the source code.
202+
This looks only at which files include other files, treating the `.cpp` and `.h` file as one unit.
203+
204+
Example usage:
205+
206+
cd .../src
207+
../contrib/devtools/circular-dependencies.py {*,*/*,*/*/*}.{h,cpp}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import re
5+
6+
MAPPING = {
7+
'core_read.cpp': 'core_io.cpp',
8+
'core_write.cpp': 'core_io.cpp',
9+
}
10+
11+
def module_name(path):
12+
if path in MAPPING:
13+
path = MAPPING[path]
14+
if path.endswith(".h"):
15+
return path[:-2]
16+
if path.endswith(".c"):
17+
return path[:-2]
18+
if path.endswith(".cpp"):
19+
return path[:-4]
20+
return None
21+
22+
files = dict()
23+
deps = dict()
24+
25+
RE = re.compile("^#include <(.*)>")
26+
27+
# Iterate over files, and create list of modules
28+
for arg in sys.argv[1:]:
29+
module = module_name(arg)
30+
if module is None:
31+
print("Ignoring file %s (does not constitute module)\n" % arg)
32+
else:
33+
files[arg] = module
34+
deps[module] = set()
35+
36+
# Iterate again, and build list of direct dependencies for each module
37+
# TODO: implement support for multiple include directories
38+
for arg in sorted(files.keys()):
39+
module = files[arg]
40+
with open(arg, 'r') as f:
41+
for line in f:
42+
match = RE.match(line)
43+
if match:
44+
include = match.group(1)
45+
included_module = module_name(include)
46+
if included_module is not None and included_module in deps and included_module != module:
47+
deps[module].add(included_module)
48+
49+
# Loop to find the shortest (remaining) circular dependency
50+
have_cycle = False
51+
while True:
52+
shortest_cycle = None
53+
for module in sorted(deps.keys()):
54+
# Build the transitive closure of dependencies of module
55+
closure = dict()
56+
for dep in deps[module]:
57+
closure[dep] = []
58+
while True:
59+
old_size = len(closure)
60+
old_closure_keys = sorted(closure.keys())
61+
for src in old_closure_keys:
62+
for dep in deps[src]:
63+
if dep not in closure:
64+
closure[dep] = closure[src] + [src]
65+
if len(closure) == old_size:
66+
break
67+
# If module is in its own transitive closure, it's a circular dependency; check if it is the shortest
68+
if module in closure and (shortest_cycle is None or len(closure[module]) + 1 < len(shortest_cycle)):
69+
shortest_cycle = [module] + closure[module]
70+
if shortest_cycle is None:
71+
break
72+
# We have the shortest circular dependency; report it
73+
module = shortest_cycle[0]
74+
print("Circular dependency: %s" % (" -> ".join(shortest_cycle + [module])))
75+
# And then break the dependency to avoid repeating in other cycles
76+
deps[shortest_cycle[-1]] = deps[shortest_cycle[-1]] - set([module])
77+
have_cycle = True
78+
79+
sys.exit(1 if have_cycle else 0)

0 commit comments

Comments
 (0)