Merge "Add a tool to let you enforce layering between packages in a java module."
This commit is contained in:
commit
b43535d84e
4 changed files with 268 additions and 2 deletions
|
@ -60,6 +60,7 @@ LOCAL_INTERMEDIATE_SOURCES:=
|
|||
LOCAL_INTERMEDIATE_SOURCE_DIR:=
|
||||
LOCAL_JAVACFLAGS:=
|
||||
LOCAL_JAVA_LIBRARIES:=
|
||||
LOCAL_JAVA_LAYERS_FILE:=
|
||||
LOCAL_NO_STANDARD_LIBRARIES:=
|
||||
LOCAL_CLASSPATH:=
|
||||
LOCAL_DROIDDOC_USE_STANDARD_DOCLET:=
|
||||
|
|
|
@ -1466,6 +1466,8 @@ $(hide) if [ -s $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq ] ; the
|
|||
\@$(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq \
|
||||
|| ( rm -rf $(PRIVATE_CLASS_INTERMEDIATES_DIR) ; exit 41 ) \
|
||||
fi
|
||||
$(if $(PRIVATE_JAVA_LAYERS_FILE), $(hide) build/tools/java-layers.py \
|
||||
$(PRIVATE_JAVA_LAYERS_FILE) \@$(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq,)
|
||||
$(hide) rm -f $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list
|
||||
$(hide) rm -f $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq
|
||||
$(if $(PRIVATE_JAR_EXCLUDE_FILES), $(hide) find $(PRIVATE_CLASS_INTERMEDIATES_DIR) \
|
||||
|
|
10
core/java.mk
10
core/java.mk
|
@ -254,6 +254,11 @@ $(full_classes_stubs_jar) : $(LOCAL_BUILT_MODULE) | $(ACP)
|
|||
$(hide) $(ACP) -fp $(PRIVATE_SOURCE_FILE) $@
|
||||
ALL_MODULES.$(LOCAL_MODULE).STUBS := $(full_classes_stubs_jar)
|
||||
|
||||
# The layers file allows you to enforce a layering between java packages.
|
||||
# Run build/tools/java-layers.py for more details.
|
||||
layers_file := $(addprefix $(LOCAL_PATH)/, $(LOCAL_JAVA_LAYERS_FILE))
|
||||
$(full_classes_compiled_jar): PRIVATE_JAVA_LAYERS_FILE := $(layers_file)
|
||||
|
||||
# Compile the java files to a .jar file.
|
||||
# This intentionally depends on java_sources, not all_java_sources.
|
||||
# Deps for generated source files must be handled separately,
|
||||
|
@ -261,8 +266,9 @@ ALL_MODULES.$(LOCAL_MODULE).STUBS := $(full_classes_stubs_jar)
|
|||
$(full_classes_compiled_jar): PRIVATE_JAVACFLAGS := $(LOCAL_JAVACFLAGS)
|
||||
$(full_classes_compiled_jar): PRIVATE_JAR_EXCLUDE_FILES := $(LOCAL_JAR_EXCLUDE_FILES)
|
||||
$(full_classes_compiled_jar): PRIVATE_DONT_DELETE_JAR_META_INF := $(LOCAL_DONT_DELETE_JAR_META_INF)
|
||||
$(full_classes_compiled_jar): $(java_sources) $(java_resource_sources) $(full_java_lib_deps) $(jar_manifest_file) \
|
||||
$(RenderScript_file_stamp) $(proto_java_sources_file_stamp) $(LOCAL_ADDITIONAL_DEPENDENCIES)
|
||||
$(full_classes_compiled_jar): $(java_sources) $(java_resource_sources) $(full_java_lib_deps) \
|
||||
$(jar_manifest_file) $(layers_file) \
|
||||
$(RenderScript_file_stamp) $(proto_java_sources_file_stamp)
|
||||
$(transform-java-to-classes.jar)
|
||||
|
||||
# All of the rules after full_classes_compiled_jar are very unlikely
|
||||
|
|
257
tools/java-layers.py
Executable file
257
tools/java-layers.py
Executable file
|
@ -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)
|
||||
|
Loading…
Reference in a new issue