/*
 * 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.
/** * 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, 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
/** * 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.newModule(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
/** * 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.
/** * 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.
/** * 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; } // ---- 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; } }