| #!/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) | 
 |  |