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