checkIncludes.py 3.4 KB

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