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