1#!/usr/bin/env python3
2
3# Copyright 2021 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import collections
18import os
19
20
21def to_inc(filename):
22    """Given filename, synthesize what should go in an include statement to get that file"""
23    if filename.startswith("include/"):
24        return '<%s>' % filename[len("include/"):]
25    return '"%s"' % filename
26
27
28def set_pragma(filename, pragma):
29    """Set the file-level IWYU pragma in filename"""
30    lines = []
31    saw_first_define = False
32    for line in open(filename).read().splitlines():
33        if line.startswith('// IWYU pragma: '):
34            continue
35        lines.append(line)
36        if not saw_first_define and line.startswith('#define '):
37            saw_first_define = True
38            lines.append('')
39            lines.append('// IWYU pragma: %s' % pragma)
40            lines.append('')
41    open(filename, 'w').write('\n'.join(lines) + '\n')
42
43
44def set_exports(pub, cg):
45    """In file pub, mark the include for cg with IWYU pragma: export"""
46    lines = []
47    for line in open(pub).read().splitlines():
48        if line.startswith('#include %s' % to_inc(cg)):
49            lines.append('#include %s  // IWYU pragma: export' % to_inc(cg))
50        else:
51            lines.append(line)
52    open(pub, 'w').write('\n'.join(lines) + '\n')
53
54
55CG_ROOTS_GRPC = (
56    (r'sync', 'grpc/support/sync.h'),
57    (r'atm', 'grpc/support/atm.h'),
58)
59
60CG_ROOTS_GRPCPP = []
61
62
63def fix_tree(tree, cg_roots):
64    """Fix one include tree"""
65    # Map of filename --> paths including that filename
66    reverse_map = collections.defaultdict(list)
67    # The same, but for things with '/impl/codegen' in their names
68    cg_reverse_map = collections.defaultdict(list)
69    for root, dirs, files in os.walk(tree):
70        root_map = cg_reverse_map if '/impl/codegen' in root else reverse_map
71        for filename in files:
72            root_map[filename].append(root)
73    # For each thing in '/impl/codegen' figure out what exports it
74    for filename, paths in cg_reverse_map.items():
75        # Exclude non-headers
76        if not filename.endswith('.h'):
77            continue
78        pragma = None
79        # Check for our 'special' headers: if we see one of these, we just
80        # hardcode where they go to because there's some complicated rules.
81        for root, target in cg_roots:
82            if filename.startswith(root):
83                pragma = 'private, include <%s>' % target
84                if len(paths) == 1:
85                    path = paths[0]
86                    if filename.startswith(root + '.'):
87                        set_exports('include/' + target, path + '/' + filename)
88                    if filename.startswith(root + '_'):
89                        set_exports(path + '/' + root + '.h',
90                                    path + '/' + filename)
91        # If the path for a file in /impl/codegen is ambiguous, just don't bother
92        if not pragma and len(paths) == 1:
93            path = paths[0]
94            # Check if we have an exporting candidate
95            if filename in reverse_map:
96                proper = reverse_map[filename]
97                # And that it too is unambiguous
98                if len(proper) == 1:
99                    # Build the two relevant pathnames
100                    cg = path + '/' + filename
101                    pub = proper[0] + '/' + filename
102                    # And see if the public file actually includes the /impl/codegen file
103                    if ('#include %s' % to_inc(cg)) in open(pub).read():
104                        # Finally, if it does, we'll set that pragma
105                        pragma = 'private, include %s' % to_inc(pub)
106                        # And mark the export
107                        set_exports(pub, cg)
108        # If we can't find a good alternative include to point people to,
109        # mark things private anyway... we don't want to recommend people include
110        # from impl/codegen
111        if not pragma:
112            pragma = 'private'
113        for path in paths:
114            set_pragma(path + '/' + filename, pragma)
115
116
117fix_tree('include/grpc', CG_ROOTS_GRPC)
118fix_tree('include/grpcpp', CG_ROOTS_GRPCPP)
119