xref: /netbsd-src/external/apache2/llvm/dist/libcxx/utils/graph_header_deps.py (revision 4d6fc14bc9b0c5bf3e30be318c143ee82cadd108)
1*4d6fc14bSjoerg#!/usr/bin/env python
2*4d6fc14bSjoerg#===----------------------------------------------------------------------===##
3*4d6fc14bSjoerg#
4*4d6fc14bSjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5*4d6fc14bSjoerg# See https://llvm.org/LICENSE.txt for license information.
6*4d6fc14bSjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7*4d6fc14bSjoerg#
8*4d6fc14bSjoerg#===----------------------------------------------------------------------===##
9*4d6fc14bSjoerg
10*4d6fc14bSjoergimport argparse
11*4d6fc14bSjoergimport os
12*4d6fc14bSjoergimport re
13*4d6fc14bSjoergimport sys
14*4d6fc14bSjoerg
15*4d6fc14bSjoerg
16*4d6fc14bSjoergdef is_config_header(h):
17*4d6fc14bSjoerg    return os.path.basename(h) in ['__config', '__libcpp_version']
18*4d6fc14bSjoerg
19*4d6fc14bSjoerg
20*4d6fc14bSjoergdef is_experimental_header(h):
21*4d6fc14bSjoerg    return ('experimental/' in h) or ('ext/' in h)
22*4d6fc14bSjoerg
23*4d6fc14bSjoerg
24*4d6fc14bSjoergdef is_support_header(h):
25*4d6fc14bSjoerg    return '__support/' in h
26*4d6fc14bSjoerg
27*4d6fc14bSjoerg
28*4d6fc14bSjoergclass FileEntry:
29*4d6fc14bSjoerg    def __init__(self, includes, individual_linecount):
30*4d6fc14bSjoerg        self.includes = includes
31*4d6fc14bSjoerg        self.individual_linecount = individual_linecount
32*4d6fc14bSjoerg        self.cumulative_linecount = None  # documentation: this gets filled in later
33*4d6fc14bSjoerg        self.is_graph_root = None  # documentation: this gets filled in later
34*4d6fc14bSjoerg
35*4d6fc14bSjoerg
36*4d6fc14bSjoergdef list_all_roots_under(root):
37*4d6fc14bSjoerg    result = []
38*4d6fc14bSjoerg    for root, _, files in os.walk(root):
39*4d6fc14bSjoerg        for fname in files:
40*4d6fc14bSjoerg            if '__support' in root:
41*4d6fc14bSjoerg                pass
42*4d6fc14bSjoerg            elif ('.' in fname and not fname.endswith('.h')):
43*4d6fc14bSjoerg                pass
44*4d6fc14bSjoerg            else:
45*4d6fc14bSjoerg                result.append(root + '/' + fname)
46*4d6fc14bSjoerg    return result
47*4d6fc14bSjoerg
48*4d6fc14bSjoerg
49*4d6fc14bSjoergdef build_file_entry(fname, options):
50*4d6fc14bSjoerg    assert os.path.exists(fname)
51*4d6fc14bSjoerg
52*4d6fc14bSjoerg    def locate_header_file(h, paths):
53*4d6fc14bSjoerg        for p in paths:
54*4d6fc14bSjoerg            fullname = p + '/' + h
55*4d6fc14bSjoerg            if os.path.exists(fullname):
56*4d6fc14bSjoerg                return fullname
57*4d6fc14bSjoerg        if options.error_on_file_not_found:
58*4d6fc14bSjoerg            raise RuntimeError('Header not found: %s, included by %s' % (h, fname))
59*4d6fc14bSjoerg        return None
60*4d6fc14bSjoerg
61*4d6fc14bSjoerg    local_includes = []
62*4d6fc14bSjoerg    system_includes = []
63*4d6fc14bSjoerg    linecount = 0
64*4d6fc14bSjoerg    with open(fname, 'r', encoding='utf-8') as f:
65*4d6fc14bSjoerg        for line in f.readlines():
66*4d6fc14bSjoerg            linecount += 1
67*4d6fc14bSjoerg            m = re.match(r'\s*#\s*include\s+"([^"]*)"', line)
68*4d6fc14bSjoerg            if m is not None:
69*4d6fc14bSjoerg                local_includes.append(m.group(1))
70*4d6fc14bSjoerg            m = re.match(r'\s*#\s*include\s+<([^>]*)>', line)
71*4d6fc14bSjoerg            if m is not None:
72*4d6fc14bSjoerg                system_includes.append(m.group(1))
73*4d6fc14bSjoerg
74*4d6fc14bSjoerg    fully_qualified_includes = [
75*4d6fc14bSjoerg        locate_header_file(h, options.search_dirs)
76*4d6fc14bSjoerg        for h in system_includes
77*4d6fc14bSjoerg    ] + [
78*4d6fc14bSjoerg        locate_header_file(h, os.path.dirname(fname))
79*4d6fc14bSjoerg        for h in local_includes
80*4d6fc14bSjoerg    ]
81*4d6fc14bSjoerg
82*4d6fc14bSjoerg    return FileEntry(
83*4d6fc14bSjoerg        # If file-not-found wasn't an error, then skip non-found files
84*4d6fc14bSjoerg        includes = [h for h in fully_qualified_includes if h is not None],
85*4d6fc14bSjoerg        individual_linecount = linecount,
86*4d6fc14bSjoerg    )
87*4d6fc14bSjoerg
88*4d6fc14bSjoerg
89*4d6fc14bSjoergdef transitive_closure_of_includes(graph, h1):
90*4d6fc14bSjoerg    visited = set()
91*4d6fc14bSjoerg    def explore(graph, h1):
92*4d6fc14bSjoerg        if h1 not in visited:
93*4d6fc14bSjoerg            visited.add(h1)
94*4d6fc14bSjoerg            for h2 in graph[h1].includes:
95*4d6fc14bSjoerg                explore(graph, h2)
96*4d6fc14bSjoerg    explore(graph, h1)
97*4d6fc14bSjoerg    return visited
98*4d6fc14bSjoerg
99*4d6fc14bSjoerg
100*4d6fc14bSjoergdef transitively_includes(graph, h1, h2):
101*4d6fc14bSjoerg    return (h1 != h2) and (h2 in transitive_closure_of_includes(graph, h1))
102*4d6fc14bSjoerg
103*4d6fc14bSjoerg
104*4d6fc14bSjoergdef build_graph(roots, options):
105*4d6fc14bSjoerg    original_roots = list(roots)
106*4d6fc14bSjoerg    graph = {}
107*4d6fc14bSjoerg    while roots:
108*4d6fc14bSjoerg        frontier = roots
109*4d6fc14bSjoerg        roots = []
110*4d6fc14bSjoerg        for fname in frontier:
111*4d6fc14bSjoerg            if fname not in graph:
112*4d6fc14bSjoerg                graph[fname] = build_file_entry(fname, options)
113*4d6fc14bSjoerg                graph[fname].is_graph_root = (fname in original_roots)
114*4d6fc14bSjoerg                roots += graph[fname].includes
115*4d6fc14bSjoerg    for fname, entry in graph.items():
116*4d6fc14bSjoerg        entry.cumulative_linecount = sum(graph[h].individual_linecount for h in transitive_closure_of_includes(graph, fname))
117*4d6fc14bSjoerg    return graph
118*4d6fc14bSjoerg
119*4d6fc14bSjoerg
120*4d6fc14bSjoergdef get_friendly_id(fname):
121*4d6fc14bSjoerg    i = fname.index('include/')
122*4d6fc14bSjoerg    assert(i >= 0)
123*4d6fc14bSjoerg    result = fname[i+8:]
124*4d6fc14bSjoerg    return result
125*4d6fc14bSjoerg
126*4d6fc14bSjoerg
127*4d6fc14bSjoergdef get_graphviz(graph, options):
128*4d6fc14bSjoerg
129*4d6fc14bSjoerg    def get_decorators(fname, entry):
130*4d6fc14bSjoerg        result = ''
131*4d6fc14bSjoerg        if entry.is_graph_root:
132*4d6fc14bSjoerg            result += ' [style=bold]'
133*4d6fc14bSjoerg        if options.show_individual_line_counts and options.show_cumulative_line_counts:
134*4d6fc14bSjoerg            result += ' [label="%s\\n%d indiv, %d cumul"]' % (
135*4d6fc14bSjoerg                get_friendly_id(fname), entry.individual_linecount, entry.cumulative_linecount
136*4d6fc14bSjoerg            )
137*4d6fc14bSjoerg        elif options.show_individual_line_counts:
138*4d6fc14bSjoerg            result += ' [label="%s\\n%d indiv"]' % (get_friendly_id(fname), entry.individual_linecount)
139*4d6fc14bSjoerg        elif options.show_cumulative_line_counts:
140*4d6fc14bSjoerg            result += ' [label="%s\\n%d cumul"]' % (get_friendly_id(fname), entry.cumulative_linecount)
141*4d6fc14bSjoerg        return result
142*4d6fc14bSjoerg
143*4d6fc14bSjoerg    result = ''
144*4d6fc14bSjoerg    result += 'strict digraph {\n'
145*4d6fc14bSjoerg    result += '    rankdir=LR;\n'
146*4d6fc14bSjoerg    result += '    layout=dot;\n\n'
147*4d6fc14bSjoerg    for fname, entry in graph.items():
148*4d6fc14bSjoerg        result += '    "%s"%s;\n' % (get_friendly_id(fname), get_decorators(fname, entry))
149*4d6fc14bSjoerg        for h in entry.includes:
150*4d6fc14bSjoerg            if any(transitively_includes(graph, i, h) for i in entry.includes) and not options.show_transitive_edges:
151*4d6fc14bSjoerg                continue
152*4d6fc14bSjoerg            result += '        "%s" -> "%s";\n' % (get_friendly_id(fname), get_friendly_id(h))
153*4d6fc14bSjoerg    result += '}\n'
154*4d6fc14bSjoerg    return result
155*4d6fc14bSjoerg
156*4d6fc14bSjoerg
157*4d6fc14bSjoergif __name__ == '__main__':
158*4d6fc14bSjoerg    parser = argparse.ArgumentParser(description='Produce a dependency graph of libc++ headers, in GraphViz dot format.')
159*4d6fc14bSjoerg    parser.add_argument('--root', default=None, metavar='FILE', help='File or directory to be the root of the dependency graph')
160*4d6fc14bSjoerg    parser.add_argument('-I', dest='search_dirs', default=[], action='append', metavar='DIR', help='Path(s) to search for local includes')
161*4d6fc14bSjoerg    parser.add_argument('--show-transitive-edges', action='store_true', help='Show edges to headers that are transitively included anyway')
162*4d6fc14bSjoerg    parser.add_argument('--show-config-headers', action='store_true', help='Show headers named __config')
163*4d6fc14bSjoerg    parser.add_argument('--show-experimental-headers', action='store_true', help='Show headers in the experimental/ and ext/ directories')
164*4d6fc14bSjoerg    parser.add_argument('--show-support-headers', action='store_true', help='Show headers in the __support/ directory')
165*4d6fc14bSjoerg    parser.add_argument('--show-individual-line-counts', action='store_true', help='Include an individual line count in each node')
166*4d6fc14bSjoerg    parser.add_argument('--show-cumulative-line-counts', action='store_true', help='Include a total line count in each node')
167*4d6fc14bSjoerg    parser.add_argument('--error-on-file-not-found', action='store_true', help="Don't ignore failure to open an #included file")
168*4d6fc14bSjoerg
169*4d6fc14bSjoerg    options = parser.parse_args()
170*4d6fc14bSjoerg
171*4d6fc14bSjoerg    if options.root is None:
172*4d6fc14bSjoerg        curr_dir = os.path.dirname(os.path.abspath(__file__))
173*4d6fc14bSjoerg        options.root = os.path.join(curr_dir, '../include')
174*4d6fc14bSjoerg
175*4d6fc14bSjoerg    if options.search_dirs == [] and os.path.isdir(options.root):
176*4d6fc14bSjoerg        options.search_dirs = [options.root]
177*4d6fc14bSjoerg
178*4d6fc14bSjoerg    options.root = os.path.abspath(options.root)
179*4d6fc14bSjoerg    options.search_dirs = [os.path.abspath(p) for p in options.search_dirs]
180*4d6fc14bSjoerg
181*4d6fc14bSjoerg    if os.path.isdir(options.root):
182*4d6fc14bSjoerg        roots = list_all_roots_under(options.root)
183*4d6fc14bSjoerg    elif os.path.isfile(options.root):
184*4d6fc14bSjoerg        roots = [options.root]
185*4d6fc14bSjoerg    else:
186*4d6fc14bSjoerg        raise RuntimeError('--root seems to be invalid')
187*4d6fc14bSjoerg
188*4d6fc14bSjoerg    graph = build_graph(roots, options)
189*4d6fc14bSjoerg
190*4d6fc14bSjoerg    # Eliminate certain kinds of "visual noise" headers, if asked for.
191*4d6fc14bSjoerg    def should_keep(fname):
192*4d6fc14bSjoerg        return all([
193*4d6fc14bSjoerg            options.show_config_headers or not is_config_header(fname),
194*4d6fc14bSjoerg            options.show_experimental_headers or not is_experimental_header(fname),
195*4d6fc14bSjoerg            options.show_support_headers or not is_support_header(fname),
196*4d6fc14bSjoerg        ])
197*4d6fc14bSjoerg
198*4d6fc14bSjoerg    for fname in list(graph.keys()):
199*4d6fc14bSjoerg        if should_keep(fname):
200*4d6fc14bSjoerg            graph[fname].includes = [h for h in graph[fname].includes if should_keep(h)]
201*4d6fc14bSjoerg        else:
202*4d6fc14bSjoerg            del graph[fname]
203*4d6fc14bSjoerg
204*4d6fc14bSjoerg    # Look for cycles.
205*4d6fc14bSjoerg    no_cycles_detected = True
206*4d6fc14bSjoerg    for fname, entry in graph.items():
207*4d6fc14bSjoerg        for h in entry.includes:
208*4d6fc14bSjoerg            if transitively_includes(graph, h, fname):
209*4d6fc14bSjoerg                sys.stderr.write('Cycle detected between %s and %s\n' % (
210*4d6fc14bSjoerg                    get_friendly_id(fname), get_friendly_id(h)
211*4d6fc14bSjoerg                ))
212*4d6fc14bSjoerg                no_cycles_detected = False
213*4d6fc14bSjoerg    assert no_cycles_detected
214*4d6fc14bSjoerg
215*4d6fc14bSjoerg    print(get_graphviz(graph, options))
216