checkIncludes.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/python
  2. # Copyright 2018 The Tor Project, Inc. See LICENSE file for licensing info.
  3. """This script looks through all the directories for files matching *.c or
  4. *.h, and checks their #include directives to make sure that only "permitted"
  5. headers are included.
  6. Any #include directives with angle brackets (like #include <stdio.h>) are
  7. ignored -- only directives with quotes (like #include "foo.h") are
  8. considered.
  9. To decide what includes are permitted, this script looks at a .may_include
  10. file in each directory. This file contains empty lines, #-prefixed
  11. comments, filenames (like "lib/foo/bar.h") and file globs (like lib/*/*.h)
  12. for files that are permitted.
  13. """
  14. from __future__ import print_function
  15. import fnmatch
  16. import os
  17. import re
  18. import sys
  19. # Global: Have there been any errors?
  20. trouble = False
  21. if sys.version_info[0] <= 2:
  22. def open_file(fname):
  23. return open(fname, 'r')
  24. else:
  25. def open_file(fname):
  26. return open(fname, 'r', encoding='utf-8')
  27. def warn(msg):
  28. print(msg, file=sys.stderr)
  29. def err(msg):
  30. """ Declare that an error has happened, and remember that there has
  31. been an error. """
  32. global trouble
  33. trouble = True
  34. print(msg, file=sys.stderr)
  35. def fname_is_c(fname):
  36. """ Return true iff 'fname' is the name of a file that we should
  37. search for possibly disallowed #include directives. """
  38. return fname.endswith(".h") or fname.endswith(".c")
  39. INCLUDE_PATTERN = re.compile(r'\s*#\s*include\s+"([^"]*)"')
  40. RULES_FNAME = ".may_include"
  41. ALLOWED_PATTERNS = [
  42. re.compile(r'^.*\*\.(h|inc)$'),
  43. re.compile(r'^.*/.*\.h$'),
  44. re.compile(r'^ext/.*\.c$'),
  45. re.compile(r'^orconfig.h$'),
  46. re.compile(r'^micro-revision.i$'),
  47. ]
  48. def pattern_is_normal(s):
  49. for p in ALLOWED_PATTERNS:
  50. if p.match(s):
  51. return True
  52. return False
  53. class Rules(object):
  54. """ A 'Rules' object is the parsed version of a .may_include file. """
  55. def __init__(self, dirpath):
  56. self.dirpath = dirpath
  57. if dirpath.startswith("src/"):
  58. self.incpath = dirpath[4:]
  59. else:
  60. self.incpath = dirpath
  61. self.patterns = []
  62. self.usedPatterns = set()
  63. def addPattern(self, pattern):
  64. if not pattern_is_normal(pattern):
  65. warn("Unusual pattern {} in {}".format(pattern, self.dirpath))
  66. self.patterns.append(pattern)
  67. def includeOk(self, path):
  68. for pattern in self.patterns:
  69. if fnmatch.fnmatchcase(path, pattern):
  70. self.usedPatterns.add(pattern)
  71. return True
  72. return False
  73. def applyToLines(self, lines, context=""):
  74. lineno = 0
  75. for line in lines:
  76. lineno += 1
  77. m = INCLUDE_PATTERN.match(line)
  78. if m:
  79. include = m.group(1)
  80. if not self.includeOk(include):
  81. err("Forbidden include of {} on line {}{}".format(
  82. include, lineno, context))
  83. def applyToFile(self, fname):
  84. with open_file(fname) as f:
  85. #print(fname)
  86. self.applyToLines(iter(f), " of {}".format(fname))
  87. def noteUnusedRules(self):
  88. for p in self.patterns:
  89. if p not in self.usedPatterns:
  90. print("Pattern {} in {} was never used.".format(p, self.dirpath))
  91. def getAllowedDirectories(self):
  92. allowed = []
  93. for p in self.patterns:
  94. m = re.match(r'^(.*)/\*\.(h|inc)$', p)
  95. if m:
  96. allowed.append(m.group(1))
  97. continue
  98. m = re.match(r'^(.*)/[^/]*$', p)
  99. if m:
  100. allowed.append(m.group(1))
  101. continue
  102. return allowed
  103. def load_include_rules(fname):
  104. """ Read a rules file from 'fname', and return it as a Rules object. """
  105. result = Rules(os.path.split(fname)[0])
  106. with open_file(fname) as f:
  107. for line in f:
  108. line = line.strip()
  109. if line.startswith("#") or not line:
  110. continue
  111. result.addPattern(line)
  112. return result
  113. list_unused = False
  114. log_sorted_levels = False
  115. uses_dirs = { }
  116. for dirpath, dirnames, fnames in os.walk("src"):
  117. if ".may_include" in fnames:
  118. rules = load_include_rules(os.path.join(dirpath, RULES_FNAME))
  119. for fname in fnames:
  120. if fname_is_c(fname):
  121. rules.applyToFile(os.path.join(dirpath,fname))
  122. if list_unused:
  123. rules.noteUnusedRules()
  124. uses_dirs[rules.incpath] = rules.getAllowedDirectories()
  125. if trouble:
  126. err(
  127. """To change which includes are allowed in a C file, edit the {}
  128. files in its enclosing directory.""".format(RULES_FNAME))
  129. sys.exit(1)
  130. all_levels = []
  131. n = 0
  132. while uses_dirs:
  133. n += 0
  134. cur_level = []
  135. for k in list(uses_dirs):
  136. uses_dirs[k] = [ d for d in uses_dirs[k]
  137. if (d in uses_dirs and d != k)]
  138. if uses_dirs[k] == []:
  139. cur_level.append(k)
  140. for k in cur_level:
  141. del uses_dirs[k]
  142. n += 1
  143. if cur_level and log_sorted_levels:
  144. print(n, cur_level)
  145. if n > 100:
  146. break
  147. if uses_dirs:
  148. print("There are circular .may_include dependencies in here somewhere:",
  149. uses_dirs)
  150. sys.exit(1)