lint-includes.py raw

   1  #!/usr/bin/env python3
   2  #
   3  # Copyright (c) 2018-2022 The Limenka 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/limenka-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, encoding="utf8").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, encoding="utf8").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, encoding="utf8").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, encoding="utf8").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, encoding="utf8").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", encoding="utf8") 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  
 134      # Check if code includes .cpp-files
 135      included_cpps = find_included_cpps()
 136  
 137      if included_cpps:
 138          print("The following files #include .cpp files:")
 139          for included_cpp in included_cpps:
 140              print(included_cpp)
 141          print("")
 142          exit_code = 1
 143  
 144      # Guard against accidental introduction of new Boost dependencies
 145      extra_boosts = find_extra_boosts()
 146  
 147      if extra_boosts:
 148          for boost in extra_boosts:
 149              print(f"A new Boost dependency in the form of \"{boost}\" appears to have been introduced:")
 150              print(check_output(["git", "grep", boost, "--", "*.cpp", "*.h"], text=True, encoding="utf8"))
 151          exit_code = 1
 152  
 153      # Check if Boost dependencies are no longer used
 154      for expected_boost in EXPECTED_BOOST_INCLUDES:
 155          try:
 156              check_output(["git", "grep", "-q", r"^#include <%s>" % expected_boost, "--", "*.cpp", "*.h"], text=True, encoding="utf8")
 157          except CalledProcessError as e:
 158              if e.returncode > 1:
 159                  raise e
 160              else:
 161                  print(f"Good job! The Boost dependency \"{expected_boost}\" is no longer used. "
 162                         "Please remove it from EXPECTED_BOOST_INCLUDES in test/lint/lint-includes.py "
 163                         "to make sure this dependency is not accidentally reintroduced.\n")
 164                  exit_code = 1
 165  
 166      # Enforce bracket syntax includes
 167      quote_syntax_inclusions = find_quote_syntax_inclusions()
 168  
 169      if quote_syntax_inclusions:
 170          print("Please use bracket syntax includes (\"#include <foo.h>\") instead of quote syntax includes:")
 171          for quote_syntax_inclusion in quote_syntax_inclusions:
 172              print(quote_syntax_inclusion)
 173          exit_code = 1
 174  
 175      sys.exit(exit_code)
 176  
 177  
 178  if __name__ == "__main__":
 179      main()
 180