lint-includes.py raw

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