lint-includes.py raw
1 #!/usr/bin/env python3
2 #
3 # Copyright (c) 2018-present The Bitcoin Core developers
4 # Distributed under the MIT software license, see the accompanying
5 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
6 #
7 # Check for duplicate includes.
8 # Guard against accidental introduction of new Boost dependencies.
9 # Check includes: Check for duplicate includes. Enforce bracket syntax includes.
10
11 import os
12 import re
13 import sys
14
15 from subprocess import check_output, CalledProcessError
16
17 from lint_ignore_dirs import SHARED_EXCLUDED_SUBTREES
18
19
20 EXCLUDED_DIRS = ["contrib/devtools/bitcoin-tidy/",
21 ] + SHARED_EXCLUDED_SUBTREES
22
23 EXPECTED_BOOST_INCLUDES = [
24 "boost/multi_index/detail/hash_index_iterator.hpp",
25 "boost/multi_index/hashed_index.hpp",
26 "boost/multi_index/identity.hpp",
27 "boost/multi_index/indexed_by.hpp",
28 "boost/multi_index/ordered_index.hpp",
29 "boost/multi_index/sequenced_index.hpp",
30 "boost/multi_index/tag.hpp",
31 "boost/multi_index_container.hpp",
32 "boost/operators.hpp",
33 "boost/test/included/unit_test.hpp",
34 "boost/test/unit_test.hpp",
35 "boost/tuple/tuple.hpp",
36 ]
37
38
39 def get_toplevel():
40 return check_output(["git", "rev-parse", "--show-toplevel"], text=True).rstrip("\n")
41
42
43 def list_files_by_suffix(suffixes):
44 exclude_args = [":(exclude)" + dir for dir in EXCLUDED_DIRS]
45
46 files_list = check_output(["git", "ls-files", "src"] + exclude_args, text=True).splitlines()
47
48 return [file for file in files_list if file.endswith(suffixes)]
49
50
51 def find_duplicate_includes(include_list):
52 tempset = set()
53 duplicates = set()
54
55 for inclusion in include_list:
56 if inclusion in tempset:
57 duplicates.add(inclusion)
58 else:
59 tempset.add(inclusion)
60
61 return duplicates
62
63
64 def find_included_cpps():
65 included_cpps = list()
66
67 try:
68 included_cpps = check_output(["git", "grep", "-E", r"^#include [<\"][^>\"]+\.cpp[>\"]", "--", "*.cpp", "*.h"], text=True).splitlines()
69 except CalledProcessError as e:
70 if e.returncode > 1:
71 raise e
72
73 return included_cpps
74
75
76 def find_extra_boosts():
77 included_boosts = list()
78 filtered_included_boost_set = set()
79 exclusion_set = set()
80
81 try:
82 included_boosts = check_output(["git", "grep", "-E", r"^#include <boost/", "--", "*.cpp", "*.h"], text=True).splitlines()
83 except CalledProcessError as e:
84 if e.returncode > 1:
85 raise e
86
87 for boost in included_boosts:
88 filtered_included_boost_set.add(re.findall(r'(?<=\<).+?(?=\>)', boost)[0])
89
90 for expected_boost in EXPECTED_BOOST_INCLUDES:
91 for boost in filtered_included_boost_set:
92 if expected_boost in boost:
93 exclusion_set.add(boost)
94
95 extra_boosts = set(filtered_included_boost_set.difference(exclusion_set))
96
97 return extra_boosts
98
99
100 def find_quote_syntax_inclusions():
101 exclude_args = [":(exclude)" + dir for dir in EXCLUDED_DIRS]
102 quote_syntax_inclusions = list()
103
104 try:
105 quote_syntax_inclusions = check_output(["git", "grep", r"^#include \"", "--", "*.cpp", "*.h"] + exclude_args, text=True).splitlines()
106 except CalledProcessError as e:
107 if e.returncode > 1:
108 raise e
109
110 return quote_syntax_inclusions
111
112
113 def main():
114 exit_code = 0
115
116 os.chdir(get_toplevel())
117
118 # Check for duplicate includes
119 for filename in list_files_by_suffix((".cpp", ".h")):
120 with open(filename, "r") as file:
121 include_list = [line.rstrip("\n") for line in file if re.match(r"^#include", line)]
122
123 duplicates = find_duplicate_includes(include_list)
124
125 if duplicates:
126 print(f"Duplicate include(s) in {filename}:")
127 for duplicate in duplicates:
128 print(duplicate)
129 print("")
130 exit_code = 1
131
132 # Check if code includes .cpp-files
133 included_cpps = find_included_cpps()
134
135 if included_cpps:
136 print("The following files #include .cpp files:")
137 for included_cpp in included_cpps:
138 print(included_cpp)
139 print("")
140 exit_code = 1
141
142 # Guard against accidental introduction of new Boost dependencies
143 extra_boosts = find_extra_boosts()
144
145 if extra_boosts:
146 for boost in extra_boosts:
147 print(f"A new Boost dependency in the form of \"{boost}\" appears to have been introduced:")
148 print(check_output(["git", "grep", boost, "--", "*.cpp", "*.h"], text=True))
149 exit_code = 1
150
151 # Check if Boost dependencies are no longer used
152 for expected_boost in EXPECTED_BOOST_INCLUDES:
153 try:
154 check_output(["git", "grep", "-q", r"^#include <%s>" % expected_boost, "--", "*.cpp", "*.h"], text=True)
155 except CalledProcessError as e:
156 if e.returncode > 1:
157 raise e
158 else:
159 print(f"Good job! The Boost dependency \"{expected_boost}\" is no longer used. "
160 "Please remove it from EXPECTED_BOOST_INCLUDES in test/lint/lint-includes.py "
161 "to make sure this dependency is not accidentally reintroduced.\n")
162 exit_code = 1
163
164 # Enforce bracket syntax includes
165 quote_syntax_inclusions = find_quote_syntax_inclusions()
166 # *Rationale*: Bracket syntax is less ambiguous because the preprocessor
167 # searches a fixed list of include directories without taking location of the
168 # source file into account. This allows quoted includes to stand out more when
169 # the location of the source file actually is relevant.
170
171 if quote_syntax_inclusions:
172 print("Please use bracket syntax includes (\"#include <foo.h>\") instead of quote syntax includes:")
173 for quote_syntax_inclusion in quote_syntax_inclusions:
174 print(quote_syntax_inclusion)
175 exit_code = 1
176
177 sys.exit(exit_code)
178
179
180 if __name__ == "__main__":
181 main()
182