ModuleAnalyzer.java revision 3294:9adfb22ff08f
152403Sbillf/* 252403Sbillf * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. 3113091Sobrien * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4113091Sobrien * 552403Sbillf * This code is free software; you can redistribute it and/or modify it 652403Sbillf * under the terms of the GNU General Public License version 2 only, as 752403Sbillf * 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 java.io.IOException; 28import java.io.PrintStream; 29import java.io.UncheckedIOException; 30 31import static java.lang.module.ModuleDescriptor.Requires.Modifier.*; 32 33import java.lang.module.Configuration; 34import java.lang.module.ModuleDescriptor; 35import java.lang.module.ModuleFinder; 36import java.lang.module.ModuleReference; 37import java.lang.module.ResolvedModule; 38import java.nio.file.Files; 39import java.nio.file.Path; 40import java.util.Comparator; 41import java.util.Deque; 42import java.util.HashMap; 43import java.util.HashSet; 44import java.util.LinkedList; 45import java.util.Map; 46import java.util.Set; 47import java.util.function.Consumer; 48import java.util.function.Function; 49import java.util.function.Predicate; 50import java.util.stream.Collectors; 51import java.util.stream.Stream; 52 53import static com.sun.tools.jdeps.Module.*; 54import static com.sun.tools.jdeps.ModulePaths.SystemModulePath.JAVA_BASE; 55 56/** 57 * Analyze module dependences and compare with module descriptor. 58 * Also identify any qualified exports not used by the target module. 59 */ 60class ModuleAnalyzer { 61 private final ModulePaths modulePaths; 62 private final DependencyFinder dependencyFinder; 63 private final Module root; 64 private final Set<Module> modules; 65 private final Set<String> requiresPublic = new HashSet<>(); 66 private final Set<String> requires = new HashSet<>(); 67 private final Set<Module> exportTargets = new HashSet<>(); 68 private final JdepsFilter filter; 69 private Graph<Module> graph; 70 ModuleAnalyzer(ModulePaths modulePaths, DependencyFinder finder, 71 String moduleName) { 72 this.modulePaths = modulePaths; 73 this.dependencyFinder = finder; 74 this.root = modulePaths.getModules().get(moduleName); 75 this.modules = modulePaths.dependences(moduleName); 76 77 root.exports().values().stream() 78 .flatMap(Set::stream) 79 .map(target -> modulePaths.getModules().get(target)) 80 .forEach(this.exportTargets::add); 81 82 this.filter = new JdepsFilter.Builder().filter(true, true).build(); 83 } 84 85 /** 86 * Returns a graph of transitive closure of the given modules. 87 * 88 * This method does not add the implicit read edges and is intended to 89 * get all transitive closures in (reverse) topological sort. 90 */ 91 public static Graph<Module> graph(ModulePaths modulePaths, Module... modules) { 92 Graph.Builder<Module> gb = new Graph.Builder<>(); 93 for (Module module : modules) { 94 module.descriptor().requires().stream() 95 .map(ModuleDescriptor.Requires::name) 96 .map(mn -> modulePaths.getModules().get(mn)) 97 .forEach(m -> { 98 gb.addNode(m); 99 gb.addEdge(module, m); 100 }); 101 } 102 return gb.build(); 103 } 104 105 /** 106 * Do the analysis 107 */ 108 public boolean run() { 109 try { 110 computeRequiresPublic(); 111 computeRequires(); 112 // apply transitive reduction and reports recommended requires. 113 analyzeDeps(); 114 // detect any qualiifed exports not used by the target module 115 checkQualifiedExports(); 116 } catch (IOException e) { 117 throw new UncheckedIOException(e); 118 } 119 return true; 120 } 121 122 /** 123 * Compute 'requires public' dependences by analyzing API dependencies 124 */ 125 private void computeRequiresPublic() throws IOException { 126 JdepsFilter.Builder builder = new JdepsFilter.Builder(); 127 // only analyze exported API 128 root.descriptor.exports().stream() 129 .filter(exp -> !exp.isQualified()) 130 .map(ModuleDescriptor.Exports::source) 131 .forEach(builder::includePackage); 132 133 JdepsFilter filter = builder.filter(true, true).build(); 134 135 // analyze dependences for exported packages 136 dependencyFinder.findDependencies(filter, true /* api only */, 1); 137 Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter); 138 analyzer.run(modules); 139 140 // record requires public 141 analyzer.requires(root) 142 .filter(m -> m != JAVA_BASE && m != root) 143 .map(Archive::getName) 144 .forEach(requiresPublic::add); 145 trace("requires public: %s%n", requiresPublic); 146 } 147 148 private void computeRequires() throws IOException { 149 // add the exportTargets of the qualified exports to the root set 150 exportTargets.stream() 151 .peek(target -> trace("add root: %s%n", target)) 152 .forEach(dependencyFinder::addRoot); 153 154 // analyze all classes 155 dependencyFinder.findDependencies(filter, false /* all classes */, 1); 156 Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter); 157 analyzer.run(modules); 158 159 // record requires 160 analyzer.requires(root) 161 .filter(m -> m != JAVA_BASE && m != root) 162 .map(Archive::getName) 163 .forEach(requires::add); 164 165 this.graph = buildGraph(analyzer, root); 166 if (traceOn) { 167 trace("dependences: %s%n", graph.nodes()); 168 graph.printGraph(System.out); 169 } 170 } 171 172 /** 173 * Apply transitive reduction on the resulting graph and reports 174 * recommended requires. 175 */ 176 private void analyzeDeps() { 177 String moduleName = root.name(); 178 179 Graph.Builder<String> builder = new Graph.Builder<>(); 180 requiresPublic.stream() 181 .forEach(mn -> { 182 builder.addNode(mn); 183 builder.addEdge(moduleName, mn); 184 }); 185 // requires public graph 186 Graph<String> rbg = builder.build().reduce(); 187 188 // convert the dependence graph from Module to name 189 Set<String> nodes = this.graph.nodes().stream() 190 .map(Module::name) 191 .collect(Collectors.toSet()); 192 Map<String, Set<String>> edges = new HashMap<>(); 193 this.graph.edges().keySet().stream() 194 .forEach(m -> { 195 String mn = m.name(); 196 Set<String> es = edges.computeIfAbsent(mn, _k -> new HashSet<String>()); 197 this.graph.edges().get(m).stream() 198 .map(Module::name) 199 .forEach(es::add); 200 }); 201 202 // transitive reduction 203 Graph<String> newGraph = new Graph<>(nodes, edges).reduce(rbg); 204 if (traceOn) { 205 System.out.println("after transitive reduction"); 206 newGraph.printGraph(System.out); 207 }; 208 209 Set<String> reducedRequires = newGraph.adjacentNodes(moduleName); 210 if (matches(root.descriptor(), requires, requiresPublic) && 211 matches(root.descriptor(), reducedRequires, requiresPublic)) { 212 System.out.println("--- Analysis result: no change for " + root.name()); 213 } else { 214 System.out.println("--- Analysis result: suggested requires for " + root.name()); 215 System.out.format("module %s%n", root.name()); 216 requires.stream() 217 .sorted() 218 .forEach(dn -> System.out.format(" requires %s%s;%n", 219 requiresPublic.contains(dn) ? "public " : "", dn)); 220 if (!requires.equals(reducedRequires) && !reducedRequires.isEmpty()) { 221 System.out.format("%nmodule %s (reduced)%n", root.name()); 222 newGraph.adjacentNodes(moduleName) 223 .stream() 224 .sorted() 225 .forEach(dn -> System.out.format(" requires %s%s;%n", 226 requiresPublic.contains(dn) ? "public " : "", dn)); 227 } 228 System.out.println("\n--- Module descriptor"); 229 Graph<Module> mdGraph = graph(modulePaths, root); 230 mdGraph.reverse(m -> printModuleDescriptor(System.out, m.descriptor())); 231 } 232 } 233 234 /** 235 * Detects any qualified exports not used by the target module. 236 */ 237 private void checkQualifiedExports() throws IOException { 238 Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter); 239 analyzer.run(dependencyFinder.roots()); 240 241 // build the qualified exports map 242 Map<String, Set<String>> qualifiedExports = 243 root.exports().entrySet().stream() 244 .filter(e -> !e.getValue().isEmpty()) 245 .map(Map.Entry::getKey) 246 .collect(Collectors.toMap(Function.identity(), _k -> new HashSet<>())); 247 248 // adds to the qualified exports map if a module references it 249 for (Module m : exportTargets) { 250 analyzer.dependences(m).stream() 251 .map(this::toPackageName) 252 .filter(qualifiedExports::containsKey) 253 .forEach(pn -> qualifiedExports.get(pn).add(m.name())); 254 } 255 256 // compare with the exports from ModuleDescriptor 257 Set<String> staleQualifiedExports = 258 qualifiedExports.keySet().stream() 259 .filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn))) 260 .collect(Collectors.toSet()); 261 262 if (!staleQualifiedExports.isEmpty()) { 263 System.out.println("--- Unused qualified exports in " + root.name()); 264 for (String pn : staleQualifiedExports) { 265 Set<String> unused = new HashSet<>(root.exports().get(pn)); 266 unused.removeAll(qualifiedExports.get(pn)); 267 System.out.format(" exports %s to %s%n", pn, 268 unused.stream().collect(Collectors.joining(","))); 269 } 270 } 271 } 272 273 private String toPackageName(String cn) { 274 int i = cn.lastIndexOf('.'); 275 return i > 0 ? cn.substring(0, i) : ""; 276 } 277 278 private boolean matches(ModuleDescriptor md, Set<String> requires, Set<String> requiresPublic) { 279 Set<String> reqPublic = md.requires().stream() 280 .filter(req -> req.modifiers().contains(PUBLIC)) 281 .map(ModuleDescriptor.Requires::name) 282 .collect(Collectors.toSet()); 283 if (!requiresPublic.equals(reqPublic)) { 284 trace("mismatch requires public: %s%n", reqPublic); 285 return false; 286 } 287 // java.base is not in requires 288 int javaBase = md.name().equals(JAVA_BASE.name()) ? 0 : 1; 289 if (requires.size()+javaBase != md.requires().size()) { 290 trace("mismatch requires: %d != %d%n", requires.size()+1, md.requires().size()); 291 return false; 292 } 293 294 Set<String> unused = md.requires().stream() 295 .map(ModuleDescriptor.Requires::name) 296 .filter(req -> !requires.contains(req) && !req.equals(JAVA_BASE.name())) 297 .collect(Collectors.toSet()); 298 if (!unused.isEmpty()) { 299 trace("mismatch requires: %s%n", unused); 300 return false; 301 } 302 return true; 303 } 304 305 private void printModuleDescriptor(PrintStream out, ModuleDescriptor descriptor) { 306 if (descriptor.name().equals("java.base")) 307 return; 308 309 out.format("module %s%n", descriptor.name()); 310 descriptor.requires() 311 .stream() 312 .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) 313 .forEach(req -> out.format(" requires %s;%n", req)); 314 } 315 316 /** 317 * Returns a graph of modules required by the specified module. 318 * 319 * Requires public edges of the dependences are added to the graph. 320 */ 321 private Graph<Module> buildGraph(Analyzer analyzer, Module module) { 322 Graph.Builder<Module> builder = new Graph.Builder<>(); 323 builder.addNode(module); 324 Set<Module> visited = new HashSet<>(); 325 visited.add(module); 326 Deque<Module> deque = new LinkedList<>(); 327 analyzer.requires(module) 328 .map(Archive::getModule) 329 .filter(m -> m != JAVA_BASE) 330 .forEach(m -> { 331 deque.add(m); 332 builder.addEdge(module, m); 333 }); 334 335 // read requires public from ModuleDescription 336 Module source; 337 while ((source = deque.poll()) != null) { 338 if (visited.contains(source)) 339 continue; 340 visited.add(source); 341 builder.addNode(source); 342 Module from = source; 343 source.descriptor().requires().stream() 344 .filter(req -> req.modifiers().contains(PUBLIC)) 345 .map(ModuleDescriptor.Requires::name) 346 .map(req -> modulePaths.getModules().get(req)) 347 .filter(m -> m != JAVA_BASE) 348 .forEach(m -> { 349 deque.add(m); 350 builder.addEdge(from, m); 351 }); 352 } 353 return builder.build(); 354 } 355 356 static class Graph<T> { 357 private final Set<T> nodes; 358 private final Map<T, Set<T>> edges; 359 360 private Graph(Set<T> nodes, Map<T, Set<T>> edges) { 361 this.nodes = nodes; 362 this.edges = edges; 363 } 364 365 public Set<T> nodes() { 366 return nodes; 367 } 368 369 public Map<T, Set<T>> edges() { 370 return edges; 371 } 372 373 public Set<T> adjacentNodes(T u) { 374 return edges.get(u); 375 } 376 377 /** 378 * Returns a new Graph after transitive reduction 379 */ 380 public Graph<T> reduce() { 381 Graph.Builder<T> builder = new Builder<>(); 382 nodes.stream() 383 .forEach(u -> { 384 builder.addNode(u); 385 edges.get(u).stream() 386 .filter(v -> !pathExists(u, v, false)) 387 .forEach(v -> builder.addEdge(u, v)); 388 }); 389 return builder.build(); 390 } 391 392 /** 393 * Returns a new Graph after transitive reduction. All edges in 394 * the given g takes precedence over this graph. 395 * 396 * @throw IllegalArgumentException g must be a subgraph this graph 397 */ 398 public Graph<T> reduce(Graph<T> g) { 399 boolean subgraph = nodes.containsAll(g.nodes) && g.edges.keySet().stream() 400 .allMatch(u -> adjacentNodes(u).containsAll(g.adjacentNodes(u))); 401 if (!subgraph) { 402 throw new IllegalArgumentException(g + " is not a subgraph of " + this); 403 } 404 405 Graph.Builder<T> builder = new Builder<>(); 406 nodes.stream() 407 .forEach(u -> { 408 builder.addNode(u); 409 // filter the edge if there exists a path from u to v in the given g 410 // or there exists another path from u to v in this graph 411 edges.get(u).stream() 412 .filter(v -> !g.pathExists(u, v) && !pathExists(u, v, false)) 413 .forEach(v -> builder.addEdge(u, v)); 414 }); 415 416 // add the overlapped edges from this graph and the given g 417 g.edges().keySet().stream() 418 .forEach(u -> g.adjacentNodes(u).stream() 419 .filter(v -> isAdjacent(u, v)) 420 .forEach(v -> builder.addEdge(u, v))); 421 return builder.build(); 422 } 423 424 /** 425 * Returns nodes sorted in topological order. 426 */ 427 public Stream<T> orderedNodes() { 428 TopoSorter<T> sorter = new TopoSorter<>(this); 429 return sorter.result.stream(); 430 } 431 432 /** 433 * Iterates the nodes sorted in topological order and performs the 434 * given action. 435 */ 436 public void ordered(Consumer<T> action) { 437 TopoSorter<T> sorter = new TopoSorter<>(this); 438 sorter.ordered(action); 439 } 440 441 /** 442 * Iterates the nodes sorted in reverse topological order and 443 * performs the given action. 444 */ 445 public void reverse(Consumer<T> action) { 446 TopoSorter<T> sorter = new TopoSorter<>(this); 447 sorter.reverse(action); 448 } 449 450 private boolean isAdjacent(T u, T v) { 451 return edges.containsKey(u) && edges.get(u).contains(v); 452 } 453 454 private boolean pathExists(T u, T v) { 455 return pathExists(u, v, true); 456 } 457 458 /** 459 * Returns true if there exists a path from u to v in this graph. 460 * If includeAdjacent is false, it returns true if there exists 461 * another path from u to v of distance > 1 462 */ 463 private boolean pathExists(T u, T v, boolean includeAdjacent) { 464 if (!nodes.contains(u) || !nodes.contains(v)) { 465 return false; 466 } 467 if (includeAdjacent && isAdjacent(u, v)) { 468 return true; 469 } 470 Deque<T> stack = new LinkedList<>(); 471 Set<T> visited = new HashSet<>(); 472 stack.push(u); 473 while (!stack.isEmpty()) { 474 T node = stack.pop(); 475 if (node.equals(v)) { 476 return true; 477 } 478 if (!visited.contains(node)) { 479 visited.add(node); 480 edges.get(node).stream() 481 .filter(e -> includeAdjacent || !node.equals(u) || !e.equals(v)) 482 .forEach(e -> stack.push(e)); 483 } 484 } 485 assert !visited.contains(v); 486 return false; 487 } 488 489 void printGraph(PrintStream out) { 490 out.println("graph for " + nodes); 491 nodes.stream() 492 .forEach(u -> adjacentNodes(u).stream() 493 .forEach(v -> out.format("%s -> %s%n", u, v))); 494 } 495 496 @Override 497 public String toString() { 498 return nodes.toString(); 499 } 500 501 static class Builder<T> { 502 final Set<T> nodes = new HashSet<>(); 503 final Map<T, Set<T>> edges = new HashMap<>(); 504 505 public void addNode(T node) { 506 if (nodes.contains(node)) { 507 return; 508 } 509 nodes.add(node); 510 edges.computeIfAbsent(node, _e -> new HashSet<>()); 511 } 512 513 public void addEdge(T u, T v) { 514 addNode(u); 515 addNode(v); 516 edges.get(u).add(v); 517 } 518 519 public Graph<T> build() { 520 return new Graph<>(nodes, edges); 521 } 522 523 void print(PrintStream out) { 524 out.println(nodes); 525 nodes.stream() 526 .forEach(u -> edges.get(u).stream() 527 .forEach(v -> out.format("%s -> %s%n", u, v))); 528 } 529 } 530 } 531 532 static class TopoSorter<T> { 533 final Deque<T> result = new LinkedList<>(); 534 final Deque<T> nodes; 535 final Graph<T> graph; 536 TopoSorter(Graph<T> graph) { 537 this.graph = graph; 538 this.nodes = new LinkedList<>(graph.nodes); 539 sort(); 540 } 541 542 public void ordered(Consumer<T> action) { 543 result.iterator().forEachRemaining(action); 544 } 545 546 public void reverse(Consumer<T> action) { 547 result.descendingIterator().forEachRemaining(action); 548 } 549 550 private void sort() { 551 Deque<T> visited = new LinkedList<>(); 552 Deque<T> done = new LinkedList<>(); 553 T node; 554 while ((node = nodes.poll()) != null) { 555 if (!visited.contains(node)) { 556 visit(node, visited, done); 557 } 558 } 559 } 560 561 private void visit(T node, Deque<T> visited, Deque<T> done) { 562 if (visited.contains(node)) { 563 if (!done.contains(node)) { 564 throw new IllegalArgumentException("Cyclic detected: " + 565 node + " " + graph.edges().get(node)); 566 } 567 return; 568 } 569 visited.add(node); 570 graph.edges().get(node).stream() 571 .forEach(x -> visit(x, visited, done)); 572 done.add(node); 573 result.addLast(node); 574 } 575 } 576 577 static class DotGraph { 578 static final String ORANGE = "#e76f00"; 579 static final String BLUE = "#437291"; 580 static final String GRAY = "#dddddd"; 581 582 static final String REEXPORTS = ""; 583 static final String REQUIRES = "style=\"dashed\""; 584 static final String REQUIRES_BASE = "color=\"" + GRAY + "\""; 585 586 static final Set<String> javaModules = modules(name -> 587 (name.startsWith("java.") && !name.equals("java.smartcardio"))); 588 static final Set<String> jdkModules = modules(name -> 589 (name.startsWith("java.") || 590 name.startsWith("jdk.") || 591 name.startsWith("javafx.")) && !javaModules.contains(name)); 592 593 private static Set<String> modules(Predicate<String> predicate) { 594 return ModuleFinder.ofSystem().findAll() 595 .stream() 596 .map(ModuleReference::descriptor) 597 .map(ModuleDescriptor::name) 598 .filter(predicate) 599 .collect(Collectors.toSet()); 600 } 601 602 static void printAttributes(PrintStream out) { 603 out.format(" size=\"25,25\";%n"); 604 out.format(" nodesep=.5;%n"); 605 out.format(" ranksep=1.5;%n"); 606 out.format(" pencolor=transparent;%n"); 607 out.format(" node [shape=plaintext, fontname=\"DejaVuSans\", fontsize=36, margin=\".2,.2\"];%n"); 608 out.format(" edge [penwidth=4, color=\"#999999\", arrowhead=open, arrowsize=2];%n"); 609 } 610 611 static void printNodes(PrintStream out, Graph<String> graph) { 612 out.format(" subgraph se {%n"); 613 graph.nodes().stream() 614 .filter(javaModules::contains) 615 .forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n", 616 mn, ORANGE, "java")); 617 out.format(" }%n"); 618 graph.nodes().stream() 619 .filter(jdkModules::contains) 620 .forEach(mn -> out.format(" \"%s\" [fontcolor=\"%s\", group=%s];%n", 621 mn, BLUE, "jdk")); 622 623 graph.nodes().stream() 624 .filter(mn -> !javaModules.contains(mn) && !jdkModules.contains(mn)) 625 .forEach(mn -> out.format(" \"%s\";%n", mn)); 626 } 627 628 static void printEdges(PrintStream out, Graph<String> graph, 629 String node, Set<String> requiresPublic) { 630 graph.adjacentNodes(node).forEach(dn -> { 631 String attr = dn.equals("java.base") ? REQUIRES_BASE 632 : (requiresPublic.contains(dn) ? REEXPORTS : REQUIRES); 633 out.format(" \"%s\" -> \"%s\" [%s];%n", node, dn, attr); 634 }); 635 } 636 } 637 638 public void genDotFile(Path dir) throws IOException { 639 String name = root.name(); 640 try (PrintStream out 641 = new PrintStream(Files.newOutputStream(dir.resolve(name + ".dot")))) { 642 Configuration cf = modulePaths.configuration(name); 643 644 // transitive reduction 645 Graph<String> graph = gengraph(cf); 646 647 out.format("digraph \"%s\" {%n", name); 648 DotGraph.printAttributes(out); 649 DotGraph.printNodes(out, graph); 650 651 cf.modules().stream() 652 .map(ResolvedModule::reference) 653 .map(ModuleReference::descriptor) 654 .sorted(Comparator.comparing(ModuleDescriptor::name)) 655 .forEach(md -> { 656 String mn = md.name(); 657 Set<String> requiresPublic = md.requires().stream() 658 .filter(d -> d.modifiers().contains(PUBLIC)) 659 .map(d -> d.name()) 660 .collect(Collectors.toSet()); 661 662 DotGraph.printEdges(out, graph, mn, requiresPublic); 663 }); 664 665 out.println("}"); 666 } 667 } 668 669 670 /** 671 * Returns a Graph of the given Configuration after transitive reduction. 672 * 673 * Transitive reduction of requires public edge and requires edge have 674 * to be applied separately to prevent the requires public edges 675 * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V) 676 * in which V would not be re-exported from U. 677 */ 678 private Graph<String> gengraph(Configuration cf) { 679 // build a Graph containing only requires public edges 680 // with transitive reduction. 681 Graph.Builder<String> rpgbuilder = new Graph.Builder<>(); 682 for (ResolvedModule resolvedModule : cf.modules()) { 683 ModuleDescriptor md = resolvedModule.reference().descriptor(); 684 String mn = md.name(); 685 md.requires().stream() 686 .filter(d -> d.modifiers().contains(PUBLIC)) 687 .map(d -> d.name()) 688 .forEach(d -> rpgbuilder.addEdge(mn, d)); 689 } 690 691 Graph<String> rpg = rpgbuilder.build().reduce(); 692 693 // build the readability graph 694 Graph.Builder<String> builder = new Graph.Builder<>(); 695 for (ResolvedModule resolvedModule : cf.modules()) { 696 ModuleDescriptor md = resolvedModule.reference().descriptor(); 697 String mn = md.name(); 698 builder.addNode(mn); 699 resolvedModule.reads().stream() 700 .map(ResolvedModule::name) 701 .forEach(d -> builder.addEdge(mn, d)); 702 } 703 704 // transitive reduction of requires edges 705 return builder.build().reduce(rpg); 706 } 707 708} 709