langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/ModuleAnalyzer.java
changeset 36526 3b41f1c69604
child 38524 badd925c1d2f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/jdk.jdeps/share/classes/com/sun/tools/jdeps/ModuleAnalyzer.java	Thu Mar 17 19:04:28 2016 +0000
@@ -0,0 +1,708 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.tools.jdeps;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.UncheckedIOException;
+
+import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
+
+import java.lang.module.Configuration;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.lang.module.ResolvedModule;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.sun.tools.jdeps.Module.*;
+import static com.sun.tools.jdeps.ModulePaths.SystemModulePath.JAVA_BASE;
+
+/**
+ * Analyze module dependences and compare with module descriptor.
+ * Also identify any qualified exports not used by the target module.
+ */
+class ModuleAnalyzer {
+    private final ModulePaths modulePaths;
+    private final DependencyFinder dependencyFinder;
+    private final Module root;
+    private final Set<Module> modules;
+    private final Set<String> requiresPublic = new HashSet<>();
+    private final Set<String> requires = new HashSet<>();
+    private final Set<Module> exportTargets = new HashSet<>();
+    private final JdepsFilter filter;
+    private Graph<Module> graph;
+    ModuleAnalyzer(ModulePaths modulePaths, DependencyFinder finder,
+                   String moduleName) {
+        this.modulePaths = modulePaths;
+        this.dependencyFinder = finder;
+        this.root = modulePaths.getModules().get(moduleName);
+        this.modules = modulePaths.dependences(moduleName);
+
+        root.exports().values().stream()
+             .flatMap(Set::stream)
+             .map(target -> modulePaths.getModules().get(target))
+             .forEach(this.exportTargets::add);
+
+        this.filter = new JdepsFilter.Builder().filter(true, true).build();
+    }
+
+    /**
+     * Returns a graph of transitive closure of the given modules.
+     *
+     * This method does not add the implicit read edges and is intended to
+     * get all transitive closures in (reverse) topological sort.
+     */
+    public static Graph<Module> graph(ModulePaths modulePaths, Module... modules) {
+        Graph.Builder<Module> gb = new Graph.Builder<>();
+        for (Module module : modules) {
+            module.descriptor().requires().stream()
+                    .map(ModuleDescriptor.Requires::name)
+                    .map(mn -> modulePaths.getModules().get(mn))
+                    .forEach(m -> {
+                        gb.addNode(m);
+                        gb.addEdge(module, m);
+                    });
+        }
+        return gb.build();
+    }
+
+    /**
+     * Do the analysis
+     */
+    public boolean run() {
+        try {
+            computeRequiresPublic();
+            computeRequires();
+            // apply transitive reduction and reports recommended requires.
+            analyzeDeps();
+            // detect any qualiifed exports not used by the target module
+            checkQualifiedExports();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        return true;
+    }
+
+    /**
+     * Compute 'requires public' dependences by analyzing API dependencies
+     */
+    private void computeRequiresPublic() throws IOException {
+        JdepsFilter.Builder builder = new JdepsFilter.Builder();
+        // only analyze exported API
+        root.descriptor.exports().stream()
+                .filter(exp -> !exp.isQualified())
+                .map(ModuleDescriptor.Exports::source)
+                .forEach(builder::includePackage);
+
+        JdepsFilter filter = builder.filter(true, true).build();
+
+        // analyze dependences for exported packages
+        dependencyFinder.findDependencies(filter, true /* api only */, 1);
+        Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
+        analyzer.run(modules);
+
+        // record requires public
+        analyzer.requires(root)
+                .filter(m -> m != JAVA_BASE && m != root)
+                .map(Archive::getName)
+                .forEach(requiresPublic::add);
+        trace("requires public: %s%n", requiresPublic);
+    }
+
+    private void computeRequires() throws IOException {
+        // add the exportTargets of the qualified exports to the root set
+        exportTargets.stream()
+                .peek(target -> trace("add root: %s%n", target))
+                .forEach(dependencyFinder::addRoot);
+
+        // analyze all classes
+        dependencyFinder.findDependencies(filter, false /* all classes */, 1);
+        Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
+        analyzer.run(modules);
+
+        // record requires
+        analyzer.requires(root)
+                .filter(m -> m != JAVA_BASE && m != root)
+                .map(Archive::getName)
+                .forEach(requires::add);
+
+        this.graph = buildGraph(analyzer, root);
+        if (traceOn) {
+            trace("dependences: %s%n", graph.nodes());
+            graph.printGraph(System.out);
+        }
+    }
+
+    /**
+     * Apply transitive reduction on the resulting graph and reports
+     * recommended requires.
+     */
+    private void analyzeDeps() {
+        String moduleName = root.name();
+
+        Graph.Builder<String> builder = new Graph.Builder<>();
+        requiresPublic.stream()
+                .forEach(mn -> {
+                    builder.addNode(mn);
+                    builder.addEdge(moduleName, mn);
+                });
+        // requires public graph
+        Graph<String> rbg = builder.build().reduce();
+
+        // convert the dependence graph from Module to name
+        Set<String> nodes = this.graph.nodes().stream()
+                .map(Module::name)
+                .collect(Collectors.toSet());
+        Map<String, Set<String>> edges = new HashMap<>();
+        this.graph.edges().keySet().stream()
+                .forEach(m -> {
+                    String mn = m.name();
+                    Set<String> es = edges.computeIfAbsent(mn, _k -> new HashSet<String>());
+                    this.graph.edges().get(m).stream()
+                            .map(Module::name)
+                            .forEach(es::add);
+                });
+
+        // transitive reduction
+        Graph<String> newGraph = new Graph<>(nodes, edges).reduce(rbg);
+        if (traceOn) {
+            System.out.println("after transitive reduction");
+            newGraph.printGraph(System.out);
+        };
+
+        Set<String> reducedRequires = newGraph.adjacentNodes(moduleName);
+        if (matches(root.descriptor(), requires, requiresPublic) &&
+                matches(root.descriptor(), reducedRequires, requiresPublic)) {
+            System.out.println("--- Analysis result: no change for " + root.name());
+        } else {
+            System.out.println("--- Analysis result: suggested requires for " + root.name());
+            System.out.format("module %s%n", root.name());
+                requires.stream()
+                        .sorted()
+                        .forEach(dn -> System.out.format("  requires %s%s;%n",
+                                requiresPublic.contains(dn) ? "public " : "", dn));
+            if (!requires.equals(reducedRequires) && !reducedRequires.isEmpty()) {
+                System.out.format("%nmodule %s (reduced)%n", root.name());
+                newGraph.adjacentNodes(moduleName)
+                     .stream()
+                     .sorted()
+                     .forEach(dn -> System.out.format("  requires %s%s;%n",
+                                requiresPublic.contains(dn) ? "public " : "", dn));
+            }
+            System.out.println("\n---  Module descriptor");
+            Graph<Module> mdGraph = graph(modulePaths, root);
+            mdGraph.reverse(m -> printModuleDescriptor(System.out, m.descriptor()));
+        }
+    }
+
+    /**
+     * Detects any qualified exports not used by the target module.
+     */
+    private void checkQualifiedExports() throws IOException {
+        Analyzer analyzer = new Analyzer(Analyzer.Type.CLASS, filter);
+        analyzer.run(dependencyFinder.roots());
+
+        // build the qualified exports map
+        Map<String, Set<String>> qualifiedExports =
+            root.exports().entrySet().stream()
+                .filter(e -> !e.getValue().isEmpty())
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toMap(Function.identity(), _k -> new HashSet<>()));
+
+        // adds to the qualified exports map if a module references it
+        for (Module m : exportTargets) {
+            analyzer.dependences(m).stream()
+                    .map(this::toPackageName)
+                    .filter(qualifiedExports::containsKey)
+                    .forEach(pn -> qualifiedExports.get(pn).add(m.name()));
+        }
+
+        // compare with the exports from ModuleDescriptor
+        Set<String> staleQualifiedExports =
+            qualifiedExports.keySet().stream()
+                .filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn)))
+                .collect(Collectors.toSet());
+
+        if (!staleQualifiedExports.isEmpty()) {
+            System.out.println("--- Unused qualified exports in " + root.name());
+            for (String pn : staleQualifiedExports) {
+                Set<String> unused = new HashSet<>(root.exports().get(pn));
+                unused.removeAll(qualifiedExports.get(pn));
+                System.out.format("  exports %s to %s%n", pn,
+                                  unused.stream().collect(Collectors.joining(",")));
+            }
+        }
+    }
+
+    private String toPackageName(String cn) {
+        int i = cn.lastIndexOf('.');
+        return i > 0 ? cn.substring(0, i) : "";
+    }
+
+    private boolean matches(ModuleDescriptor md, Set<String> requires, Set<String> requiresPublic) {
+        Set<String> reqPublic = md.requires().stream()
+                .filter(req -> req.modifiers().contains(PUBLIC))
+                .map(ModuleDescriptor.Requires::name)
+                .collect(Collectors.toSet());
+        if (!requiresPublic.equals(reqPublic)) {
+            trace("mismatch requires public: %s%n", reqPublic);
+            return false;
+        }
+        // java.base is not in requires
+        int javaBase = md.name().equals(JAVA_BASE.name()) ? 0 : 1;
+        if (requires.size()+javaBase != md.requires().size()) {
+            trace("mismatch requires: %d != %d%n", requires.size()+1, md.requires().size());
+            return false;
+        }
+
+        Set<String> unused = md.requires().stream()
+                 .map(ModuleDescriptor.Requires::name)
+                 .filter(req -> !requires.contains(req) && !req.equals(JAVA_BASE.name()))
+                 .collect(Collectors.toSet());
+        if (!unused.isEmpty()) {
+            trace("mismatch requires: %s%n", unused);
+            return false;
+        }
+        return true;
+    }
+
+    private void printModuleDescriptor(PrintStream out, ModuleDescriptor descriptor) {
+        if (descriptor.name().equals("java.base"))
+            return;
+
+        out.format("module %s%n", descriptor.name());
+        descriptor.requires()
+                .stream()
+                .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
+                .forEach(req -> out.format("  requires %s;%n", req));
+    }
+
+    /**
+     * Returns a graph of modules required by the specified module.
+     *
+     * Requires public edges of the dependences are added to the graph.
+     */
+    private Graph<Module> buildGraph(Analyzer analyzer, Module module) {
+        Graph.Builder<Module> builder = new Graph.Builder<>();
+        builder.addNode(module);
+        Set<Module> visited = new HashSet<>();
+        visited.add(module);
+        Deque<Module> deque = new LinkedList<>();
+        analyzer.requires(module)
+                .map(Archive::getModule)
+                .filter(m -> m != JAVA_BASE)
+                .forEach(m -> {
+                    deque.add(m);
+                    builder.addEdge(module, m);
+                });
+
+        // read requires public from ModuleDescription
+        Module source;
+        while ((source = deque.poll()) != null) {
+            if (visited.contains(source))
+                continue;
+            visited.add(source);
+            builder.addNode(source);
+            Module from = source;
+            source.descriptor().requires().stream()
+                    .filter(req -> req.modifiers().contains(PUBLIC))
+                    .map(ModuleDescriptor.Requires::name)
+                    .map(req -> modulePaths.getModules().get(req))
+                    .filter(m -> m != JAVA_BASE)
+                    .forEach(m -> {
+                        deque.add(m);
+                        builder.addEdge(from, m);
+                    });
+        }
+        return builder.build();
+    }
+
+    static class Graph<T> {
+        private final Set<T> nodes;
+        private final Map<T, Set<T>> edges;
+
+        private Graph(Set<T> nodes, Map<T, Set<T>> edges) {
+            this.nodes = nodes;
+            this.edges = edges;
+        }
+
+        public Set<T> nodes() {
+            return nodes;
+        }
+
+        public Map<T, Set<T>> edges() {
+            return edges;
+        }
+
+        public Set<T> adjacentNodes(T u) {
+            return edges.get(u);
+        }
+
+        /**
+         * Returns a new Graph after transitive reduction
+         */
+        public Graph<T> reduce() {
+            Graph.Builder<T> builder = new Builder<>();
+            nodes.stream()
+                    .forEach(u -> {
+                        builder.addNode(u);
+                        edges.get(u).stream()
+                                .filter(v -> !pathExists(u, v, false))
+                                .forEach(v -> builder.addEdge(u, v));
+                    });
+            return builder.build();
+        }
+
+        /**
+         * Returns a new Graph after transitive reduction.  All edges in
+         * the given g takes precedence over this graph.
+         *
+         * @throw IllegalArgumentException g must be a subgraph this graph
+         */
+        public Graph<T> reduce(Graph<T> g) {
+            boolean subgraph = nodes.containsAll(g.nodes) && g.edges.keySet().stream()
+                    .allMatch(u -> adjacentNodes(u).containsAll(g.adjacentNodes(u)));
+            if (!subgraph) {
+                throw new IllegalArgumentException(g + " is not a subgraph of " + this);
+            }
+
+            Graph.Builder<T> builder = new Builder<>();
+            nodes.stream()
+                    .forEach(u -> {
+                        builder.addNode(u);
+                        // filter the edge if there exists a path from u to v in the given g
+                        // or there exists another path from u to v in this graph
+                        edges.get(u).stream()
+                                .filter(v -> !g.pathExists(u, v) && !pathExists(u, v, false))
+                                .forEach(v -> builder.addEdge(u, v));
+                    });
+
+            // add the overlapped edges from this graph and the given g
+            g.edges().keySet().stream()
+                    .forEach(u -> g.adjacentNodes(u).stream()
+                                    .filter(v -> isAdjacent(u, v))
+                                    .forEach(v -> builder.addEdge(u, v)));
+            return builder.build();
+        }
+
+        /**
+         * Returns nodes sorted in topological order.
+         */
+        public Stream<T> orderedNodes() {
+            TopoSorter<T> sorter = new TopoSorter<>(this);
+            return sorter.result.stream();
+        }
+
+        /**
+         * Iterates the nodes sorted in topological order and performs the
+         * given action.
+         */
+        public void ordered(Consumer<T> action) {
+            TopoSorter<T> sorter = new TopoSorter<>(this);
+            sorter.ordered(action);
+        }
+
+        /**
+         * Iterates the nodes sorted in reverse topological order and
+         * performs the given action.
+         */
+        public void reverse(Consumer<T> action) {
+            TopoSorter<T> sorter = new TopoSorter<>(this);
+            sorter.reverse(action);
+        }
+
+        private boolean isAdjacent(T u, T v) {
+            return edges.containsKey(u) && edges.get(u).contains(v);
+        }
+
+        private boolean pathExists(T u, T v) {
+            return pathExists(u, v, true);
+        }
+
+        /**
+         * Returns true if there exists a path from u to v in this graph.
+         * If includeAdjacent is false, it returns true if there exists
+         * another path from u to v of distance > 1
+         */
+        private boolean pathExists(T u, T v, boolean includeAdjacent) {
+            if (!nodes.contains(u) || !nodes.contains(v)) {
+                return false;
+            }
+            if (includeAdjacent && isAdjacent(u, v)) {
+                return true;
+            }
+            Deque<T> stack = new LinkedList<>();
+            Set<T> visited = new HashSet<>();
+            stack.push(u);
+            while (!stack.isEmpty()) {
+                T node = stack.pop();
+                if (node.equals(v)) {
+                    return true;
+                }
+                if (!visited.contains(node)) {
+                    visited.add(node);
+                    edges.get(node).stream()
+                            .filter(e -> includeAdjacent || !node.equals(u) || !e.equals(v))
+                            .forEach(e -> stack.push(e));
+                }
+            }
+            assert !visited.contains(v);
+            return false;
+        }
+
+        void printGraph(PrintStream out) {
+            out.println("graph for " + nodes);
+            nodes.stream()
+                 .forEach(u -> adjacentNodes(u).stream()
+                                   .forEach(v -> out.format("%s -> %s%n", u, v)));
+        }
+
+        @Override
+        public String toString() {
+            return nodes.toString();
+        }
+
+        static class Builder<T> {
+            final Set<T> nodes = new HashSet<>();
+            final Map<T, Set<T>> edges = new HashMap<>();
+
+            public void addNode(T node) {
+                if (nodes.contains(node)) {
+                    return;
+                }
+                nodes.add(node);
+                edges.computeIfAbsent(node, _e -> new HashSet<>());
+            }
+
+            public void addEdge(T u, T v) {
+                addNode(u);
+                addNode(v);
+                edges.get(u).add(v);
+            }
+
+            public Graph<T> build() {
+                return new Graph<>(nodes, edges);
+            }
+
+            void print(PrintStream out) {
+                out.println(nodes);
+                nodes.stream()
+                        .forEach(u -> edges.get(u).stream()
+                                        .forEach(v -> out.format("%s -> %s%n", u, v)));
+            }
+        }
+    }
+
+    static class TopoSorter<T> {
+        final Deque<T> result = new LinkedList<>();
+        final Deque<T> nodes;
+        final Graph<T> graph;
+        TopoSorter(Graph<T> graph) {
+            this.graph = graph;
+            this.nodes = new LinkedList<>(graph.nodes);
+            sort();
+        }
+
+        public void ordered(Consumer<T> action) {
+            result.iterator().forEachRemaining(action);
+        }
+
+        public void reverse(Consumer<T> action) {
+            result.descendingIterator().forEachRemaining(action);
+        }
+
+        private void sort() {
+            Deque<T> visited = new LinkedList<>();
+            Deque<T> done = new LinkedList<>();
+            T node;
+            while ((node = nodes.poll()) != null) {
+                if (!visited.contains(node)) {
+                    visit(node, visited, done);
+                }
+            }
+        }
+
+        private void visit(T node, Deque<T> visited, Deque<T> done) {
+            if (visited.contains(node)) {
+                if (!done.contains(node)) {
+                    throw new IllegalArgumentException("Cyclic detected: " +
+                            node + " " + graph.edges().get(node));
+                }
+                return;
+            }
+            visited.add(node);
+            graph.edges().get(node).stream()
+                 .forEach(x -> visit(x, visited, done));
+            done.add(node);
+            result.addLast(node);
+        }
+    }
+
+    static class DotGraph {
+        static final String ORANGE = "#e76f00";
+        static final String BLUE = "#437291";
+        static final String GRAY = "#dddddd";
+
+        static final String REEXPORTS = "";
+        static final String REQUIRES = "style=\"dashed\"";
+        static final String REQUIRES_BASE = "color=\"" + GRAY + "\"";
+
+        static final Set<String> javaModules = modules(name ->
+                (name.startsWith("java.") && !name.equals("java.smartcardio")));
+        static final Set<String> jdkModules = modules(name ->
+                (name.startsWith("java.") ||
+                        name.startsWith("jdk.") ||
+                        name.startsWith("javafx.")) && !javaModules.contains(name));
+
+        private static Set<String> modules(Predicate<String> predicate) {
+            return ModuleFinder.ofSystem().findAll()
+                               .stream()
+                               .map(ModuleReference::descriptor)
+                               .map(ModuleDescriptor::name)
+                               .filter(predicate)
+                               .collect(Collectors.toSet());
+        }
+
+        static void printAttributes(PrintStream out) {
+            out.format("  size=\"25,25\";%n");
+            out.format("  nodesep=.5;%n");
+            out.format("  ranksep=1.5;%n");
+            out.format("  pencolor=transparent;%n");
+            out.format("  node [shape=plaintext, fontname=\"DejaVuSans\", fontsize=36, margin=\".2,.2\"];%n");
+            out.format("  edge [penwidth=4, color=\"#999999\", arrowhead=open, arrowsize=2];%n");
+        }
+
+        static void printNodes(PrintStream out, Graph<String> graph) {
+            out.format("  subgraph se {%n");
+            graph.nodes().stream()
+                 .filter(javaModules::contains)
+                 .forEach(mn -> out.format("  \"%s\" [fontcolor=\"%s\", group=%s];%n",
+                                           mn, ORANGE, "java"));
+            out.format("  }%n");
+            graph.nodes().stream()
+                 .filter(jdkModules::contains)
+                 .forEach(mn -> out.format("    \"%s\" [fontcolor=\"%s\", group=%s];%n",
+                                              mn, BLUE, "jdk"));
+
+            graph.nodes().stream()
+                    .filter(mn -> !javaModules.contains(mn) && !jdkModules.contains(mn))
+                    .forEach(mn -> out.format("  \"%s\";%n", mn));
+        }
+
+        static void printEdges(PrintStream out, Graph<String> graph,
+                               String node, Set<String> requiresPublic) {
+            graph.adjacentNodes(node).forEach(dn -> {
+                String attr = dn.equals("java.base") ? REQUIRES_BASE
+                        : (requiresPublic.contains(dn) ? REEXPORTS : REQUIRES);
+                out.format("  \"%s\" -> \"%s\" [%s];%n", node, dn, attr);
+            });
+        }
+    }
+
+    public void genDotFile(Path dir) throws IOException {
+        String name = root.name();
+        try (PrintStream out
+                     = new PrintStream(Files.newOutputStream(dir.resolve(name + ".dot")))) {
+            Configuration cf = modulePaths.configuration(name);
+
+            // transitive reduction
+            Graph<String> graph = gengraph(cf);
+
+            out.format("digraph \"%s\" {%n", name);
+            DotGraph.printAttributes(out);
+            DotGraph.printNodes(out, graph);
+
+            cf.modules().stream()
+                    .map(ResolvedModule::reference)
+                    .map(ModuleReference::descriptor)
+                    .sorted(Comparator.comparing(ModuleDescriptor::name))
+                    .forEach(md -> {
+                String mn = md.name();
+                Set<String> requiresPublic = md.requires().stream()
+                        .filter(d -> d.modifiers().contains(PUBLIC))
+                        .map(d -> d.name())
+                        .collect(Collectors.toSet());
+
+                DotGraph.printEdges(out, graph, mn, requiresPublic);
+            });
+
+            out.println("}");
+        }
+    }
+
+
+    /**
+     * Returns a Graph of the given Configuration after transitive reduction.
+     *
+     * Transitive reduction of requires public edge and requires edge have
+     * to be applied separately to prevent the requires public edges
+     * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V)
+     * in which  V would not be re-exported from U.
+     */
+    private Graph<String> gengraph(Configuration cf) {
+        // build a Graph containing only requires public edges
+        // with transitive reduction.
+        Graph.Builder<String> rpgbuilder = new Graph.Builder<>();
+        for (ResolvedModule resolvedModule : cf.modules()) {
+            ModuleDescriptor md = resolvedModule.reference().descriptor();
+            String mn = md.name();
+            md.requires().stream()
+                    .filter(d -> d.modifiers().contains(PUBLIC))
+                    .map(d -> d.name())
+                    .forEach(d -> rpgbuilder.addEdge(mn, d));
+        }
+
+        Graph<String> rpg = rpgbuilder.build().reduce();
+
+        // build the readability graph
+        Graph.Builder<String> builder = new Graph.Builder<>();
+        for (ResolvedModule resolvedModule : cf.modules()) {
+            ModuleDescriptor md = resolvedModule.reference().descriptor();
+            String mn = md.name();
+            builder.addNode(mn);
+            resolvedModule.reads().stream()
+                    .map(ResolvedModule::name)
+                    .forEach(d -> builder.addEdge(mn, d));
+        }
+
+        // transitive reduction of requires edges
+        return builder.build().reduce(rpg);
+    }
+
+}