ModuleAnalyzer.java revision 3976:65d446c80cdf
1/*
2 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * 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 static com.sun.tools.jdeps.Graph.*;
28import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER;
29import static com.sun.tools.jdeps.Module.*;
30import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
31import static java.util.stream.Collectors.*;
32
33import com.sun.tools.classfile.Dependency;
34import com.sun.tools.jdeps.JdepsTask.BadArgs;
35
36import java.io.IOException;
37import java.io.OutputStream;
38import java.io.PrintWriter;
39import java.lang.module.ModuleDescriptor;
40import java.nio.file.Files;
41import java.nio.file.Path;
42import java.util.Collections;
43import java.util.Comparator;
44import java.util.HashMap;
45import java.util.HashSet;
46import java.util.Map;
47import java.util.Optional;
48import java.util.Set;
49import java.util.function.Function;
50import java.util.stream.Collectors;
51import java.util.stream.Stream;
52
53/**
54 * Analyze module dependences and compare with module descriptor.
55 * Also identify any qualified exports not used by the target module.
56 */
57public class ModuleAnalyzer {
58    private static final String JAVA_BASE = "java.base";
59
60    private final JdepsConfiguration configuration;
61    private final PrintWriter log;
62
63    private final DependencyFinder dependencyFinder;
64    private final Map<Module, ModuleDeps> modules;
65
66    public ModuleAnalyzer(JdepsConfiguration config,
67                          PrintWriter log) {
68        this(config, log, Collections.emptySet());
69    }
70    public ModuleAnalyzer(JdepsConfiguration config,
71                          PrintWriter log,
72                          Set<String> names) {
73        this.configuration = config;
74        this.log = log;
75
76        this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER);
77        if (names.isEmpty()) {
78            this.modules = configuration.rootModules().stream()
79                .collect(toMap(Function.identity(), ModuleDeps::new));
80        } else {
81            this.modules = names.stream()
82                .map(configuration::findModule)
83                .flatMap(Optional::stream)
84                .collect(toMap(Function.identity(), ModuleDeps::new));
85        }
86    }
87
88    public boolean run() throws IOException {
89        try {
90            // compute "requires transitive" dependences
91            modules.values().forEach(ModuleDeps::computeRequiresTransitive);
92
93            modules.values().forEach(md -> {
94                // compute "requires" dependences
95                md.computeRequires();
96                // apply transitive reduction and reports recommended requires.
97                md.analyzeDeps();
98            });
99        } finally {
100            dependencyFinder.shutdown();
101        }
102        return true;
103    }
104
105    class ModuleDeps {
106        final Module root;
107        Set<Module> requiresTransitive;
108        Set<Module> requires;
109        Map<String, Set<String>> unusedQualifiedExports;
110
111        ModuleDeps(Module root) {
112            this.root = root;
113        }
114
115        /**
116         * Compute 'requires transitive' dependences by analyzing API dependencies
117         */
118        private void computeRequiresTransitive() {
119            // record requires transitive
120            this.requiresTransitive = computeRequires(true)
121                .filter(m -> !m.name().equals(JAVA_BASE))
122                .collect(toSet());
123
124            trace("requires transitive: %s%n", requiresTransitive);
125        }
126
127        private void computeRequires() {
128            this.requires = computeRequires(false).collect(toSet());
129            trace("requires: %s%n", requires);
130        }
131
132        private Stream<Module> computeRequires(boolean apionly) {
133            // analyze all classes
134
135            if (apionly) {
136                dependencyFinder.parseExportedAPIs(Stream.of(root));
137            } else {
138                dependencyFinder.parse(Stream.of(root));
139            }
140
141            // find the modules of all the dependencies found
142            return dependencyFinder.getDependences(root)
143                        .map(Archive::getModule);
144        }
145
146        ModuleDescriptor descriptor() {
147            return descriptor(requiresTransitive, requires);
148        }
149
150        private ModuleDescriptor descriptor(Set<Module> requiresTransitive,
151                                            Set<Module> requires) {
152
153            ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(root.name());
154
155            if (!root.name().equals(JAVA_BASE))
156                builder.requires(Set.of(MANDATED), JAVA_BASE);
157
158            requiresTransitive.stream()
159                .filter(m -> !m.name().equals(JAVA_BASE))
160                .map(Module::name)
161                .forEach(mn -> builder.requires(Set.of(TRANSITIVE), mn));
162
163            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