blob: b3aec2b1d5fc88e688383ecd27f0daeb11579b3c [file] [log] [blame]
Joe Onorato0eccce92011-10-30 21:37:35 -07001#!/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