checkIncludes.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/python3
  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 err(msg):
  28. """ Declare that an error has happened, and remember that there has
  29. been an error. """
  30. global trouble
  31. trouble = True
  32. print(msg, file=sys.stderr)
  33. def fname_is_c(fname):
  34. """ Return true iff 'fname' is the name of a file that we should
  35. search for possibly disallowed #include directives. """
  36. return fname.endswith(".h") or fname.endswith(".c")
  37. INCLUDE_PATTERN = re.compile(r'\s*#\s*include\s+"([^"]*)"')
  38. RULES_FNAME = ".may_include"
  39. class Rules(object):
  40. """ A 'Rules' object is the parsed version of a .may_include file. """
  41. def __init__(self, dirpath):
  42. self.dirpath = dirpath
  43. self.patterns = []
  44. self.usedPatterns = set()
  45. def addPattern(self, pattern):
  46. self.patterns.append(pattern)
  47. def includeOk(self, path):
  48. for pattern in self.patterns:
  49. if fnmatch.fnmatchcase(path, pattern):
  50. self.usedPatterns.add(pattern)
  51. return True
  52. return False
  53. def applyToLines(self, lines, context=""):
  54. lineno = 0
  55. for line in lines:
  56. lineno += 1
  57. m = INCLUDE_PATTERN.match(line)
  58. if m:
  59. include = m.group(1)
  60. if not self.includeOk(include):
  61. err("Forbidden include of {} on line {}{}".format(
  62. include, lineno, context))
  63. def applyToFile(self, fname):
  64. with open_file(fname) as f:
  65. #print(fname)
  66. self.applyToLines(iter(f), " of {}".format(fname))
  67. def noteUnusedRules(self):
  68. for p in self.patterns:
  69. if p not in self.usedPatterns:
  70. print("Pattern {} in {} was never used.".format(p, self.dirpath))
  71. def load_include_rules(fname):
  72. """ Read a rules file from 'fname', and return it as a Rules object. """
  73. result = Rules(os.path.split(fname)[0])
  74. with open_file(fname) as f:
  75. for line in f:
  76. line = line.strip()
  77. if line.startswith("#") or not line:
  78. continue
  79. result.addPattern(line)
  80. return result
  81. list_unused = False
  82. for dirpath, dirnames, fnames in os.walk("src"):
  83. if ".may_include" in fnames:
  84. rules = load_include_rules(os.path.join(dirpath, RULES_FNAME))
  85. for fname in fnames:
  86. if fname_is_c(fname):
  87. rules.applyToFile(os.path.join(dirpath,fname))
  88. if list_unused:
  89. rules.noteUnusedRules()
  90. if trouble:
  91. err(
  92. """To change which includes are allowed in a C file, edit the {}
  93. files in its enclosing directory.""".format(RULES_FNAME))
  94. sys.exit(1)