summaryrefslogtreecommitdiffstats
path: root/tools/java-layers.py
diff options
context:
space:
mode:
authorJoe Onorato <joeo@google.com>2011-10-30 21:37:35 -0700
committerYing Wang <wangying@google.com>2012-10-18 10:21:46 -0700
commit0eccce99d7c23b403c6047738d88c616213ad7d7 (patch)
tree7d9b50f575ba104d85fdcf9b5e1b54e4bfd665f4 /tools/java-layers.py
parenta7fa6a460790b04883dd0d9e48ff6e40e333eb96 (diff)
downloadbuild-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-xtools/java-layers.py257
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)
+