author | mchung |
Fri, 11 Nov 2016 17:32:21 -0800 | |
changeset 41997 | 79da2a8f4274 |
parent 41860 | 906670ff49c7 |
child 42407 | f3702cff2933 |
permissions | -rw-r--r-- |
36526 | 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 |
*/ |
|
25 |
package com.sun.tools.jdeps; |
|
26 |
||
38524 | 27 |
import static com.sun.tools.jdeps.Graph.*; |
28 |
import static com.sun.tools.jdeps.JdepsFilter.DEFAULT_FILTER; |
|
29 |
import static com.sun.tools.jdeps.Module.*; |
|
36526 | 30 |
import static java.lang.module.ModuleDescriptor.Requires.Modifier.*; |
38524 | 31 |
import static java.util.stream.Collectors.*; |
36526 | 32 |
|
38524 | 33 |
import com.sun.tools.classfile.Dependency; |
34 |
import com.sun.tools.jdeps.JdepsTask.BadArgs; |
|
35 |
||
36 |
import java.io.IOException; |
|
37 |
import java.io.OutputStream; |
|
38 |
import java.io.PrintWriter; |
|
36526 | 39 |
import java.lang.module.ModuleDescriptor; |
40 |
import java.nio.file.Files; |
|
41 |
import java.nio.file.Path; |
|
38524 | 42 |
import java.util.Collections; |
36526 | 43 |
import java.util.Comparator; |
44 |
import java.util.HashMap; |
|
45 |
import java.util.HashSet; |
|
46 |
import java.util.Map; |
|
38524 | 47 |
import java.util.Optional; |
36526 | 48 |
import java.util.Set; |
49 |
import java.util.function.Function; |
|
50 |
import java.util.stream.Collectors; |
|
51 |
import 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 |
*/ |
|
38524 | 57 |
public class ModuleAnalyzer { |
58 |
private static final String JAVA_BASE = "java.base"; |
|
59 |
||
60 |
private final JdepsConfiguration configuration; |
|
61 |
private final PrintWriter log; |
|
62 |
||
36526 | 63 |
private final DependencyFinder dependencyFinder; |
38524 | 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; |
|
36526 | 75 |
|
38524 | 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 |
} |
|
36526 | 86 |
} |
87 |
||
38524 | 88 |
public boolean run() throws IOException { |
89 |
try { |
|
90 |
// compute "requires public" dependences |
|
91 |
modules.values().forEach(ModuleDeps::computeRequiresPublic); |
|
36526 | 92 |
|
38524 | 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(); |
|
36526 | 101 |
} |
102 |
return true; |
|
103 |
} |
|
104 |
||
38524 | 105 |
class ModuleDeps { |
106 |
final Module root; |
|
107 |
Set<Module> requiresPublic; |
|
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 public' dependences by analyzing API dependencies |
|
117 |
*/ |
|
118 |
private void computeRequiresPublic() { |
|
119 |
// record requires public |
|
120 |
this.requiresPublic = computeRequires(true) |
|
121 |
.filter(m -> !m.name().equals(JAVA_BASE)) |
|
122 |
.collect(toSet()); |
|
123 |
||
124 |
trace("requires public: %s%n", requiresPublic); |
|
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(requiresPublic, requires); |
|
148 |
} |
|
149 |
||
150 |
private ModuleDescriptor descriptor(Set<Module> requiresPublic, |
|
151 |
Set<Module> requires) { |
|
152 |
||
153 |
ModuleDescriptor.Builder builder = new ModuleDescriptor.Builder(root.name()); |
|
154 |
||
155 |
if (!root.name().equals(JAVA_BASE)) |
|
156 |
builder.requires(MANDATED, JAVA_BASE); |
|
157 |
||
158 |
requiresPublic.stream() |
|
159 |
.filter(m -> !m.name().equals(JAVA_BASE)) |
|
160 |
.map(Module::name) |
|
161 |
.forEach(mn -> builder.requires(PUBLIC, mn)); |
|
36526 | 162 |
|
38524 | 163 |
requires.stream() |
164 |
.filter(m -> !requiresPublic.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 |
||
41860
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
172 |
private Graph<Module> buildReducedGraph() { |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
173 |
ModuleGraphBuilder rpBuilder = new ModuleGraphBuilder(configuration); |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
174 |
rpBuilder.addModule(root); |
38524 | 175 |
requiresPublic.stream() |
41860
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
176 |
.forEach(m -> rpBuilder.addEdge(root, m)); |
38524 | 177 |
|
178 |
// requires public graph |
|
41860
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
179 |
Graph<Module> rbg = rpBuilder.build().reduce(); |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
180 |
|
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
181 |
ModuleGraphBuilder gb = new ModuleGraphBuilder(configuration); |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
182 |
gb.addModule(root); |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
183 |
requires.stream() |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
184 |
.forEach(m -> gb.addEdge(root, m)); |
38524 | 185 |
|
186 |
// transitive reduction |
|
41860
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
187 |
Graph<Module> newGraph = gb.buildGraph().reduce(rbg); |
38524 | 188 |
if (DEBUG) { |
189 |
System.err.println("after transitive reduction: "); |
|
190 |
newGraph.printGraph(log); |
|
191 |
} |
|
41860
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
192 |
return newGraph; |
38524 | 193 |
} |
194 |
||
41860
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
195 |
/** |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
196 |
* Apply the transitive reduction on the module graph |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
197 |
* and returns the corresponding ModuleDescriptor |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
198 |
*/ |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
199 |
ModuleDescriptor reduced() { |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
200 |
Graph<Module> g = buildReducedGraph(); |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
201 |
return descriptor(requiresPublic, g.adjacentNodes(root)); |
906670ff49c7
8167057: jdeps option to list modules and internal APIs for @modules for test dev
mchung
parents:
40308
diff
changeset
|
202 |
} |
36526 | 203 |
|
38524 | 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 |
} |
|
36526 | 219 |
|
38524 | 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 |
} |
|
36526 | 246 |
|
38524 | 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<>(); |
|
36526 | 267 |
|
38524 | 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()); |
|
36526 | 286 |
|
38524 | 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; |
|
36526 | 308 |
} |
309 |
} |
|
310 |
||
38524 | 311 |
private boolean matches(ModuleDescriptor md, ModuleDescriptor other) { |
312 |
// build requires public from ModuleDescriptor |
|
313 |
Set<ModuleDescriptor.Requires> reqPublic = md.requires().stream() |
|
314 |
.filter(req -> req.modifiers().contains(PUBLIC)) |
|
315 |
.collect(toSet()); |
|
316 |
Set<ModuleDescriptor.Requires> otherReqPublic = other.requires().stream() |
|
317 |
.filter(req -> req.modifiers().contains(PUBLIC)) |
|
318 |
.collect(toSet()); |
|
36526 | 319 |
|
38524 | 320 |
if (!reqPublic.equals(otherReqPublic)) { |
36526 | 321 |
trace("mismatch requires public: %s%n", reqPublic); |
322 |
return false; |
|
323 |
} |
|
324 |
||
38524 | 325 |
Set<ModuleDescriptor.Requires> unused = md.requires().stream() |
326 |
.filter(req -> !other.requires().contains(req)) |
|
327 |
.collect(Collectors.toSet()); |
|
328 |
||
36526 | 329 |
if (!unused.isEmpty()) { |
330 |
trace("mismatch requires: %s%n", unused); |
|
331 |
return false; |
|
332 |
} |
|
333 |
return true; |
|
334 |
} |
|
335 |
||
336 |
/** |
|
38524 | 337 |
* Generate dotfile from module descriptor |
36526 | 338 |
* |
38524 | 339 |
* @param dir output directory |
36526 | 340 |
*/ |
38524 | 341 |
public boolean genDotFiles(Path dir) throws IOException { |
342 |
Files.createDirectories(dir); |
|
343 |
for (Module m : modules.keySet()) { |
|
344 |
genDotFile(dir, m.name()); |
|
36526 | 345 |
} |
38524 | 346 |
return true; |
36526 | 347 |
} |
348 |
||
349 |
||
38524 | 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()); |
|
36526 | 355 |
|
356 |
// transitive reduction |
|
38524 | 357 |
Graph<String> graph = gengraph(modules); |
36526 | 358 |
|
359 |
out.format("digraph \"%s\" {%n", name); |
|
360 |
DotGraph.printAttributes(out); |
|
361 |
DotGraph.printNodes(out, graph); |
|
362 |
||
38524 | 363 |
modules.stream() |
364 |
.map(Module::descriptor) |
|
365 |
.sorted(Comparator.comparing(ModuleDescriptor::name)) |
|
366 |
.forEach(md -> { |
|
367 |
String mn = md.name(); |
|
368 |
Set<String> requiresPublic = md.requires().stream() |
|
36526 | 369 |
.filter(d -> d.modifiers().contains(PUBLIC)) |
370 |
.map(d -> d.name()) |
|
38524 | 371 |
.collect(toSet()); |
36526 | 372 |
|
38524 | 373 |
DotGraph.printEdges(out, graph, mn, requiresPublic); |
374 |
}); |
|
36526 | 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 public edge and requires edge have |
|
384 |
* to be applied separately to prevent the requires public 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 |
*/ |
|
38524 | 388 |
private Graph<String> gengraph(Set<Module> modules) { |
36526 | 389 |
// build a Graph containing only requires public edges |
390 |
// with transitive reduction. |
|
391 |
Graph.Builder<String> rpgbuilder = new Graph.Builder<>(); |
|
38524 | 392 |
for (Module module : modules) { |
393 |
ModuleDescriptor md = module.descriptor(); |
|
36526 | 394 |
String mn = md.name(); |
395 |
md.requires().stream() |
|
396 |
.filter(d -> d.modifiers().contains(PUBLIC)) |
|
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<>(); |
|
38524 | 405 |
for (Module module : modules) { |
406 |
ModuleDescriptor md = module.descriptor(); |
|
36526 | 407 |
String mn = md.name(); |
408 |
builder.addNode(mn); |
|
38524 | 409 |
configuration.reads(module) |
410 |
.map(Module::name) |
|
36526 | 411 |
.forEach(d -> builder.addEdge(mn, d)); |
412 |
} |
|
413 |
||
414 |
// transitive reduction of requires edges |
|
415 |
return builder.build().reduce(rpg); |
|
416 |
} |
|
417 |
||
38524 | 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 |
} |
|
36526 | 439 |
} |