ModuleAnalyzer.java revision 3792:d516975e8110
1321369Sdim/* 2218885Sdim * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. 3353358Sdim * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4353358Sdim * 5353358Sdim * This code is free software; you can redistribute it and/or modify it 6218885Sdim * under the terms of the GNU General Public License version 2 only, as 7218885Sdim * published by the Free Software Foundation. Oracle designates this 8218885Sdim * particular file as subject to the "Classpath" exception as provided 9218885Sdim * by Oracle in the LICENSE file that accompanied this code. 10218885Sdim * 11218885Sdim * This code is distributed in the hope that it will be useful, but WITHOUT 12234353Sdim * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13321369Sdim * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14341825Sdim * version 2 for more details (a copy is included in the LICENSE file that 15344779Sdim * accompanied this code). 16321369Sdim * 17309124Sdim * You should have received a copy of the GNU General Public License version 18321369Sdim * 2 along with this work; if not, write to the Free Software Foundation, 19234353Sdim * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20218885Sdim * 21218885Sdim * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22321369Sdim * or visit www.oracle.com if you need additional information or have any 23234353Sdim * questions. 24309124Sdim */ 25234353Sdimpackage com.sun.tools.jdeps; 26234353Sdim 27234353Sdimimport static com.sun.tools.jdeps.Graph.*; 28288943Sdimimport static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER; 29234353Sdimimport static com.sun.tools.jdeps.Module.*; 30234353Sdimimport static java.lang.module.ModuleDescriptor.Requires.Modifier.*; 31288943Sdimimport static java.util.stream.Collectors.*; 32288943Sdim 33288943Sdimimport com.sun.tools.classfile.Dependency; 34288943Sdimimport com.sun.tools.jdeps.JdepsTask.BadArgs; 35288943Sdim 36309124Sdimimport java.io.IOException; 37309124Sdimimport java.io.OutputStream; 38288943Sdimimport java.io.PrintWriter; 39288943Sdimimport java.lang.module.ModuleDescriptor; 40309124Sdimimport java.nio.file.Files; 41309124Sdimimport java.nio.file.Path; 42309124Sdimimport java.util.Collections; 43309124Sdimimport java.util.Comparator; 44309124Sdimimport java.util.HashMap; 45309124Sdimimport java.util.HashSet; 46309124Sdimimport java.util.Map; 47309124Sdimimport java.util.Optional; 48309124Sdimimport java.util.Set; 49309124Sdimimport java.util.function.Function; 50321369Sdimimport java.util.stream.Collectors; 51309124Sdimimport java.util.stream.Stream; 52288943Sdim 53288943Sdim/** 54341825Sdim * Analyze module dependences and compare with module descriptor. 55234353Sdim * Also identify any qualified exports not used by the target module. 56353358Sdim */ 57218885Sdimpublic class ModuleAnalyzer { 58218885Sdim private static final String JAVA_BASE = "java.base"; 59218885Sdim 60234353Sdim private final JdepsConfiguration configuration; 61218885Sdim private final PrintWriter log; 62321369Sdim 63353358Sdim private final DependencyFinder dependencyFinder; 64234353Sdim private final Map<Module, ModuleDeps> modules; 65321369Sdim 66321369Sdim public ModuleAnalyzer(JdepsConfiguration config, 67218885Sdim PrintWriter log) { 68341825Sdim this(config, log, Collections.emptySet()); 69341825Sdim } 70341825Sdim public ModuleAnalyzer(JdepsConfiguration config, 71341825Sdim PrintWriter log, 72341825Sdim Set<String> names) { 73234353Sdim this.configuration = config; 74234353Sdim this.log = log; 75288943Sdim 76288943Sdim this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER); 77344779Sdim if (names.isEmpty()) { 78344779Sdim this.modules = configuration.rootModules().stream() 79288943Sdim .collect(toMap(Function.identity(), ModuleDeps::new)); 80234353Sdim } else { 81234353Sdim this.modules = names.stream() 82234353Sdim .map(configuration::findModule) 83234353Sdim .flatMap(Optional::stream) 84234353Sdim .collect(toMap(Function.identity(), ModuleDeps::new)); 85234353Sdim } 86218885Sdim } 87309124Sdim 88309124Sdim public boolean run() throws IOException { 89234353Sdim try { 90288943Sdim // compute "requires transitive" dependences 91276479Sdim modules.values().forEach(ModuleDeps::computeRequiresTransitive); 92234353Sdim 93288943Sdim modules.values().forEach(md -> { 94288943Sdim // compute "requires" dependences 95288943Sdim md.computeRequires(); 96344779Sdim // apply transitive reduction and reports recommended requires. 97344779Sdim md.analyzeDeps(); 98288943Sdim }); 99218885Sdim } finally { 100243830Sdim dependencyFinder.shutdown(); 101353358Sdim } 102243830Sdim return true; 103243830Sdim } 104234353Sdim 105218885Sdim class ModuleDeps { 106239462Sdim final Module root; 107239462Sdim Set<Module> requiresTransitive; 108239462Sdim Set<Module> requires; 109239462Sdim Map<String, Set<String>> unusedQualifiedExports; 110239462Sdim 111239462Sdim ModuleDeps(Module root) { 112239462Sdim this.root = root; 113239462Sdim } 114239462Sdim 115239462Sdim /** 116239462Sdim * Compute 'requires transitive' dependences by analyzing API dependencies 117239462Sdim */ 118239462Sdim private void computeRequiresTransitive() { 119276479Sdim // record requires transitive 120239462Sdim this.requiresTransitive = computeRequires(true) 121239462Sdim .filter(m -> !m.name().equals(JAVA_BASE)) 122276479Sdim .collect(toSet()); 123239462Sdim 124239462Sdim trace("requires transitive: %s%n", requiresTransitive); 125276479Sdim } 126239462Sdim 127239462Sdim private void computeRequires() { 128276479Sdim this.requires = computeRequires(false).collect(toSet()); 129239462Sdim trace("requires: %s%n", requires); 130239462Sdim } 131239462Sdim 132239462Sdim private Stream<Module> computeRequires(boolean apionly) { 133239462Sdim // analyze all classes 134239462Sdim 135239462Sdim if (apionly) { 136239462Sdim dependencyFinder.parseExportedAPIs(Stream.of(root)); 137239462Sdim } else { 138239462Sdim dependencyFinder.parse(Stream.of(root)); 139239462Sdim } 140239462Sdim 141239462Sdim // find the modules of all the dependencies found 142239462Sdim return dependencyFinder.getDependences(root) 143239462Sdim .map(Archive::getModule); 144239462Sdim } 145218885Sdim 146218885Sdim ModuleDescriptor descriptor() { 147341825Sdim return descriptor(requiresTransitive, requires); 148218885Sdim } 149218885Sdim 150218885Sdim private ModuleDescriptor descriptor(Set<Module> requiresTransitive, 151218885Sdim Set<Module> requires) { 152327952Sdim 153327952Sdim ModuleDescriptor.Builder builder = ModuleDescriptor.module(root.name()); 154327952Sdim 155218885Sdim if (!root.name().equals(JAVA_BASE)) 156341825Sdim builder.requires(Set.of(MANDATED), JAVA_BASE); 157341825Sdim 158341825Sdim requiresTransitive.stream() 159341825Sdim .filter(m -> !m.name().equals(JAVA_BASE)) 160341825Sdim .map(Module::name) 161321369Sdim .forEach(mn -> builder.requires(Set.of(TRANSITIVE), mn)); 162321369Sdim 163321369Sdim 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