ModuleAnalyzer.java revision 3976:65d446c80cdf
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 */ 25package com.sun.tools.jdeps; 26 27import static com.sun.tools.jdeps.Graph.*; 28import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER; 29import static com.sun.tools.jdeps.Module.*; 30import static java.lang.module.ModuleDescriptor.Requires.Modifier.*; 31import static java.util.stream.Collectors.*; 32 33import com.sun.tools.classfile.Dependency; 34import com.sun.tools.jdeps.JdepsTask.BadArgs; 35 36import java.io.IOException; 37import java.io.OutputStream; 38import java.io.PrintWriter; 39import java.lang.module.ModuleDescriptor; 40import java.nio.file.Files; 41import java.nio.file.Path; 42import java.util.Collections; 43import java.util.Comparator; 44import java.util.HashMap; 45import java.util.HashSet; 46import java.util.Map; 47import java.util.Optional; 48import java.util.Set; 49import java.util.function.Function; 50import java.util.stream.Collectors; 51import 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 */ 57public 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.newModule(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} 440