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