ModuleAnalyzer.java revision 3573:c4a18ee691c4
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.Deque;
45import java.util.HashMap;
46import java.util.HashSet;
47import java.util.LinkedList;
48import java.util.Map;
49import java.util.Optional;
50import java.util.Set;
51import java.util.function.Function;
52import java.util.stream.Collectors;
53import java.util.stream.Stream;
54
55/**
56 * Analyze module dependences and compare with module descriptor.
57 * Also identify any qualified exports not used by the target module.
58 */
59public class ModuleAnalyzer {
60    private static final String JAVA_BASE = "java.base";
61
62    private final JdepsConfiguration configuration;
63    private final PrintWriter log;
64
65    private final DependencyFinder dependencyFinder;
66    private final Map<Module, ModuleDeps> modules;
67
68    public ModuleAnalyzer(JdepsConfiguration config,
69                          PrintWriter log) {
70        this(config, log, Collections.emptySet());
71    }
72    public ModuleAnalyzer(JdepsConfiguration config,
73                          PrintWriter log,
74                          Set<String> names) {
75
76        if (!config.initialArchives().isEmpty()) {
77            String list = config.initialArchives().stream()
78                .map(Archive::getPathName).collect(joining(" "));
79            throw new JdepsTask.UncheckedBadArgs(new BadArgs("err.invalid.module.option",
80                list, "--check"));
81        }
82
83        this.configuration = config;
84        this.log = log;
85
86        this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER);
87        if (names.isEmpty()) {
88            this.modules = configuration.rootModules().stream()
89                .collect(toMap(Function.identity(), ModuleDeps::new));
90        } else {
91            this.modules = names.stream()
92                .map(configuration::findModule)
93                .flatMap(Optional::stream)
94                .collect(toMap(Function.identity(), ModuleDeps::new));
95        }
96    }
97
98    public boolean run() throws IOException {
99        try {
100            // compute "requires public" dependences
101            modules.values().forEach(ModuleDeps::computeRequiresPublic);
102
103            modules.values().forEach(md -> {
104                // compute "requires" dependences
105                md.computeRequires();
106                // apply transitive reduction and reports recommended requires.
107                md.analyzeDeps();
108            });
109        } finally {
110            dependencyFinder.shutdown();
111        }
112        return true;
113    }
114
115    class ModuleDeps {
116        final Module root;
117        Set<Module> requiresPublic;
118        Set<Module> requires;
119        Map<String, Set<String>> unusedQualifiedExports;
120
121        ModuleDeps(Module root) {
122            this.root = root;
123        }
124
125        /**
126         * Compute 'requires public' dependences by analyzing API dependencies
127         */
128        private void computeRequiresPublic() {
129            // record requires public
130            this.requiresPublic = computeRequires(true)
131                .filter(m -> !m.name().equals(JAVA_BASE))
132                .collect(toSet());
133
134            trace("requires public: %s%n", requiresPublic);
135        }
136
137        private void computeRequires() {
138            this.requires = computeRequires(false).collect(toSet());
139            trace("requires: %s%n", requires);
140        }
141
142        private Stream<Module> computeRequires(boolean apionly) {
143            // analyze all classes
144
145            if (apionly) {
146                dependencyFinder.parseExportedAPIs(Stream.of(root));
147            } else {
148                dependencyFinder.parse(Stream.of(root));
149            }
150
151            // find the modules of all the dependencies found
152            return dependencyFinder.getDependences(root)
153                        .map(Archive::getModule);
154        }
155
156        ModuleDescriptor descriptor() {
157            return descriptor(requiresPublic, requires);
158        }
159
160        private ModuleDescriptor descriptor(Set<Module> requiresPublic,
161                                            Set<Module> requires) {
162
163            ModuleDescriptor.Builder builder = new ModuleDescriptor.Builder(root.name());
164
165            if (!root.name().equals(JAVA_BASE))
166                builder.requires(MANDATED, JAVA_BASE);
167
168            requiresPublic.stream()
169                .filter(m -> !m.name().equals(JAVA_BASE))
170                .map(Module::name)
171                .forEach(mn -> builder.requires(PUBLIC, mn));
172
173            requires.stream()
174                .filter(m -> !requiresPublic.contains(m))
175                .filter(m -> !m.name().equals(JAVA_BASE))
176                .map(Module::name)
177                .forEach(mn -> builder.requires(mn));
178
179            return builder.build();
180        }
181
182        ModuleDescriptor reduced() {
183            Graph.Builder<Module> bd = new Graph.Builder<>();
184            requiresPublic.stream()
185                .forEach(m -> {
186                    bd.addNode(m);
187                    bd.addEdge(root, m);
188                });
189
190            // requires public graph
191            Graph<Module> rbg = bd.build().reduce();
192
193            // transitive reduction
194            Graph<Module> newGraph = buildGraph(requires).reduce(rbg);
195            if (DEBUG) {
196                System.err.println("after transitive reduction: ");
197                newGraph.printGraph(log);
198            }
199
200            return descriptor(requiresPublic, newGraph.adjacentNodes(root));
201        }
202
203
204        /**
205         * Apply transitive reduction on the resulting graph and reports
206         * recommended requires.
207         */
208        private void analyzeDeps() {
209            Graph.Builder<Module> builder = new Graph.Builder<>();
210            requiresPublic.stream()
211                .forEach(m -> {
212                    builder.addNode(m);
213                    builder.addEdge(root, m);
214                });
215
216            // requires public graph
217            Graph<Module> rbg = buildGraph(requiresPublic).reduce();
218
219            // transitive reduction
220            Graph<Module> newGraph = buildGraph(requires).reduce(builder.build().reduce());
221            if (DEBUG) {
222                System.err.println("after transitive reduction: ");
223                newGraph.printGraph(log);
224            }
225
226            printModuleDescriptor(log, root);
227
228            ModuleDescriptor analyzedDescriptor = descriptor();
229            if (!matches(root.descriptor(), analyzedDescriptor)) {
230                log.format("  [Suggested module descriptor for %s]%n", root.name());
231                analyzedDescriptor.requires()
232                    .stream()
233                    .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
234                    .forEach(req -> log.format("    requires %s;%n", req));
235            }
236
237            ModuleDescriptor reduced = reduced();
238            if (!matches(root.descriptor(), reduced)) {
239                log.format("  [Transitive reduced graph for %s]%n", root.name());
240                reduced.requires()
241                    .stream()
242                    .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
243                    .forEach(req -> log.format("    requires %s;%n", req));
244            }
245
246            checkQualifiedExports();
247            log.println();
248        }
249
250        private void checkQualifiedExports() {
251            // detect any qualified exports not used by the target module
252            unusedQualifiedExports = unusedQualifiedExports();
253            if (!unusedQualifiedExports.isEmpty())
254                log.format("  [Unused qualified exports in %s]%n", root.name());
255
256            unusedQualifiedExports.keySet().stream()
257                .sorted()
258                .forEach(pn -> log.format("    exports %s to %s%n", pn,
259                    unusedQualifiedExports.get(pn).stream()
260                        .sorted()
261                        .collect(joining(","))));
262        }
263
264        private void printModuleDescriptor(PrintWriter out, Module module) {
265            ModuleDescriptor descriptor = module.descriptor();
266            out.format("%s (%s)%n", descriptor.name(), module.location());
267
268            if (descriptor.name().equals(JAVA_BASE))
269                return;
270
271            out.println("  [Module descriptor]");
272            descriptor.requires()
273                .stream()
274                .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
275                .forEach(req -> out.format("    requires %s;%n", req));
276        }
277
278
279        /**
280         * Returns a graph of modules required by the specified module.
281         *
282         * Requires public edges of the dependences are added to the graph.
283         */
284        private Graph<Module> buildGraph(Set<Module> deps) {
285            Graph.Builder<Module> builder = new Graph.Builder<>();
286            builder.addNode(root);
287            Set<Module> visited = new HashSet<>();
288            visited.add(root);
289            Deque<Module> deque = new LinkedList<>();
290            deps.stream()
291                .forEach(m -> {
292                    deque.add(m);
293                    builder.addEdge(root, m);
294                });
295
296            // read requires public from ModuleDescription
297            Module source;
298            while ((source = deque.poll()) != null) {
299                if (visited.contains(source))
300                    continue;
301
302                visited.add(source);
303                builder.addNode(source);
304                Module from = source;
305                source.descriptor().requires().stream()
306                    .filter(req -> req.modifiers().contains(PUBLIC))
307                    .map(ModuleDescriptor.Requires::name)
308                    .map(configuration::findModule)
309                    .flatMap(Optional::stream)
310                    .forEach(m -> {
311                        deque.add(m);
312                        builder.addEdge(from, m);
313                    });
314            }
315            return builder.build();
316        }
317
318        /**
319         * Detects any qualified exports not used by the target module.
320         */
321        private Map<String, Set<String>> unusedQualifiedExports() {
322            Map<String, Set<String>> unused = new HashMap<>();
323
324            // build the qualified exports map
325            Map<String, Set<String>> qualifiedExports =
326                root.exports().entrySet().stream()
327                    .filter(e -> !e.getValue().isEmpty())
328                    .map(Map.Entry::getKey)
329                    .collect(toMap(Function.identity(), _k -> new HashSet<>()));
330
331            Set<Module> mods = new HashSet<>();
332            root.exports().values()
333                .stream()
334                .flatMap(Set::stream)
335                .forEach(target -> configuration.findModule(target)
336                    .ifPresentOrElse(mods::add,
337                        () -> log.format("Warning: %s not found%n", target))
338                );
339
340            // parse all target modules
341            dependencyFinder.parse(mods.stream());
342
343            // adds to the qualified exports map if a module references it
344            mods.stream().forEach(m ->
345                m.getDependencies()
346                    .map(Dependency.Location::getPackageName)
347                    .filter(qualifiedExports::containsKey)
348                    .forEach(pn -> qualifiedExports.get(pn).add(m.name())));
349
350            // compare with the exports from ModuleDescriptor
351            Set<String> staleQualifiedExports =
352                qualifiedExports.keySet().stream()
353                    .filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn)))
354                    .collect(toSet());
355
356            if (!staleQualifiedExports.isEmpty()) {
357                for (String pn : staleQualifiedExports) {
358                    Set<String> targets = new HashSet<>(root.exports().get(pn));
359                    targets.removeAll(qualifiedExports.get(pn));
360                    unused.put(pn, targets);
361                }
362            }
363            return unused;
364        }
365    }
366
367    private boolean matches(ModuleDescriptor md, ModuleDescriptor other) {
368        // build requires public from ModuleDescriptor
369        Set<ModuleDescriptor.Requires> reqPublic = md.requires().stream()
370            .filter(req -> req.modifiers().contains(PUBLIC))
371            .collect(toSet());
372        Set<ModuleDescriptor.Requires> otherReqPublic = other.requires().stream()
373            .filter(req -> req.modifiers().contains(PUBLIC))
374            .collect(toSet());
375
376        if (!reqPublic.equals(otherReqPublic)) {
377            trace("mismatch requires public: %s%n", reqPublic);
378            return false;
379        }
380
381        Set<ModuleDescriptor.Requires> unused = md.requires().stream()
382            .filter(req -> !other.requires().contains(req))
383            .collect(Collectors.toSet());
384
385        if (!unused.isEmpty()) {
386            trace("mismatch requires: %s%n", unused);
387            return false;
388        }
389        return true;
390    }
391
392    /**
393     * Generate dotfile from module descriptor
394     *
395     * @param dir output directory
396     */
397    public boolean genDotFiles(Path dir) throws IOException {
398        Files.createDirectories(dir);
399        for (Module m : modules.keySet()) {
400            genDotFile(dir, m.name());
401        }
402        return true;
403    }
404
405
406    private void genDotFile(Path dir, String name) throws IOException {
407        try (OutputStream os = Files.newOutputStream(dir.resolve(name + ".dot"));
408             PrintWriter out = new PrintWriter(os)) {
409            Set<Module> modules = configuration.resolve(Set.of(name))
410                .collect(Collectors.toSet());
411
412            // transitive reduction
413            Graph<String> graph = gengraph(modules);
414
415            out.format("digraph \"%s\" {%n", name);
416            DotGraph.printAttributes(out);
417            DotGraph.printNodes(out, graph);
418
419            modules.stream()
420                .map(Module::descriptor)
421                .sorted(Comparator.comparing(ModuleDescriptor::name))
422                .forEach(md -> {
423                    String mn = md.name();
424                    Set<String> requiresPublic = md.requires().stream()
425                        .filter(d -> d.modifiers().contains(PUBLIC))
426                        .map(d -> d.name())
427                        .collect(toSet());
428
429                    DotGraph.printEdges(out, graph, mn, requiresPublic);
430                });
431
432            out.println("}");
433        }
434    }
435
436    /**
437     * Returns a Graph of the given Configuration after transitive reduction.
438     *
439     * Transitive reduction of requires public edge and requires edge have
440     * to be applied separately to prevent the requires public edges
441     * (e.g. U -> V) from being reduced by a path (U -> X -> Y -> V)
442     * in which  V would not be re-exported from U.
443     */
444    private Graph<String> gengraph(Set<Module> modules) {
445        // build a Graph containing only requires public edges
446        // with transitive reduction.
447        Graph.Builder<String> rpgbuilder = new Graph.Builder<>();
448        for (Module module : modules) {
449            ModuleDescriptor md = module.descriptor();
450            String mn = md.name();
451            md.requires().stream()
452                    .filter(d -> d.modifiers().contains(PUBLIC))
453                    .map(d -> d.name())
454                    .forEach(d -> rpgbuilder.addEdge(mn, d));
455        }
456
457        Graph<String> rpg = rpgbuilder.build().reduce();
458
459        // build the readability graph
460        Graph.Builder<String> builder = new Graph.Builder<>();
461        for (Module module : modules) {
462            ModuleDescriptor md = module.descriptor();
463            String mn = md.name();
464            builder.addNode(mn);
465            configuration.reads(module)
466                    .map(Module::name)
467                    .forEach(d -> builder.addEdge(mn, d));
468        }
469
470        // transitive reduction of requires edges
471        return builder.build().reduce(rpg);
472    }
473
474    // ---- for testing purpose
475    public ModuleDescriptor[] descriptors(String name) {
476        ModuleDeps moduleDeps = modules.keySet().stream()
477            .filter(m -> m.name().equals(name))
478            .map(modules::get)
479            .findFirst().get();
480
481        ModuleDescriptor[] descriptors = new ModuleDescriptor[3];
482        descriptors[0] = moduleDeps.root.descriptor();
483        descriptors[1] = moduleDeps.descriptor();
484        descriptors[2] = moduleDeps.reduced();
485        return descriptors;
486    }
487
488    public Map<String, Set<String>> unusedQualifiedExports(String name) {
489        ModuleDeps moduleDeps = modules.keySet().stream()
490            .filter(m -> m.name().equals(name))
491            .map(modules::get)
492            .findFirst().get();
493        return moduleDeps.unusedQualifiedExports;
494    }
495}
496