/ contrib / devtools / circular-dependencies.py
circular-dependencies.py
 1  #!/usr/bin/env python3
 2  # Copyright (c) 2018-present The Bitcoin Core developers
 3  # Distributed under the MIT software license, see the accompanying
 4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
 5  
 6  import sys
 7  import re
 8  
 9  # Directories with header-based modules, where the assumption that .cpp files
10  # define functions and variables declared in corresponding .h files is
11  # incorrect.
12  HEADER_MODULE_PATHS = [
13      'interfaces/'
14  ]
15  
16  def module_name(path):
17      if any(path.startswith(dirpath) for dirpath in HEADER_MODULE_PATHS):
18          return path
19      if path.endswith(".h"):
20          return path[:-2]
21      if path.endswith(".c"):
22          return path[:-2]
23      if path.endswith(".cpp"):
24          return path[:-4]
25      return None
26  
27  files = dict()
28  deps: dict[str, set[str]] = dict()
29  
30  RE = re.compile("^#include <(.*)>")
31  
32  # Iterate over files, and create list of modules
33  for arg in sys.argv[1:]:
34      module = module_name(arg)
35      if module is None:
36          print("Ignoring file %s (does not constitute module)\n" % arg)
37      else:
38          files[arg] = module
39          deps[module] = set()
40  
41  # Iterate again, and build list of direct dependencies for each module
42  # TODO: implement support for multiple include directories
43  for arg in sorted(files.keys()):
44      module = files[arg]
45      with open(arg, 'r') as f:
46          for line in f:
47              match = RE.match(line)
48              if match:
49                  include = match.group(1)
50                  included_module = module_name(include)
51                  if included_module is not None and included_module in deps and included_module != module:
52                      deps[module].add(included_module)
53  
54  # Loop to find the shortest (remaining) circular dependency
55  have_cycle: bool = False
56  while True:
57      shortest_cycle = None
58      for module in sorted(deps.keys()):
59          # Build the transitive closure of dependencies of module
60          closure: dict[str, list[str]] = dict()
61          for dep in deps[module]:
62              closure[dep] = []
63          while True:
64              old_size = len(closure)
65              old_closure_keys = sorted(closure.keys())
66              for src in old_closure_keys:
67                  for dep in deps[src]:
68                      if dep not in closure:
69                          closure[dep] = closure[src] + [src]
70              if len(closure) == old_size:
71                  break
72          # If module is in its own transitive closure, it's a circular dependency; check if it is the shortest
73          if module in closure and (shortest_cycle is None or len(closure[module]) + 1 < len(shortest_cycle)):
74              shortest_cycle = [module] + closure[module]
75      if shortest_cycle is None:
76          break
77      # We have the shortest circular dependency; report it
78      module = shortest_cycle[0]
79      print("Circular dependency: %s" % (" -> ".join(shortest_cycle + [module])))
80      # And then break the dependency to avoid repeating in other cycles
81      deps[shortest_cycle[-1]] = deps[shortest_cycle[-1]] - set([module])
82      have_cycle = True
83  
84  sys.exit(1 if have_cycle else 0)