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