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