lint-includes.py raw
1 #!/usr/bin/env python3
2 #
3 # Copyright (c) 2018-2022 The Limenka 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/limenka-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/signals2/connection.hpp",
34 "boost/signals2/optional_last_value.hpp",
35 "boost/signals2/signal.hpp",
36 "boost/test/included/unit_test.hpp",
37 "boost/test/unit_test.hpp",
38 "boost/tuple/tuple.hpp",
39 ]
40
41
42 def get_toplevel():
43 return check_output(["git", "rev-parse", "--show-toplevel"], text=True, encoding="utf8").rstrip("\n")
44
45
46 def list_files_by_suffix(suffixes):
47 exclude_args = [":(exclude)" + dir for dir in EXCLUDED_DIRS]
48
49 files_list = check_output(["git", "ls-files", "src"] + exclude_args, text=True, encoding="utf8").splitlines()
50
51 return [file for file in files_list if file.endswith(suffixes)]
52
53
54 def find_duplicate_includes(include_list):
55 tempset = set()
56 duplicates = set()
57
58 for inclusion in include_list:
59 if inclusion in tempset:
60 duplicates.add(inclusion)
61 else:
62 tempset.add(inclusion)
63
64 return duplicates
65
66
67 def find_included_cpps():
68 included_cpps = list()
69
70 try:
71 included_cpps = check_output(["git", "grep", "-E", r"^#include [<\"][^>\"]+\.cpp[>\"]", "--", "*.cpp", "*.h"], text=True, encoding="utf8").splitlines()
72 except CalledProcessError as e:
73 if e.returncode > 1:
74 raise e
75
76 return included_cpps
77
78
79 def find_extra_boosts():
80 included_boosts = list()
81 filtered_included_boost_set = set()
82 exclusion_set = set()
83
84 try:
85 included_boosts = check_output(["git", "grep", "-E", r"^#include <boost/", "--", "*.cpp", "*.h"], text=True, encoding="utf8").splitlines()
86 except CalledProcessError as e:
87 if e.returncode > 1:
88 raise e
89
90 for boost in included_boosts:
91 filtered_included_boost_set.add(re.findall(r'(?<=\<).+?(?=\>)', boost)[0])
92
93 for expected_boost in EXPECTED_BOOST_INCLUDES:
94 for boost in filtered_included_boost_set:
95 if expected_boost in boost:
96 exclusion_set.add(boost)
97
98 extra_boosts = set(filtered_included_boost_set.difference(exclusion_set))
99
100 return extra_boosts
101
102
103 def find_quote_syntax_inclusions():
104 exclude_args = [":(exclude)" + dir for dir in EXCLUDED_DIRS]
105 quote_syntax_inclusions = list()
106
107 try:
108 quote_syntax_inclusions = check_output(["git", "grep", r"^#include \"", "--", "*.cpp", "*.h"] + exclude_args, text=True, encoding="utf8").splitlines()
109 except CalledProcessError as e:
110 if e.returncode > 1:
111 raise e
112
113 return quote_syntax_inclusions
114
115
116 def main():
117 exit_code = 0
118
119 os.chdir(get_toplevel())
120
121 # Check for duplicate includes
122 for filename in list_files_by_suffix((".cpp", ".h")):
123 with open(filename, "r", encoding="utf8") as file:
124 include_list = [line.rstrip("\n") for line in file if re.match(r"^#include", line)]
125
126 duplicates = find_duplicate_includes(include_list)
127
128 if duplicates:
129 print(f"Duplicate include(s) in {filename}:")
130 for duplicate in duplicates:
131 print(duplicate)
132 print("")
133
134 # Check if code includes .cpp-files
135 included_cpps = find_included_cpps()
136
137 if included_cpps:
138 print("The following files #include .cpp files:")
139 for included_cpp in included_cpps:
140 print(included_cpp)
141 print("")
142 exit_code = 1
143
144 # Guard against accidental introduction of new Boost dependencies
145 extra_boosts = find_extra_boosts()
146
147 if extra_boosts:
148 for boost in extra_boosts:
149 print(f"A new Boost dependency in the form of \"{boost}\" appears to have been introduced:")
150 print(check_output(["git", "grep", boost, "--", "*.cpp", "*.h"], text=True, encoding="utf8"))
151 exit_code = 1
152
153 # Check if Boost dependencies are no longer used
154 for expected_boost in EXPECTED_BOOST_INCLUDES:
155 try:
156 check_output(["git", "grep", "-q", r"^#include <%s>" % expected_boost, "--", "*.cpp", "*.h"], text=True, encoding="utf8")
157 except CalledProcessError as e:
158 if e.returncode > 1:
159 raise e
160 else:
161 print(f"Good job! The Boost dependency \"{expected_boost}\" is no longer used. "
162 "Please remove it from EXPECTED_BOOST_INCLUDES in test/lint/lint-includes.py "
163 "to make sure this dependency is not accidentally reintroduced.\n")
164 exit_code = 1
165
166 # Enforce bracket syntax includes
167 quote_syntax_inclusions = find_quote_syntax_inclusions()
168
169 if quote_syntax_inclusions:
170 print("Please use bracket syntax includes (\"#include <foo.h>\") instead of quote syntax includes:")
171 for quote_syntax_inclusion in quote_syntax_inclusions:
172 print(quote_syntax_inclusion)
173 exit_code = 1
174
175 sys.exit(exit_code)
176
177
178 if __name__ == "__main__":
179 main()
180