diff options
author | Joe Onorato <joeo@google.com> | 2011-10-30 21:37:35 -0700 |
---|---|---|
committer | Ying Wang <wangying@google.com> | 2012-10-18 10:21:46 -0700 |
commit | 0eccce99d7c23b403c6047738d88c616213ad7d7 (patch) | |
tree | 7d9b50f575ba104d85fdcf9b5e1b54e4bfd665f4 /tools/java-layers.py | |
parent | a7fa6a460790b04883dd0d9e48ff6e40e333eb96 (diff) | |
download | build-0eccce99d7c23b403c6047738d88c616213ad7d7.zip build-0eccce99d7c23b403c6047738d88c616213ad7d7.tar.gz build-0eccce99d7c23b403c6047738d88c616213ad7d7.tar.bz2 |
Add a tool to let you enforce layering between packages in a java module.
And build system support for it too.
Change-Id: I4dd5ed0b9edab6e8884b0d00cfeeae5fa38d967a
Diffstat (limited to 'tools/java-layers.py')
-rwxr-xr-x | tools/java-layers.py | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/tools/java-layers.py b/tools/java-layers.py new file mode 100755 index 0000000..b3aec2b --- /dev/null +++ b/tools/java-layers.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python + +import os +import re +import sys + +def fail_with_usage(): + sys.stderr.write("usage: java-layers.py DEPENDENCY_FILE SOURCE_DIRECTORIES...\n") + sys.stderr.write("\n") + sys.stderr.write("Enforces layering between java packages. Scans\n") + sys.stderr.write("DIRECTORY and prints errors when the packages violate\n") + sys.stderr.write("the rules defined in the DEPENDENCY_FILE.\n") + sys.stderr.write("\n") + sys.stderr.write("Prints a warning when an unknown package is encountered\n") + sys.stderr.write("on the assumption that it should fit somewhere into the\n") + sys.stderr.write("layering.\n") + sys.stderr.write("\n") + sys.stderr.write("DEPENDENCY_FILE format\n") + sys.stderr.write(" - # starts comment\n") + sys.stderr.write(" - Lines consisting of two java package names: The\n") + sys.stderr.write(" first package listed must not contain any references\n") + sys.stderr.write(" to any classes present in the second package, or any\n") + sys.stderr.write(" of its dependencies.\n") + sys.stderr.write(" - Lines consisting of one java package name: The\n") + sys.stderr.write(" packge is assumed to be a high level package and\n") + sys.stderr.write(" nothing may depend on it.\n") + sys.stderr.write(" - Lines consisting of a dash (+) followed by one java\n") + sys.stderr.write(" package name: The package is considered a low level\n") + sys.stderr.write(" package and may not import any of the other packages\n") + sys.stderr.write(" listed in the dependency file.\n") + sys.stderr.write(" - Lines consisting of a plus (-) followed by one java\n") + sys.stderr.write(" package name: The package is considered \'legacy\'\n") + sys.stderr.write(" and excluded from errors.\n") + sys.stderr.write("\n") + sys.exit(1) + +class Dependency: + def __init__(self, filename, lineno, lower, top, lowlevel, legacy): + self.filename = filename + self.lineno = lineno + self.lower = lower + self.top = top + self.lowlevel = lowlevel + self.legacy = legacy + self.uppers = [] + self.transitive = set() + + def matches(self, imp): + for d in self.transitive: + if imp.startswith(d): + return True + return False + +class Dependencies: + def __init__(self, deps): + def recurse(obj, dep, visited): + global err + if dep in visited: + sys.stderr.write("%s:%d: Circular dependency found:\n" + % (dep.filename, dep.lineno)) + for v in visited: + sys.stderr.write("%s:%d: Dependency: %s\n" + % (v.filename, v.lineno, v.lower)) + err = True + return + visited.append(dep) + for upper in dep.uppers: + obj.transitive.add(upper) + if upper in deps: + recurse(obj, deps[upper], visited) + self.deps = deps + self.parts = [(dep.lower.split('.'),dep) for dep in deps.itervalues()] + # transitive closure of dependencies + for dep in deps.itervalues(): + recurse(dep, dep, []) + # disallow everything from the low level components + for dep in deps.itervalues(): + if dep.lowlevel: + for d in deps.itervalues(): + if dep != d and not d.legacy: + dep.transitive.add(d.lower) + # disallow the 'top' components everywhere but in their own package + for dep in deps.itervalues(): + if dep.top and not dep.legacy: + for d in deps.itervalues(): + if dep != d and not d.legacy: + d.transitive.add(dep.lower) + for dep in deps.itervalues(): + dep.transitive = set([x+"." for x in dep.transitive]) + if False: + for dep in deps.itervalues(): + print "-->", dep.lower, "-->", dep.transitive + + # Lookup the dep object for the given package. If pkg is a subpackage + # of one with a rule, that one will be returned. If no matches are found, + # None is returned. + def lookup(self, pkg): + # Returns the number of parts that match + def compare_parts(parts, pkg): + if len(parts) > len(pkg): + return 0 + n = 0 + for i in range(0, len(parts)): + if parts[i] != pkg[i]: + return 0 + n = n + 1 + return n + pkg = pkg.split(".") + matched = 0 + result = None + for (parts,dep) in self.parts: + x = compare_parts(parts, pkg) + if x > matched: + matched = x + result = dep + return result + +def parse_dependency_file(filename): + global err + f = file(filename) + lines = f.readlines() + f.close() + def lineno(s, i): + i[0] = i[0] + 1 + return (i[0],s) + n = [0] + lines = [lineno(x,n) for x in lines] + lines = [(n,s.split("#")[0].strip()) for (n,s) in lines] + lines = [(n,s) for (n,s) in lines if len(s) > 0] + lines = [(n,s.split()) for (n,s) in lines] + deps = {} + for n,words in lines: + if len(words) == 1: + lower = words[0] + top = True + legacy = False + lowlevel = False + if lower[0] == '+': + lower = lower[1:] + top = False + lowlevel = True + elif lower[0] == '-': + lower = lower[1:] + legacy = True + if lower in deps: + sys.stderr.write(("%s:%d: Package '%s' already defined on" + + " line %d.\n") % (filename, n, lower, deps[lower].lineno)) + err = True + else: + deps[lower] = Dependency(filename, n, lower, top, lowlevel, legacy) + elif len(words) == 2: + lower = words[0] + upper = words[1] + if lower in deps: + dep = deps[lower] + if dep.top: + sys.stderr.write(("%s:%d: Can't add dependency to top level package " + + "'%s'\n") % (filename, n, lower)) + err = True + else: + dep = Dependency(filename, n, lower, False, False, False) + deps[lower] = dep + dep.uppers.append(upper) + else: + sys.stderr.write("%s:%d: Too many words on line starting at \'%s\'\n" % ( + filename, n, words[2])) + err = True + return Dependencies(deps) + +def find_java_files(srcs): + result = [] + for d in srcs: + if d[0] == '@': + f = file(d[1:]) + result.extend([fn for fn in [s.strip() for s in f.readlines()] + if len(fn) != 0]) + f.close() + else: + for root, dirs, files in os.walk(d): + result.extend([os.sep.join((root,f)) for f in files + if f.lower().endswith(".java")]) + return result + +COMMENTS = re.compile("//.*?\n|/\*.*?\*/", re.S) +PACKAGE = re.compile("package\s+(.*)") +IMPORT = re.compile("import\s+(.*)") + +def examine_java_file(deps, filename): + global err + # Yes, this is a crappy java parser. Write a better one if you want to. + f = file(filename) + text = f.read() + f.close() + text = COMMENTS.sub("", text) + index = text.find("{") + if index < 0: + sys.stderr.write(("%s: Error: Unable to parse java. Can't find class " + + "declaration.\n") % filename) + err = True + return + text = text[0:index] + statements = [s.strip() for s in text.split(";")] + # First comes the package declaration. Then iterate while we see import + # statements. Anything else is either bad syntax that we don't care about + # because the compiler will fail, or the beginning of the class declaration. + m = PACKAGE.match(statements[0]) + if not m: + sys.stderr.write(("%s: Error: Unable to parse java. Missing package " + + "statement.\n") % filename) + err = True + return + pkg = m.group(1) + imports = [] + for statement in statements[1:]: + m = IMPORT.match(statement) + if not m: + break + imports.append(m.group(1)) + # Do the checking + if False: + print filename + print "'%s' --> %s" % (pkg, imports) + dep = deps.lookup(pkg) + if not dep: + sys.stderr.write(("%s: Error: Package does not appear in dependency file: " + + "%s\n") % (filename, pkg)) + err = True + return + for imp in imports: + if dep.matches(imp): + sys.stderr.write("%s: Illegal import in package '%s' of '%s'\n" + % (filename, pkg, imp)) + err = True + +err = False + +def main(argv): + if len(argv) < 3: + fail_with_usage() + deps = parse_dependency_file(argv[1]) + + if err: + sys.exit(1) + + java = find_java_files(argv[2:]) + for filename in java: + examine_java_file(deps, filename) + + if err: + sys.stderr.write("%s: Using this file as dependency file.\n" % argv[1]) + sys.exit(1) + + sys.exit(0) + +if __name__ == "__main__": + main(sys.argv) + |