/ test / lint / lint-includes.py
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()