/ 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/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()