/* * 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 static com.sun.tools.jdeps.Graph.*; import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER; import static com.sun.tools.jdeps.Module.*; import static java.lang.module.ModuleDescriptor.Requires.Modifier.*; import static java.util.stream.Collectors.*; import com.sun.tools.classfile.Dependency; import com.sun.tools.jdeps.JdepsTask.BadArgs; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Analyze module dependences and compare with module descriptor. * Also identify any qualified exports not used by the target module. */ public class ModuleAnalyzer { private static final String JAVA_BASE = "java.base"; private final JdepsConfiguration configuration; private final PrintWriter log; private final DependencyFinder dependencyFinder; private final Map<Module, ModuleDeps> modules; public ModuleAnalyzer(JdepsConfiguration config, PrintWriter log) { this(config, log, Collections.emptySet()); } public ModuleAnalyzer(JdepsConfiguration config, PrintWriter log, Set<String> names) { this.configuration = config; this.log = log; this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER); if (names.isEmpty()) { this.modules = configuration.rootModules().stream() .collect(toMap(Function.identity(), ModuleDeps::new)); } else { this.modules = names.stream() .map(configuration::findModule) .flatMap(Optional::stream) .collect(toMap(Function.identity(), ModuleDeps::new)); } } public boolean run() throws IOException { try { // compute "requires transitive" dependences modules.values().forEach(ModuleDeps::computeRequiresTransitive); modules.values().forEach(md -> { // compute "requires" dependences md.computeRequires(); // apply transitive reduction and reports recommended requires. md.analyzeDeps(); }); } finally { dependencyFinder.shutdown(); } return true; } class ModuleDeps { final Module root; Set<Module> requiresTransitive; Set<Module> requires; Map<String, Set<String>> unusedQualifiedExports; ModuleDeps(Module root) { this.root = root; } /** * Compute 'requires transitive' dependences by analyzing API dependencies */ private void computeRequiresTransitive() { // record requires transitive this.requiresTransitive = computeRequires(true) .filter(m -> !m.name().equals(JAVA_BASE)) .collect(toSet()); trace("requires transitive: %s%n", requiresTransitive); } private void computeRequires() { this.requires = computeRequires(false).collect(toSet()); trace("requires: %s%n", requires); } private Stream<Module> computeRequires(boolean apionly) { // analyze all classes if (apionly) { dependencyFinder.parseExportedAPIs(Stream.of(root)); } else { dependencyFinder.parse(Stream.of(root)); } // find the modules of all the dependencies found return dependencyFinder.getDependences(root) .map(Archive::getModule); } ModuleDescriptor descriptor() { return descriptor(requiresTransitive, requires); } private ModuleDescriptor descriptor(Set<Module> requiresTransitive, Set<Module> requires) { ModuleDescriptor.Builder builder = ModuleDescriptor.module(root.name()); if (!root.name().equals(JAVA_BASE)) builder.requires(Set.of(MANDATED), JAVA_BASE); requiresTransitive.stream() .filter(m -> !m.name().equals(JAVA_BASE)) .map(Module::name) .forEach(mn -> builder.requires(Set.of(TRANSITIVE), mn)); requires.stream() .filter(m -> !requiresTransitive.contains(m)) .filter(m -> !m.name().equals(JAVA_BASE)) .map(Module::name) .forEach(mn -> builder.requires(mn)); return builder.build(); } private Graph<Module> buildReducedGraph() { ModuleGraphBuilder rpBuilder = new ModuleGraphBuilder(configuration); rpBuilder.addModule(root); requiresTransitive.stream() .forEach(m -> rpBuilder.addEdge(root, m)); // requires transitive graph Graph<Module> rbg = rpBuilder.build().reduce(); ModuleGraphBuilder gb = new ModuleGraphBuilder(configuration); gb.addModule(root); requires.stream() .forEach(m -> gb.addEdge(root, m)); // transitive reduction Graph<Module> newGraph = gb.buildGraph().reduce(rbg); if (DEBUG) { System.err.println("after transitive reduction: "); newGraph.printGraph(log); } return newGraph; } /** * Apply the transitive reduction on the module graph * and returns the corresponding ModuleDescriptor */ ModuleDescriptor reduced() { Graph<Module> g = buildReducedGraph(); return descriptor(requiresTransitive, g.adjacentNodes(root)); } /** * Apply transitive reduction on the resulting graph and reports * recommended requires. */ private void analyzeDeps() { printModuleDescriptor(log, root); ModuleDescriptor analyzedDescriptor = descriptor(); if (!matches(root.descriptor(), analyzedDescriptor)) { log.format(" [Suggested module descriptor for %s]%n", root.name()); analyzedDescriptor.requires() .stream() .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) .forEach(req -> log.format(" requires %s;%n", req)); } ModuleDescriptor reduced = reduced(); if (!matches(root.descriptor(), reduced)) { log.format(" [Transitive reduced graph for %s]%n", root.name()); reduced.requires() .stream() .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) .forEach(req -> log.format(" requires %s;%n", req)); } checkQualifiedExports(); log.println(); } private void checkQualifiedExports() { // detect any qualified exports not used by the target module unusedQualifiedExports = unusedQualifiedExports(); if (!unusedQualifiedExports.isEmpty()) log.format(" [Unused qualified exports in %s]%n", root.name()); unusedQualifiedExports.keySet().stream() .sorted() .forEach(pn -> log.format(" exports %s to %s%n", pn, unusedQualifiedExports.get(pn).stream() .sorted() .collect(joining(",")))); } private void printModuleDescriptor(PrintWriter out, Module module) { ModuleDescriptor descriptor = module.descriptor(); out.format("%s (%s)%n", descriptor.name(), module.location()); if (descriptor.name().equals(JAVA_BASE)) return; out.println(" [Module descriptor]"); descriptor.requires() .stream() .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) .forEach(req -> out.format(" requires %s;%n", req)); } /** * Detects any qualified exports not used by the target module. */ private Map<String, Set<String>> unusedQualifiedExports() { Map<String, Set<String>> unused = new HashMap<>(); // build the qualified exports map Map<String, Set<String>> qualifiedExports = root.exports().entrySet().stream() .filter(e -> !e.getValue().isEmpty()) .map(Map.Entry::getKey) .collect(toMap(Function.identity(), _k -> new HashSet<>())); Set<Module> mods = new HashSet<>(); root.exports().values() .stream() .flatMap(Set::stream) .forEach(target -> configuration.findModule(target) .ifPresentOrElse(mods::add, () -> log.format("Warning: %s not found%n", target)) ); // parse all target modules dependencyFinder.parse(mods.stream()); // adds to the qualified exports map if a module references it mods.stream().forEach(m -> m.getDependencies() .map(Dependency.Location::getPackageName) .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(toSet()); if (!staleQualifiedExports.isEmpty()) { for (String pn : staleQualifiedExports) { Set<String> targets = new HashSet<>(root.exports().get(pn)); targets.removeAll(qualifiedExports.get(pn)); unused.put(pn, targets); } } return unused; } } private boolean matches(ModuleDescriptor md, ModuleDescriptor other) { // build requires transitive from ModuleDescriptor Set<ModuleDescriptor.Requires> reqTransitive = md.requires().stream() .filter(req -> req.modifiers().contains(TRANSITIVE)) .collect(toSet()); Set<ModuleDescriptor.Requires> otherReqTransitive = other.requires().stream() .filter(req -> req.modifiers().contains(TRANSITIVE)) .collect(toSet()); if (!reqTransitive.equals(otherReqTransitive)) { trace("mismatch requires transitive: %s%n", reqTransitive); return false; } Set<ModuleDescriptor.Requires> unused = md.requires().stream() .filter(req -> !other.requires().contains(req)) .collect(Collectors.toSet()); if (!unused.isEmpty()) { trace("mismatch requires: %s%n", unused); return false; } return true; } /** * Generate dotfile from module descriptor * * @param dir output directory */ public boolean genDotFiles(Path dir) throws IOException { Files.createDirectories(dir); for (Module m : modules.keySet()) { genDotFile(dir, m.name()); } return true; } private void genDotFile(Path dir, String name) throws IOException { try (OutputStream os = Files.newOutputStream(dir.resolve(name + ".dot")); PrintWriter out = new PrintWriter(os)) { Set<Module> modules = configuration.resolve(Set.of(name)) .collect(Collectors.toSet()); // transitive reduction Graph<String> graph = gengraph(modules); out.format("digraph \"%s\" {%n", name); DotGraph.printAttributes(out); DotGraph.printNodes(out, graph); modules.stream() .map(Module::descriptor) .sorted(Comparator.comparing(ModuleDescriptor::name)) .forEach(md -> { String mn = md.name(); Set<String> requiresTransitive = md.requires().stream() .filter(d -> d.modifiers().contains(TRANSITIVE)) .map(d -> d.name()) .collect(toSet()); DotGraph.printEdges(out, graph, mn, requiresTransitive); }); out.println("}"); } } /** * Returns a Graph of the given Configuration after transitive reduction. * * Transitive reduction of requires transitive edge and requires edge have * to be applied separately to prevent the requires transitive 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(Set<Module> modules) { // build a Graph containing only requires transitive edges // with transitive reduction. Graph.Builder<String> rpgbuilder = new Graph.Builder<>(); for (Module module : modules) { ModuleDescriptor md = module.descriptor(); String mn = md.name(); md.requires().stream() .filter(d -> d.modifiers().contains(TRANSITIVE)) .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 (Module module : modules) { ModuleDescriptor md = module.descriptor(); String mn = md.name(); builder.addNode(mn); configuration.reads(module) .map(Module::name) .forEach(d -> builder.addEdge(mn, d)); } // transitive reduction of requires edges return builder.build().reduce(rpg); } // ---- for testing purpose public ModuleDescriptor[] descriptors(String name) { ModuleDeps moduleDeps = modules.keySet().stream() .filter(m -> m.name().equals(name)) .map(modules::get) .findFirst().get(); ModuleDescriptor[] descriptors = new ModuleDescriptor[3]; descriptors[0] = moduleDeps.root.descriptor(); descriptors[1] = moduleDeps.descriptor(); descriptors[2] = moduleDeps.reduced(); return descriptors; } public Map<String, Set<String>> unusedQualifiedExports(String name) { ModuleDeps moduleDeps = modules.keySet().stream() .filter(m -> m.name().equals(name)) .map(modules::get) .findFirst().get(); return moduleDeps.unusedQualifiedExports; } }