summaryrefslogtreecommitdiff
path: root/scripts/maint/checkIncludes.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/maint/checkIncludes.py')
-rwxr-xr-xscripts/maint/checkIncludes.py115
1 files changed, 115 insertions, 0 deletions
diff --git a/scripts/maint/checkIncludes.py b/scripts/maint/checkIncludes.py
new file mode 100755
index 0000000000..46a3f39638
--- /dev/null
+++ b/scripts/maint/checkIncludes.py
@@ -0,0 +1,115 @@
+#!/usr/bin/python3
+# Copyright 2018 The Tor Project, Inc. See LICENSE file for licensing info.
+
+"""This script looks through all the directories for files matching *.c or
+ *.h, and checks their #include directives to make sure that only "permitted"
+ headers are included.
+
+ Any #include directives with angle brackets (like #include <stdio.h>) are
+ ignored -- only directives with quotes (like #include "foo.h") are
+ considered.
+
+ To decide what includes are permitted, this script looks at a .may_include
+ file in each directory. This file contains empty lines, #-prefixed
+ comments, filenames (like "lib/foo/bar.h") and file globs (like lib/*/*.h)
+ for files that are permitted.
+"""
+
+
+from __future__ import print_function
+
+import fnmatch
+import os
+import re
+import sys
+
+# Global: Have there been any errors?
+trouble = False
+
+if sys.version_info[0] <= 2:
+ def open_file(fname):
+ return open(fname, 'r')
+else:
+ def open_file(fname):
+ return open(fname, 'r', encoding='utf-8')
+
+def err(msg):
+ """ Declare that an error has happened, and remember that there has
+ been an error. """
+ global trouble
+ trouble = True
+ print(msg, file=sys.stderr)
+
+def fname_is_c(fname):
+ """ Return true iff 'fname' is the name of a file that we should
+ search for possibly disallowed #include directives. """
+ return fname.endswith(".h") or fname.endswith(".c")
+
+INCLUDE_PATTERN = re.compile(r'\s*#\s*include\s+"([^"]*)"')
+RULES_FNAME = ".may_include"
+
+class Rules(object):
+ """ A 'Rules' object is the parsed version of a .may_include file. """
+ def __init__(self, dirpath):
+ self.dirpath = dirpath
+ self.patterns = []
+ self.usedPatterns = set()
+
+ def addPattern(self, pattern):
+ self.patterns.append(pattern)
+
+ def includeOk(self, path):
+ for pattern in self.patterns:
+ if fnmatch.fnmatchcase(path, pattern):
+ self.usedPatterns.add(pattern)
+ return True
+ return False
+
+ def applyToLines(self, lines, context=""):
+ lineno = 0
+ for line in lines:
+ lineno += 1
+ m = INCLUDE_PATTERN.match(line)
+ if m:
+ include = m.group(1)
+ if not self.includeOk(include):
+ err("Forbidden include of {} on line {}{}".format(
+ include, lineno, context))
+
+ def applyToFile(self, fname):
+ with open_file(fname) as f:
+ #print(fname)
+ self.applyToLines(iter(f), " of {}".format(fname))
+
+ def noteUnusedRules(self):
+ for p in self.patterns:
+ if p not in self.usedPatterns:
+ print("Pattern {} in {} was never used.".format(p, self.dirpath))
+
+def load_include_rules(fname):
+ """ Read a rules file from 'fname', and return it as a Rules object. """
+ result = Rules(os.path.split(fname)[0])
+ with open_file(fname) as f:
+ for line in f:
+ line = line.strip()
+ if line.startswith("#") or not line:
+ continue
+ result.addPattern(line)
+ return result
+
+list_unused = False
+
+for dirpath, dirnames, fnames in os.walk("src"):
+ if ".may_include" in fnames:
+ rules = load_include_rules(os.path.join(dirpath, RULES_FNAME))
+ for fname in fnames:
+ if fname_is_c(fname):
+ rules.applyToFile(os.path.join(dirpath,fname))
+ if list_unused:
+ rules.noteUnusedRules()
+
+if trouble:
+ err(
+"""To change which includes are allowed in a C file, edit the {}
+files in its enclosing directory.""".format(RULES_FNAME))
+ sys.exit(1)