blob: f419e5631df5ddcf0b9afd0022b04cbe8c2b14d7 [file] [log] [blame]
/*
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.jdeps;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.module.ResolvedModule;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.sun.tools.jdeps.Module.*;
import static com.sun.tools.jdeps.ModulePaths.SystemModulePath.JAVA_BASE;
/**
* Analyze module dependences and compare with module descriptor.
* Also identify any qualified exports not used by the target module.
*/
class ModuleAnalyzer {
private final ModulePaths modulePaths;
private final DependencyFinder dependencyFinder;
private final Module root;
private final Set<Module> modules;
private final Set<String> requiresPublic = new HashSet<>();
private final Set<String> requires = new HashSet<>();
private final Set<Module> exportTargets = new HashSet<>();
private final JdepsFilter filter;
private Graph<Module> graph;
ModuleAnalyzer(ModulePaths modulePaths, DependencyFinder finder,
String moduleName) {
this.modulePaths = modulePaths;
this.dependencyFinder = finder;
this.root = modulePaths.getModules().get(moduleName);
this.modules = modulePaths.dependences(moduleName);
root.exports().values().stream()
.flatMap(Set::stream)
.map(target -> modulePaths.getModules().get(target))
.forEach(this.exportTargets::add);
this.filter = new JdepsFilter.Builder().filter(true, true).build();
}
/**
* Returns a graph of transitive closure of the given modules.
*
* This method does not add the implicit read edges and is intended to
* get all transitive closures in (reverse) topological sort.
*/
public static Graph<Module> graph(ModulePaths modulePaths, Module... modules) {
Graph.Builder<Module> gb = new Graph.Builder<>();
for (Module module : modules) {
module.descriptor().requires().stream()
.map(ModuleDescriptor.Requires::name)
.map(mn -> modulePaths.getModules().get(mn))
.forEach(m -> {
gb.addNode(m);
gb.addEdge(module, m);
});
}
return gb.build();
}
/**
* Do the analysis
*/
public boolean run() {
try {
computeRequiresPublic();
computeRequires();
// apply transitive reduction and reports recommended requires.
analyzeDeps();
// detect any qualiifed exports not used by the target module
checkQualifiedExports();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return true;
}
/**
* Compute 'requires public' dependences by analyzing API dependencies
*/
private void computeRequiresPublic() throws IOException {
JdepsFilter.Builder builder = new JdepsFilter.Builder();
// only analyze exported API
root.descriptor.exports().stream()
.filter(exp -> !exp.isQualified())
.map(ModuleDescriptor.Exports::source)
.forEach(builder::includePackage);
JdepsFilter filter = builder.filter(true, true).build();
// analyze dependences for exported packages
dependencyFinder.findDependencies(filter, true /* api only */, 1);
Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
analyzer.run(modules);
// record requires public
analyzer.requires(root)
.filter(m -> m != JAVA_BASE && m != root)
.map(Archive::getName)
.forEach(requiresPublic::add);
trace("requires public: %s%n", requiresPublic);
}
private void computeRequires() throws IOException {
// add the exportTargets of the qualified exports to the root set
exportTargets.stream()
.peek(target -> trace("add root: %s%n", target))
.forEach(dependencyFinder::addRoot);
// analyze all classes
dependencyFinder.findDependencies(filter, false /* all classes */, 1);
Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
analyzer.run(modules);
// record requires
analyzer.requires(root)
.filter(m -> m != JAVA_BASE && m != root)
.map(Archive::getName)
.forEach(requires::add);
this.graph = buildGraph(analyzer, root);
if (traceOn) {
trace("dependences: %s%n", graph.nodes());
graph.printGraph(System.out);
}
}
/**
* Apply transitive reduction on the resulting graph and reports
* recommended requires.
*/
private void analyzeDeps() {
String moduleName = root.name();
Graph.Builder<String> builder = new Graph.Builder<>();
requiresPublic.stream()
.forEach(mn -> {
builder.addNode(mn);
builder.addEdge(moduleName, mn);
});
// requires public graph
Graph<String> rbg = builder.build().reduce();
// convert the dependence graph from Module to name
Set<String> nodes = this.graph.nodes().stream()
.map(Module::name)
.collect(Collectors.toSet());
Map<String, Set<String>> edges = new HashMap<>();
this.graph.edges().keySet().stream()
.forEach(m -> {
String mn = m.name();
Set<String> es = edges.computeIfAbsent(mn, _k -> new HashSet<String>());
this.graph.edges().get(m).stream()
.map(Module::name)
.forEach(es::add);
});
// transitive reduction
Graph<String> newGraph = new Graph<>(nodes, edges).reduce(rbg);
if (traceOn) {
System.out.println("after transitive reduction");
newGraph.printGraph(System.out);
};
Set<String> reducedRequires = newGraph.adjacentNodes(moduleName);
if (matches(root.descriptor(), requires, requiresPublic) &&
matches(root.descriptor(), reducedRequires, requiresPublic)) {
System.out.println("--- Analysis result: no change for " + root.name());
} else {
System.out.println("--- Analysis result: suggested requires for " + root.name());
System.out.format("module %s%n", root.name());
requires.stream()
.sorted()
.forEach(dn -> System.out.format(" requires %s%s;%n",
requiresPublic.contains(dn) ? "public " : "", dn));
if (!requires.equals(reducedRequires) && !reducedRequires.isEmpty()) {
System.out.format("%nmodule %s (reduced)%n", root.name());
newGraph.adjacentNodes(moduleName)
.stream()
.sorted()
.forEach(dn -> System.out.format(" requires %s%s;%n",
requiresPublic.contains(dn) ? "public " : "", dn));
}
System.out.println("\n--- Module descriptor");
Graph<Module> mdGraph = graph(modulePaths, root);
mdGraph.reverse(m -> printModuleDescriptor(System.out, m.descriptor()));
}
}
/**
* Detects any qualified exports not used by the target module.
*/
private void checkQualifiedExports() throws IOException {
Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
analyzer.run(dependencyFinder.roots());
// build the qualified exports map
Map<String, Set<String>> qualifiedExports =
root.exports().entrySet().stream()
.filter(e -> !e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(Collectors.toMap(Function.identity(), _k -> new HashSet<>()));
// adds to the qualified exports map if a module references it
for (Module m : exportTargets) {
analyzer.dependences(m).stream()
.map(this::toPackageName)
.filter(qualifiedExports::containsKey)
.forEach(pn -> qualifiedExports.get(pn).add(m.name()));
}
// compare with the exports from ModuleDescriptor
Set<String> staleQualifiedExports =
qualifiedExports.keySet().stream()
.filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn)))
.collect(Collectors.toSet());
if (!staleQualifiedExports.isEmpty()) {
System.out.println("--- Unused qualified exports in " + root.name());
for (String pn : staleQualifiedExports) {
Set<String> unused = new HashSet<>(root.exports().get(pn));
unused.removeAll(qualifiedExports.get(pn));
System.out.format(" exports %s to %s%n", pn,
unused.stream().collect(Collectors.joining(",")));
}
}
}
private String toPackageName(String cn) {
int i = cn.lastIndexOf('.');
return i > 0 ? cn.substring(0, i) : "";
}
private boolean matches(ModuleDescriptor md, Set<String> requires, Set<String> requiresPublic) {
Set<String> reqPublic = md.requires().stream()
.filter(req -> req.modifiers().contains(PUBLIC))
.map(ModuleDescriptor.Requires::name)
.collect(Collectors.toSet());
if (!requiresPublic.equals(reqPublic)) {
trace("mismatch requires public: %s%n", reqPublic);
return false;
}
// java.base is not in requires
int javaBase = md.name().equals(JAVA_BASE.name()) ? 0 : 1;
if (requires.size()+javaBase != md.requires().size()) {
trace("mismatch requires: %d != %d%n", requires.size()+1, md.requires().size());
return false;
}
Set<String> unused = md.requires().stream()
.map(ModuleDescriptor.Requires::name)
.filter(req -> !requires.contains(req) && !req.equals(JAVA_BASE.name()))
.collect(Collectors.toSet());
if (!unused.isEmpty()) {
trace("mismatch requires: %s%n", unused);
return false;
}
return true;
}
private void printModuleDescriptor(PrintStream out, ModuleDescriptor descriptor) {
if (descriptor.name().equals("java.base"))
return;
out.format("module %s%n", descriptor.name());
descriptor.requires()
.stream()
.sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
.forEach(req -> out.format(" requires %s;%n", req));
}
/**
* Returns a graph of modules required by the specified module.
*
* Requires public edges of the dependences are added to the graph.
*/
private Graph<Module> buildGraph(Analyzer analyzer, Module module) {
Graph.Builder<Module> builder = new Graph.Builder<>();
builder.addNode(module);
Set<Module> visited = new HashSet<>();
visited.add(module);
Deque<Module> deque = new LinkedList<>();
analyzer.requires(module)
.map(Archive::getModule)
.filter(m -> m != JAVA_BASE)
.forEach(m -> {
deque.add(m);
builder.addEdge(module, m);
});
// read requires public from ModuleDescription
Module source;
while ((source = deque.poll()) != null) {
if (visited.contains(source))
continue;
visited.add(source);
builder.addNode(source);
Module from = source;
source.descriptor().requires().stream()
.filter(req -> req.modifiers().contains(PUBLIC))
.map(ModuleDescriptor.Requires::name)
.map(req -> modulePaths.getModules().get(req))
.filter(m -> m != JAVA_BASE)
.forEach(m -> {
deque.add(m);
builder.addEdge(from, m);
});
}
return builder.build();
}
static class Graph<T> {
private final Set<T> nodes;
private final Map<T, Set<T>> edges;
private Graph(Set<T> nodes, Map<T, Set<T>> edges) {
this.nodes = nodes;
this.edges = edges;
}
public Set<T> nodes() {
return nodes;
}
public Map<T, Set<T>> edges() {
return edges;
}
public Set<T> adjacentNodes(T u) {
return edges.get(u);
}
/**
* Returns a new Graph after transitive reduction
*/
public Graph<T> reduce() {
Graph.Builder<T> builder = new Builder<>();
nodes.stream()
.forEach(u -> {
builder.addNode(u);
edges.get(u).stream()
.filter(v -> !pathExists(u, v, false))
.forEach(v -> builder.addEdge(u, v));
});
return builder.build();
}
/**
* Returns a new Graph after transitive reduction. All edges in
* the given g takes precedence over this graph.
*
* @throw IllegalArgumentException g must be a subgraph this graph
*/
public Graph<T> reduce(Graph<T> g) {
boolean subgraph = nodes.containsAll(g.nodes) && g.edges.keySet().stream()
.allMatch(u -> adjacentNodes(u).containsAll(g.adjacentNodes(u)));
if (!subgraph) {
throw new IllegalArgumentException(g + " is not a subgraph of " + this);
}
Graph.Builder<T> builder = new Builder<>();
nodes.stream()
.forEach(u -> {
builder.addNode(u);
// filter the edge if there exists a path from u to v in the given g
// or there exists another path from u to v in this graph
edges.get(u).stream()
.filter(v -> !g.pathExists(u, v) && !pathExists(u, v, false))
.forEach(v -> builder.addEdge(u, v));
});
// add the overlapped edges from this graph and the given g
g.edges().keySet().stream()
.forEach(u -> g.adjacentNodes(u).stream()
.filter(v -> isAdjacent(u, v))
.forEach(v -> builder.addEdge(u, v)));
return builder.build();
}
/**
* Returns nodes sorted in topological order.
*/
public Stream<T> orderedNodes() {
TopoSorter<T> sorter = new TopoSorter<>(this);
return sorter.result.stream();
}
/**
* Iterates the nodes sorted in topological order and performs the
* given action.
*/
public void ordered(Consumer<T> action) {
TopoSorter<T> sorter = new TopoSorter<>(this);
sorter.ordered(action);
}
/**
* Iterates the nodes sorted in reverse topological order and
* performs the given action.
*/
public void reverse(Consumer<T> action) {
TopoSorter<T> sorter = new TopoSorter<>(this);
sorter.reverse(action);
}
private boolean isAdjacent(T u, T v) {
return edges.containsKey(u) && edges.get(u).contains(v);
}
private boolean pathExists(T u, T v) {
return pathExists(u, v, true);
}
/**
* Returns true if there exists a path from u to v in this graph.
* If includeAdjacent is false, it returns true if there exists
* another path from u to v of distance > 1
*/
private boolean pathExists(T u, T v, boolean includeAdjacent) {
if (!nodes.contains(u) || !nodes.contains(v)) {
return false;
}
if (includeAdjacent && isAdjacent(u, v)) {
return true;
}
Deque<T> stack = new LinkedList<>();
Set<T> visited = new HashSet<>();
stack.push(u);
while (!stack.isEmpty()) {
T node = stack.pop();
if (node.equals(v)) {
return true;
}
if (!visited.contains(node)) {
visited.add(node);
edges.get(node).stream()
.filter(e -> includeAdjacent || !node.equals(u) || !e.equals(v))
.forEach(e -> stack.push(e));
}
}
assert !visited.contains(v);
return false;
}
void printGraph(PrintStream out) {
out.println("graph for " + nodes);
nodes.stream()
.forEach(u -> adjacentNodes(u).stream()
.forEach(v -> out.format("%s -> %s%n", u, v)));
}
@Override
public String toString() {
return nodes.toString();
}
static class Builder<T> {
final Set<T> nodes = new HashSet<>();
final Map<T, Set<T>> edges = new HashMap<>();
public void addNode(T node) {
if (nodes.contains(node)) {
return;
}
nodes.add(node);
edges.computeIfAbsent(node, _e -> new HashSet<>());
}
public void addEdge(T u, T v) {
addNode(u);
addNode(v);
edges.get(u).add(v);
}
public Graph<T> build() {
return new Graph<>(nodes, edges);
}
void print(PrintStream out) {
out.println(nodes);
nodes.stream()
.forEach(u -> edges.get(u).stream()
.forEach(v -> out.format("%s -> %s%n", u, v)));
}
}
}
static class TopoSorter<T> {
final Deque<T> result = new LinkedList<>();
final Deque<T> nodes;
final Graph<T> graph;
TopoSorter(Graph<T> graph) {
this.graph = graph;
this.nodes = new LinkedList<>(graph.nodes);
sort();
}
public void ordered(Consumer<T> action) {
result.iterator().forEachRemaining(action);
}
public void reverse(Consumer<T> action) {
result.descendingIterator().forEachRemaining(action);
}
private void sort() {
Deque<T> visited = new LinkedList<>();
Deque<T> done = new LinkedList<>();
T node;
while ((node = nodes.poll()) != null) {
if (!visited.contains(node)) {
visit(node, visited, done);
}
}
}
private void visit(T node, Deque<T> visited, Deque<T> done) {
if (visited.contains(node)) {
if (!done.contains(node)) {
throw new IllegalArgumentException("Cyclic detected: " +
node + " " + graph.edges().get(node));
}
return;
}
visited.add(node);
graph.edges().get(node).stream()
.forEach(x -> visit(x, visited, done));
done.add(node);
result.addLast(node);
}
}
static class DotGraph {
static final String ORANGE = "#e76f00";
static final String BLUE = "#437291";
static final String GRAY = "#dddddd";
static final String REEXPORTS = "";
static final String REQUIRES = "style=\"dashed\"";
static final String REQUIRES_BASE = "color=\"" + GRAY + "\"";
static final Set<String> javaModules = modules(name ->
(name.startsWith("java.") && !name.equals("java.smartcardio")));
static final Set<String> jdkModules = modules(name ->
(name.startsWith("java.") ||
name.startsWith("jdk.") ||
name.startsWith("javafx.")) && !javaModules.contains(name));
private static Set<String> modules(Predicate<String> predicate) {
return ModuleFinder.ofSystem().findAll()
.stream()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::name)
.filter(predicate)
.collect(Collectors.toSet());
}
static void printAttributes(PrintStream out) {
out.format(" size=\"25,25\";%n");
out.format(" nodesep=.5;%n");
out.format(" ranksep=1.5;%n");
out.format(" pencolor=transparent;%n");
out.format(" node [shape=plaintext, fontname=\"DejaVuSans\", fontsize=36, margin=\".2,.2\"];%n");
out.format(" edge [penwidth=4, color=\"#999999\", arrowhead=open, arrowsize=2];%n");
}
static void printNodes(PrintStream out, Graph<String> graph) {
out.format(" subgraph se {%n");
graph.nodes().stream()
.filter(javaModules::contains)
.forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
mn, ORANGE, "java"));
out.format(" }%n");
graph.nodes().stream()
.filter(jdkModules::contains)
.forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n",
mn, BLUE, "jdk"));
graph.nodes().stream()
.filter(mn -> !javaModules.contains(mn) && !jdkModules.contains(mn))
.forEach(mn -> out.format(" \"%s\";%n", mn));
}
static void printEdges(PrintStream out, Graph<String> graph,
String node, Set<String> requiresPublic) {
graph.adjacentNodes(node).forEach(dn -> {
String attr = dn.equals("java.base") ? REQUIRES_BASE
: (requiresPublic.contains(dn) ? REEXPORTS : REQUIRES);
out.format(" \"%s\" -> \"%s\" [%s];%n", node, dn, attr);
});
}
}
public void genDotFile(Path dir) throws IOException {
String name = root.name();
try (PrintStream out
= new PrintStream(Files.newOutputStream(dir.resolve(name + ".dot")))) {
Configuration cf = modulePaths.configuration(name);
// transitive reduction
Graph<String> graph = gengraph(cf);
out.format("digraph \"%s\" {%n", name);
DotGraph.printAttributes(out);
DotGraph.printNodes(out, graph);
cf.modules().stream()
.map(ResolvedModule::reference)
.map(ModuleReference::descriptor)
.sorted(Comparator.comparing(ModuleDescriptor::name))
.forEach(md -> {
String mn = md.name();
Set<String> requiresPublic = md.requires().stream()
.filter(d -> d.modifiers().contains(PUBLIC))
.map(d -> d.name())
.collect(Collectors.toSet());
DotGraph.printEdges(out, graph, mn, requiresPublic);
});
out.println("}");
}
}
/**
* Returns a Graph of the given Configuration after transitive reduction.
*
* Transitive reduction of requires public edge and requires edge have
* to be applied separately to prevent the requires public edges
* (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V)
* in which V would not be re-exported from U.
*/
private Graph<String> gengraph(Configuration cf) {
// build a Graph containing only requires public edges
// with transitive reduction.
Graph.Builder<String> rpgbuilder = new Graph.Builder<>();
for (ResolvedModule resolvedModule : cf.modules()) {
ModuleDescriptor md = resolvedModule.reference().descriptor();
String mn = md.name();
md.requires().stream()
.filter(d -> d.modifiers().contains(PUBLIC))
.map(d -> d.name())
.forEach(d -> rpgbuilder.addEdge(mn, d));
}
Graph<String> rpg = rpgbuilder.build().reduce();
// build the readability graph
Graph.Builder<String> builder = new Graph.Builder<>();
for (ResolvedModule resolvedModule : cf.modules()) {
ModuleDescriptor md = resolvedModule.reference().descriptor();
String mn = md.name();
builder.addNode(mn);
resolvedModule.reads().stream()
.map(ResolvedModule::name)
.forEach(d -> builder.addEdge(mn, d));
}
// transitive reduction of requires edges
return builder.build().reduce(rpg);
}
}