1#!/usr/bin/env python 2 3import os 4import re 5import sys 6 7def fail_with_usage(): 8 sys.stderr.write("usage: java-layers.py DEPENDENCY_FILE SOURCE_DIRECTORIES...\n") 9 sys.stderr.write("\n") 10 sys.stderr.write("Enforces layering between java packages. Scans\n") 11 sys.stderr.write("DIRECTORY and prints errors when the packages violate\n") 12 sys.stderr.write("the rules defined in the DEPENDENCY_FILE.\n") 13 sys.stderr.write("\n") 14 sys.stderr.write("Prints a warning when an unknown package is encountered\n") 15 sys.stderr.write("on the assumption that it should fit somewhere into the\n") 16 sys.stderr.write("layering.\n") 17 sys.stderr.write("\n") 18 sys.stderr.write("DEPENDENCY_FILE format\n") 19 sys.stderr.write(" - # starts comment\n") 20 sys.stderr.write(" - Lines consisting of two java package names: The\n") 21 sys.stderr.write(" first package listed must not contain any references\n") 22 sys.stderr.write(" to any classes present in the second package, or any\n") 23 sys.stderr.write(" of its dependencies.\n") 24 sys.stderr.write(" - Lines consisting of one java package name: The\n") 25 sys.stderr.write(" packge is assumed to be a high level package and\n") 26 sys.stderr.write(" nothing may depend on it.\n") 27 sys.stderr.write(" - Lines consisting of a dash (+) followed by one java\n") 28 sys.stderr.write(" package name: The package is considered a low level\n") 29 sys.stderr.write(" package and may not import any of the other packages\n") 30 sys.stderr.write(" listed in the dependency file.\n") 31 sys.stderr.write(" - Lines consisting of a plus (-) followed by one java\n") 32 sys.stderr.write(" package name: The package is considered \'legacy\'\n") 33 sys.stderr.write(" and excluded from errors.\n") 34 sys.stderr.write("\n") 35 sys.exit(1) 36 37class Dependency: 38 def __init__(self, filename, lineno, lower, top, lowlevel, legacy): 39 self.filename = filename 40 self.lineno = lineno 41 self.lower = lower 42 self.top = top 43 self.lowlevel = lowlevel 44 self.legacy = legacy 45 self.uppers = [] 46 self.transitive = set() 47 48 def matches(self, imp): 49 for d in self.transitive: 50 if imp.startswith(d): 51 return True 52 return False 53 54class Dependencies: 55 def __init__(self, deps): 56 def recurse(obj, dep, visited): 57 global err 58 if dep in visited: 59 sys.stderr.write("%s:%d: Circular dependency found:\n" 60 % (dep.filename, dep.lineno)) 61 for v in visited: 62 sys.stderr.write("%s:%d: Dependency: %s\n" 63 % (v.filename, v.lineno, v.lower)) 64 err = True 65 return 66 visited.append(dep) 67 for upper in dep.uppers: 68 obj.transitive.add(upper) 69 if upper in deps: 70 recurse(obj, deps[upper], visited) 71 self.deps = deps 72 self.parts = [(dep.lower.split('.'),dep) for dep in deps.itervalues()] 73 # transitive closure of dependencies 74 for dep in deps.itervalues(): 75 recurse(dep, dep, []) 76 # disallow everything from the low level components 77 for dep in deps.itervalues(): 78 if dep.lowlevel: 79 for d in deps.itervalues(): 80 if dep != d and not d.legacy: 81 dep.transitive.add(d.lower) 82 # disallow the 'top' components everywhere but in their own package 83 for dep in deps.itervalues(): 84 if dep.top and not dep.legacy: 85 for d in deps.itervalues(): 86 if dep != d and not d.legacy: 87 d.transitive.add(dep.lower) 88 for dep in deps.itervalues(): 89 dep.transitive = set([x+"." for x in dep.transitive]) 90 if False: 91 for dep in deps.itervalues(): 92 print "-->", dep.lower, "-->", dep.transitive 93 94 # Lookup the dep object for the given package. If pkg is a subpackage 95 # of one with a rule, that one will be returned. If no matches are found, 96 # None is returned. 97 def lookup(self, pkg): 98 # Returns the number of parts that match 99 def compare_parts(parts, pkg): 100 if len(parts) > len(pkg): 101 return 0 102 n = 0 103 for i in range(0, len(parts)): 104 if parts[i] != pkg[i]: 105 return 0 106 n = n + 1 107 return n 108 pkg = pkg.split(".") 109 matched = 0 110 result = None 111 for (parts,dep) in self.parts: 112 x = compare_parts(parts, pkg) 113 if x > matched: 114 matched = x 115 result = dep 116 return result 117 118def parse_dependency_file(filename): 119 global err 120 f = file(filename) 121 lines = f.readlines() 122 f.close() 123 def lineno(s, i): 124 i[0] = i[0] + 1 125 return (i[0],s) 126 n = [0] 127 lines = [lineno(x,n) for x in lines] 128 lines = [(n,s.split("#")[0].strip()) for (n,s) in lines] 129 lines = [(n,s) for (n,s) in lines if len(s) > 0] 130 lines = [(n,s.split()) for (n,s) in lines] 131 deps = {} 132 for n,words in lines: 133 if len(words) == 1: 134 lower = words[0] 135 top = True 136 legacy = False 137 lowlevel = False 138 if lower[0] == '+': 139 lower = lower[1:] 140 top = False 141 lowlevel = True 142 elif lower[0] == '-': 143 lower = lower[1:] 144 legacy = True 145 if lower in deps: 146 sys.stderr.write(("%s:%d: Package '%s' already defined on" 147 + " line %d.\n") % (filename, n, lower, deps[lower].lineno)) 148 err = True 149 else: 150 deps[lower] = Dependency(filename, n, lower, top, lowlevel, legacy) 151 elif len(words) == 2: 152 lower = words[0] 153 upper = words[1] 154 if lower in deps: 155 dep = deps[lower] 156 if dep.top: 157 sys.stderr.write(("%s:%d: Can't add dependency to top level package " 158 + "'%s'\n") % (filename, n, lower)) 159 err = True 160 else: 161 dep = Dependency(filename, n, lower, False, False, False) 162 deps[lower] = dep 163 dep.uppers.append(upper) 164 else: 165 sys.stderr.write("%s:%d: Too many words on line starting at \'%s\'\n" % ( 166 filename, n, words[2])) 167 err = True 168 return Dependencies(deps) 169 170def find_java_files(srcs): 171 result = [] 172 for d in srcs: 173 if d[0] == '@': 174 f = file(d[1:]) 175 result.extend([fn for fn in [s.strip() for s in f.readlines()] 176 if len(fn) != 0]) 177 f.close() 178 else: 179 for root, dirs, files in os.walk(d): 180 result.extend([os.sep.join((root,f)) for f in files 181 if f.lower().endswith(".java")]) 182 return result 183 184COMMENTS = re.compile("//.*?\n|/\*.*?\*/", re.S) 185PACKAGE = re.compile("package\s+(.*)") 186IMPORT = re.compile("import\s+(.*)") 187 188def examine_java_file(deps, filename): 189 global err 190 # Yes, this is a crappy java parser. Write a better one if you want to. 191 f = file(filename) 192 text = f.read() 193 f.close() 194 text = COMMENTS.sub("", text) 195 index = text.find("{") 196 if index < 0: 197 sys.stderr.write(("%s: Error: Unable to parse java. Can't find class " 198 + "declaration.\n") % filename) 199 err = True 200 return 201 text = text[0:index] 202 statements = [s.strip() for s in text.split(";")] 203 # First comes the package declaration. Then iterate while we see import 204 # statements. Anything else is either bad syntax that we don't care about 205 # because the compiler will fail, or the beginning of the class declaration. 206 m = PACKAGE.match(statements[0]) 207 if not m: 208 sys.stderr.write(("%s: Error: Unable to parse java. Missing package " 209 + "statement.\n") % filename) 210 err = True 211 return 212 pkg = m.group(1) 213 imports = [] 214 for statement in statements[1:]: 215 m = IMPORT.match(statement) 216 if not m: 217 break 218 imports.append(m.group(1)) 219 # Do the checking 220 if False: 221 print filename 222 print "'%s' --> %s" % (pkg, imports) 223 dep = deps.lookup(pkg) 224 if not dep: 225 sys.stderr.write(("%s: Error: Package does not appear in dependency file: " 226 + "%s\n") % (filename, pkg)) 227 err = True 228 return 229 for imp in imports: 230 if dep.matches(imp): 231 sys.stderr.write("%s: Illegal import in package '%s' of '%s'\n" 232 % (filename, pkg, imp)) 233 err = True 234 235err = False 236 237def main(argv): 238 if len(argv) < 3: 239 fail_with_usage() 240 deps = parse_dependency_file(argv[1]) 241 242 if err: 243 sys.exit(1) 244 245 java = find_java_files(argv[2:]) 246 for filename in java: 247 examine_java_file(deps, filename) 248 249 if err: 250 sys.stderr.write("%s: Using this file as dependency file.\n" % argv[1]) 251 sys.exit(1) 252 253 sys.exit(0) 254 255if __name__ == "__main__": 256 main(sys.argv) 257 258