1 /*
   2  * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.sun.tools.jdeps;
  26 
  27 import static com.sun.tools.jdeps.Graph.*;
  28 import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER;
  29 import static com.sun.tools.jdeps.Module.*;
  30 import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
  31 import static java.util.stream.Collectors.*;
  32 
  33 import com.sun.tools.classfile.Dependency;
  34 import com.sun.tools.jdeps.JdepsTask.BadArgs;
  35 
  36 import java.io.IOException;
  37 import java.io.OutputStream;
  38 import java.io.PrintWriter;
  39 import java.lang.module.ModuleDescriptor;
  40 import java.nio.file.Files;
  41 import java.nio.file.Path;
  42 import java.util.Collections;
  43 import java.util.Comparator;
  44 import java.util.HashMap;
  45 import java.util.HashSet;
  46 import java.util.Map;
  47 import java.util.Optional;
  48 import java.util.Set;
  49 import java.util.function.Function;
  50 import java.util.stream.Collectors;
  51 import java.util.stream.Stream;
  52 
  53 /**
  54  * Analyze module dependences and compare with module descriptor.
  55  * Also identify any qualified exports not used by the target module.
  56  */
  57 public class ModuleAnalyzer {
  58     private static final String JAVA_BASE = "java.base";
  59 
  60     private final JdepsConfiguration configuration;
  61     private final PrintWriter log;
  62 
  63     private final DependencyFinder dependencyFinder;
  64     private final Map<Module, ModuleDeps> modules;
  65 
  66     public ModuleAnalyzer(JdepsConfiguration config,
  67                           PrintWriter log) {
  68         this(config, log, Collections.emptySet());
  69     }
  70     public ModuleAnalyzer(JdepsConfiguration config,
  71                           PrintWriter log,
  72                           Set<String> names) {
  73         this.configuration = config;
  74         this.log = log;
  75 
  76         this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER);
  77         if (names.isEmpty()) {
  78             this.modules = configuration.rootModules().stream()
  79                 .collect(toMap(Function.identity(), ModuleDeps::new));
  80         } else {
  81             this.modules = names.stream()
  82                 .map(configuration::findModule)
  83                 .flatMap(Optional::stream)
  84                 .collect(toMap(Function.identity(), ModuleDeps::new));
  85         }
  86     }
  87 
  88     public boolean run() throws IOException {
  89         try {
  90             // compute "requires transitive" dependences
  91             modules.values().forEach(ModuleDeps::computeRequiresTransitive);
  92 
  93             modules.values().forEach(md -> {
  94                 // compute "requires" dependences
  95                 md.computeRequires();
  96                 // apply transitive reduction and reports recommended requires.
  97                 md.analyzeDeps();
  98             });
  99         } finally {
 100             dependencyFinder.shutdown();
 101         }
 102         return true;
 103     }
 104 
 105     class ModuleDeps {
 106         final Module root;
 107         Set<Module> requiresTransitive;
 108         Set<Module> requires;
 109         Map<String, Set<String>> unusedQualifiedExports;
 110 
 111         ModuleDeps(Module root) {
 112             this.root = root;
 113         }
 114 
 115         /**
 116          * Compute 'requires transitive' dependences by analyzing API dependencies
 117          */
 118         private void computeRequiresTransitive() {
 119             // record requires transitive
 120             this.requiresTransitive = computeRequires(true)
 121                 .filter(m -> !m.name().equals(JAVA_BASE))
 122                 .collect(toSet());
 123 
 124             trace("requires transitive: %s%n", requiresTransitive);
 125         }
 126 
 127         private void computeRequires() {
 128             this.requires = computeRequires(false).collect(toSet());
 129             trace("requires: %s%n", requires);
 130         }
 131 
 132         private Stream<Module> computeRequires(boolean apionly) {
 133             // analyze all classes
 134 
 135             if (apionly) {
 136                 dependencyFinder.parseExportedAPIs(Stream.of(root));
 137             } else {
 138                 dependencyFinder.parse(Stream.of(root));
 139             }
 140 
 141             // find the modules of all the dependencies found
 142             return dependencyFinder.getDependences(root)
 143                         .map(Archive::getModule);
 144         }
 145 
 146         ModuleDescriptor descriptor() {
 147             return descriptor(requiresTransitive, requires);
 148         }
 149 
 150         private ModuleDescriptor descriptor(Set<Module> requiresTransitive,
 151                                             Set<Module> requires) {
 152 
 153             ModuleDescriptor.Builder builder = ModuleDescriptor.module(root.name());
 154 
 155             if (!root.name().equals(JAVA_BASE))
 156                 builder.requires(Set.of(MANDATED), JAVA_BASE);
 157 
 158             requiresTransitive.stream()
 159                 .filter(m -> !m.name().equals(JAVA_BASE))
 160                 .map(Module::name)
 161                 .forEach(mn -> builder.requires(Set.of(TRANSITIVE), mn));
 162 
 163             requires.stream()
 164                 .filter(m -> !requiresTransitive.contains(m))
 165                 .filter(m -> !m.name().equals(JAVA_BASE))
 166                 .map(Module::name)
 167                 .forEach(mn -> builder.requires(mn));
 168 
 169             return builder.build();
 170         }
 171 
 172         private Graph<Module> buildReducedGraph() {
 173             ModuleGraphBuilder rpBuilder = new ModuleGraphBuilder(configuration);
 174             rpBuilder.addModule(root);
 175             requiresTransitive.stream()
 176                           .forEach(m -> rpBuilder.addEdge(root, m));
 177 
 178             // requires transitive graph
 179             Graph<Module> rbg = rpBuilder.build().reduce();
 180 
 181             ModuleGraphBuilder gb = new ModuleGraphBuilder(configuration);
 182             gb.addModule(root);
 183             requires.stream()
 184                     .forEach(m -> gb.addEdge(root, m));
 185 
 186             // transitive reduction
 187             Graph<Module> newGraph = gb.buildGraph().reduce(rbg);
 188             if (DEBUG) {
 189                 System.err.println("after transitive reduction: ");
 190                 newGraph.printGraph(log);
 191             }
 192             return newGraph;
 193         }
 194 
 195         /**
 196          * Apply the transitive reduction on the module graph
 197          * and returns the corresponding ModuleDescriptor
 198          */
 199         ModuleDescriptor reduced() {
 200             Graph<Module> g = buildReducedGraph();
 201             return descriptor(requiresTransitive, g.adjacentNodes(root));
 202         }
 203 
 204         /**
 205          * Apply transitive reduction on the resulting graph and reports
 206          * recommended requires.
 207          */
 208         private void analyzeDeps() {
 209             printModuleDescriptor(log, root);
 210 
 211             ModuleDescriptor analyzedDescriptor = descriptor();
 212             if (!matches(root.descriptor(), analyzedDescriptor)) {
 213                 log.format("  [Suggested module descriptor for %s]%n", root.name());
 214                 analyzedDescriptor.requires()
 215                     .stream()
 216                     .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
 217                     .forEach(req -> log.format("    requires %s;%n", req));
 218             }
 219 
 220             ModuleDescriptor reduced = reduced();
 221             if (!matches(root.descriptor(), reduced)) {
 222                 log.format("  [Transitive reduced graph for %s]%n", root.name());
 223                 reduced.requires()
 224                     .stream()
 225                     .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
 226                     .forEach(req -> log.format("    requires %s;%n", req));
 227             }
 228 
 229             checkQualifiedExports();
 230             log.println();
 231         }
 232 
 233         private void checkQualifiedExports() {
 234             // detect any qualified exports not used by the target module
 235             unusedQualifiedExports = unusedQualifiedExports();
 236             if (!unusedQualifiedExports.isEmpty())
 237                 log.format("  [Unused qualified exports in %s]%n", root.name());
 238 
 239             unusedQualifiedExports.keySet().stream()
 240                 .sorted()
 241                 .forEach(pn -> log.format("    exports %s to %s%n", pn,
 242                     unusedQualifiedExports.get(pn).stream()
 243                         .sorted()
 244                         .collect(joining(","))));
 245         }
 246 
 247         private void printModuleDescriptor(PrintWriter out, Module module) {
 248             ModuleDescriptor descriptor = module.descriptor();
 249             out.format("%s (%s)%n", descriptor.name(), module.location());
 250 
 251             if (descriptor.name().equals(JAVA_BASE))
 252                 return;
 253 
 254             out.println("  [Module descriptor]");
 255             descriptor.requires()
 256                 .stream()
 257                 .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
 258                 .forEach(req -> out.format("    requires %s;%n", req));
 259         }
 260 
 261 
 262         /**
 263          * Detects any qualified exports not used by the target module.
 264          */
 265         private Map<String, Set<String>> unusedQualifiedExports() {
 266             Map<String, Set<String>> unused = new HashMap<>();
 267 
 268             // build the qualified exports map
 269             Map<String, Set<String>> qualifiedExports =
 270                 root.exports().entrySet().stream()
 271                     .filter(e -> !e.getValue().isEmpty())
 272                     .map(Map.Entry::getKey)
 273                     .collect(toMap(Function.identity(), _k -> new HashSet<>()));
 274 
 275             Set<Module> mods = new HashSet<>();
 276             root.exports().values()
 277                 .stream()
 278                 .flatMap(Set::stream)
 279                 .forEach(target -> configuration.findModule(target)
 280                     .ifPresentOrElse(mods::add,
 281                         () -> log.format("Warning: %s not found%n", target))
 282                 );
 283 
 284             // parse all target modules
 285             dependencyFinder.parse(mods.stream());
 286 
 287             // adds to the qualified exports map if a module references it
 288             mods.stream().forEach(m ->
 289                 m.getDependencies()
 290                     .map(Dependency.Location::getPackageName)
 291                     .filter(qualifiedExports::containsKey)
 292                     .forEach(pn -> qualifiedExports.get(pn).add(m.name())));
 293 
 294             // compare with the exports from ModuleDescriptor
 295             Set<String> staleQualifiedExports =
 296                 qualifiedExports.keySet().stream()
 297                     .filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn)))
 298                     .collect(toSet());
 299 
 300             if (!staleQualifiedExports.isEmpty()) {
 301                 for (String pn : staleQualifiedExports) {
 302                     Set<String> targets = new HashSet<>(root.exports().get(pn));
 303                     targets.removeAll(qualifiedExports.get(pn));
 304                     unused.put(pn, targets);
 305                 }
 306             }
 307             return unused;
 308         }
 309     }
 310 
 311     private boolean matches(ModuleDescriptor md, ModuleDescriptor other) {
 312         // build requires transitive from ModuleDescriptor
 313         Set<ModuleDescriptor.Requires> reqTransitive = md.requires().stream()
 314             .filter(req -> req.modifiers().contains(TRANSITIVE))
 315             .collect(toSet());
 316         Set<ModuleDescriptor.Requires> otherReqTransitive = other.requires().stream()
 317             .filter(req -> req.modifiers().contains(TRANSITIVE))
 318             .collect(toSet());
 319 
 320         if (!reqTransitive.equals(otherReqTransitive)) {
 321             trace("mismatch requires transitive: %s%n", reqTransitive);
 322             return false;
 323         }
 324 
 325         Set<ModuleDescriptor.Requires> unused = md.requires().stream()
 326             .filter(req -> !other.requires().contains(req))
 327             .collect(Collectors.toSet());
 328 
 329         if (!unused.isEmpty()) {
 330             trace("mismatch requires: %s%n", unused);
 331             return false;
 332         }
 333         return true;
 334     }
 335 
 336     /**
 337      * Generate dotfile from module descriptor
 338      *
 339      * @param dir output directory
 340      */
 341     public boolean genDotFiles(Path dir) throws IOException {
 342         Files.createDirectories(dir);
 343         for (Module m : modules.keySet()) {
 344             genDotFile(dir, m.name());
 345         }
 346         return true;
 347     }
 348 
 349 
 350     private void genDotFile(Path dir, String name) throws IOException {
 351         try (OutputStream os = Files.newOutputStream(dir.resolve(name + ".dot"));
 352              PrintWriter out = new PrintWriter(os)) {
 353             Set<Module> modules = configuration.resolve(Set.of(name))
 354                 .collect(Collectors.toSet());
 355 
 356             // transitive reduction
 357             Graph<String> graph = gengraph(modules);
 358 
 359             out.format("digraph \"%s\" {%n", name);
 360             DotGraph.printAttributes(out);
 361             DotGraph.printNodes(out, graph);
 362 
 363             modules.stream()
 364                 .map(Module::descriptor)
 365                 .sorted(Comparator.comparing(ModuleDescriptor::name))
 366                 .forEach(md -> {
 367                     String mn = md.name();
 368                     Set<String> requiresTransitive = md.requires().stream()
 369                         .filter(d -> d.modifiers().contains(TRANSITIVE))
 370                         .map(d -> d.name())
 371                         .collect(toSet());
 372 
 373                     DotGraph.printEdges(out, graph, mn, requiresTransitive);
 374                 });
 375 
 376             out.println("}");
 377         }
 378     }
 379 
 380     /**
 381      * Returns a Graph of the given Configuration after transitive reduction.
 382      *
 383      * Transitive reduction of requires transitive edge and requires edge have
 384      * to be applied separately to prevent the requires transitive edges
 385      * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V)
 386      * in which  V would not be re-exported from U.
 387      */
 388     private Graph<String> gengraph(Set<Module> modules) {
 389         // build a Graph containing only requires transitive edges
 390         // with transitive reduction.
 391         Graph.Builder<String> rpgbuilder = new Graph.Builder<>();
 392         for (Module module : modules) {
 393             ModuleDescriptor md = module.descriptor();
 394             String mn = md.name();
 395             md.requires().stream()
 396                     .filter(d -> d.modifiers().contains(TRANSITIVE))
 397                     .map(d -> d.name())
 398                     .forEach(d -> rpgbuilder.addEdge(mn, d));
 399         }
 400 
 401         Graph<String> rpg = rpgbuilder.build().reduce();
 402 
 403         // build the readability graph
 404         Graph.Builder<String> builder = new Graph.Builder<>();
 405         for (Module module : modules) {
 406             ModuleDescriptor md = module.descriptor();
 407             String mn = md.name();
 408             builder.addNode(mn);
 409             configuration.reads(module)
 410                     .map(Module::name)
 411                     .forEach(d -> builder.addEdge(mn, d));
 412         }
 413 
 414         // transitive reduction of requires edges
 415         return builder.build().reduce(rpg);
 416     }
 417 
 418     // ---- for testing purpose
 419     public ModuleDescriptor[] descriptors(String name) {
 420         ModuleDeps moduleDeps = modules.keySet().stream()
 421             .filter(m -> m.name().equals(name))
 422             .map(modules::get)
 423             .findFirst().get();
 424 
 425         ModuleDescriptor[] descriptors = new ModuleDescriptor[3];
 426         descriptors[0] = moduleDeps.root.descriptor();
 427         descriptors[1] = moduleDeps.descriptor();
 428         descriptors[2] = moduleDeps.reduced();
 429         return descriptors;
 430     }
 431 
 432     public Map<String, Set<String>> unusedQualifiedExports(String name) {
 433         ModuleDeps moduleDeps = modules.keySet().stream()
 434             .filter(m -> m.name().equals(name))
 435             .map(modules::get)
 436             .findFirst().get();
 437         return moduleDeps.unusedQualifiedExports;
 438     }
 439 }