lint-includes.py
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/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).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).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).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).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).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") 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 exit_code = 1 134 135 # Check if code includes .cpp-files 136 included_cpps = find_included_cpps() 137 138 if included_cpps: 139 print("The following files #include .cpp files:") 140 for included_cpp in included_cpps: 141 print(included_cpp) 142 print("") 143 exit_code = 1 144 145 # Guard against accidental introduction of new Boost dependencies 146 extra_boosts = find_extra_boosts() 147 148 if extra_boosts: 149 for boost in extra_boosts: 150 print(f"A new Boost dependency in the form of \"{boost}\" appears to have been introduced:") 151 print(check_output(["git", "grep", boost, "--", "*.cpp", "*.h"], text=True)) 152 exit_code = 1 153 154 # Check if Boost dependencies are no longer used 155 for expected_boost in EXPECTED_BOOST_INCLUDES: 156 try: 157 check_output(["git", "grep", "-q", r"^#include <%s>" % expected_boost, "--", "*.cpp", "*.h"], text=True) 158 except CalledProcessError as e: 159 if e.returncode > 1: 160 raise e 161 else: 162 print(f"Good job! The Boost dependency \"{expected_boost}\" is no longer used. " 163 "Please remove it from EXPECTED_BOOST_INCLUDES in test/lint/lint-includes.py " 164 "to make sure this dependency is not accidentally reintroduced.\n") 165 exit_code = 1 166 167 # Enforce bracket syntax includes 168 quote_syntax_inclusions = find_quote_syntax_inclusions() 169 # *Rationale*: Bracket syntax is less ambiguous because the preprocessor 170 # searches a fixed list of include directories without taking location of the 171 # source file into account. This allows quoted includes to stand out more when 172 # the location of the source file actually is relevant. 173 174 if quote_syntax_inclusions: 175 print("Please use bracket syntax includes (\"#include <foo.h>\") instead of quote syntax includes:") 176 for quote_syntax_inclusion in quote_syntax_inclusions: 177 print(quote_syntax_inclusion) 178 exit_code = 1 179 180 sys.exit(exit_code) 181 182 183 if __name__ == "__main__": 184 main()