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