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 private final DependencyFinder dependencyFinder; 63 private final Map<Module, ModuleDeps> modules; 64 65 public ModuleAnalyzer(JdepsConfiguration config, 66 PrintWriter log, 67 Set<String> names) { 68 this.configuration = config; 69 this.log = log; 70 71 this.dependencyFinder = new DependencyFinder(config, DEFAULT_FILTER); 72 if (names.isEmpty()) { 73 this.modules = configuration.rootModules().stream() 74 .collect(toMap(Function.identity(), ModuleDeps::new)); 75 } else { 76 this.modules = names.stream() 77 .map(configuration::findModule) 78 .flatMap(Optional::stream) 79 .collect(toMap(Function.identity(), ModuleDeps::new)); 80 } 81 } 82 83 public boolean run() throws IOException { 84 try { 85 // compute "requires transitive" dependences 86 modules.values().forEach(ModuleDeps::computeRequiresTransitive); 87 88 modules.values().forEach(md -> { 89 // compute "requires" dependences 90 md.computeRequires(); 91 // apply transitive reduction and reports recommended requires. 92 md.analyzeDeps(); 93 }); 94 } finally { 95 dependencyFinder.shutdown(); 96 } 97 return true; 98 } 99 100 class ModuleDeps { 101 final Module root; 102 Set<Module> requiresTransitive; 103 Set<Module> requires; 104 Map<String, Set<String>> unusedQualifiedExports; 105 106 ModuleDeps(Module root) { 107 this.root = root; 108 } 109 110 /** 111 * Compute 'requires transitive' dependences by analyzing API dependencies 112 */ 113 private void computeRequiresTransitive() { 114 // record requires transitive 115 this.requiresTransitive = computeRequires(true) 116 .filter(m -> !m.name().equals(JAVA_BASE)) 117 .collect(toSet()); 118 119 trace("requires transitive: %s%n", requiresTransitive); 120 } 121 122 private void computeRequires() { 123 this.requires = computeRequires(false).collect(toSet()); 124 trace("requires: %s%n", requires); 125 } 126 127 private Stream<Module> computeRequires(boolean apionly) { 128 // analyze all classes 129 130 if (apionly) { 131 dependencyFinder.parseExportedAPIs(Stream.of(root)); 132 } else { 133 dependencyFinder.parse(Stream.of(root)); 134 } 135 136 // find the modules of all the dependencies found 137 return dependencyFinder.getDependences(root) 138 .map(Archive::getModule); 139 } 140 141 ModuleDescriptor descriptor() { 142 return descriptor(requiresTransitive, requires); 143 } 144 145 private ModuleDescriptor descriptor(Set<Module> requiresTransitive, 146 Set<Module> requires) { 147 148 ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(root.name()); 149 150 if (!root.name().equals(JAVA_BASE)) 151 builder.requires(Set.of(MANDATED), JAVA_BASE); 152 153 requiresTransitive.stream() 154 .filter(m -> !m.name().equals(JAVA_BASE)) 155 .map(Module::name) 156 .forEach(mn -> builder.requires(Set.of(TRANSITIVE), mn)); 157 158 requires.stream() 159 .filter(m -> !requiresTransitive.contains(m)) 160 .filter(m -> !m.name().equals(JAVA_BASE)) 161 .map(Module::name) 162 .forEach(mn -> builder.requires(mn)); 163 164 return builder.build(); 165 } 166 167 private Graph<Module> buildReducedGraph() { 168 ModuleGraphBuilder rpBuilder = new ModuleGraphBuilder(configuration); 169 rpBuilder.addModule(root); 170 requiresTransitive.stream() 171 .forEach(m -> rpBuilder.addEdge(root, m)); 172 173 // requires transitive graph 174 Graph<Module> rbg = rpBuilder.build().reduce(); 175 176 ModuleGraphBuilder gb = new ModuleGraphBuilder(configuration); 177 gb.addModule(root); 178 requires.stream() 179 .forEach(m -> gb.addEdge(root, m)); 180 181 // transitive reduction 182 Graph<Module> newGraph = gb.buildGraph().reduce(rbg); 183 if (DEBUG) { 184 System.err.println("after transitive reduction: "); 185 newGraph.printGraph(log); 186 } 187 return newGraph; 188 } 189 190 /** 191 * Apply the transitive reduction on the module graph 192 * and returns the corresponding ModuleDescriptor 193 */ 194 ModuleDescriptor reduced() { 195 Graph<Module> g = buildReducedGraph(); 196 return descriptor(requiresTransitive, g.adjacentNodes(root)); 197 } 198 199 /** 200 * Apply transitive reduction on the resulting graph and reports 201 * recommended requires. 202 */ 203 private void analyzeDeps() { 204 printModuleDescriptor(log, root); 205 206 ModuleDescriptor analyzedDescriptor = descriptor(); 207 if (!matches(root.descriptor(), analyzedDescriptor)) { 208 log.format(" [Suggested module descriptor for %s]%n", root.name()); 209 analyzedDescriptor.requires() 210 .stream() 211 .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) 212 .forEach(req -> log.format(" requires %s;%n", req)); 213 } 214 215 ModuleDescriptor reduced = reduced(); 216 if (!matches(root.descriptor(), reduced)) { 217 log.format(" [Transitive reduced graph for %s]%n", root.name()); 218 reduced.requires() 219 .stream() 220 .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) 221 .forEach(req -> log.format(" requires %s;%n", req)); 222 } 223 224 checkQualifiedExports(); 225 log.println(); 226 } 227 228 private void checkQualifiedExports() { 229 // detect any qualified exports not used by the target module 230 unusedQualifiedExports = unusedQualifiedExports(); 231 if (!unusedQualifiedExports.isEmpty()) 232 log.format(" [Unused qualified exports in %s]%n", root.name()); 233 234 unusedQualifiedExports.keySet().stream() 235 .sorted() 236 .forEach(pn -> log.format(" exports %s to %s%n", pn, 237 unusedQualifiedExports.get(pn).stream() 238 .sorted() 239 .collect(joining(",")))); 240 } 241 242 private void printModuleDescriptor(PrintWriter out, Module module) { 243 ModuleDescriptor descriptor = module.descriptor(); 244 out.format("%s (%s)%n", descriptor.name(), module.location()); 245 246 if (descriptor.name().equals(JAVA_BASE)) 247 return; 248 249 out.println(" [Module descriptor]"); 250 descriptor.requires() 251 .stream() 252 .sorted(Comparator.comparing(ModuleDescriptor.Requires::name)) 253 .forEach(req -> out.format(" requires %s;%n", req)); 254 } 255 256 257 /** 258 * Detects any qualified exports not used by the target module. 259 */ 260 private Map<String, Set<String>> unusedQualifiedExports() { 261 Map<String, Set<String>> unused = new HashMap<>(); 262 263 // build the qualified exports map 264 Map<String, Set<String>> qualifiedExports = 265 root.exports().entrySet().stream() 266 .filter(e -> !e.getValue().isEmpty()) 267 .map(Map.Entry::getKey) 268 .collect(toMap(Function.identity(), _k -> new HashSet<>())); 269 270 Set<Module> mods = new HashSet<>(); 271 root.exports().values() 272 .stream() 273 .flatMap(Set::stream) 274 .forEach(target -> configuration.findModule(target) 275 .ifPresentOrElse(mods::add, 276 () -> log.format("Warning: %s not found%n", target)) 277 ); 278 279 // parse all target modules 280 dependencyFinder.parse(mods.stream()); 281 282 // adds to the qualified exports map if a module references it 283 mods.stream().forEach(m -> 284 m.getDependencies() 285 .map(Dependency.Location::getPackageName) 286 .filter(qualifiedExports::containsKey) 287 .forEach(pn -> qualifiedExports.get(pn).add(m.name()))); 288 289 // compare with the exports from ModuleDescriptor 290 Set<String> staleQualifiedExports = 291 qualifiedExports.keySet().stream() 292 .filter(pn -> !qualifiedExports.get(pn).equals(root.exports().get(pn))) 293 .collect(toSet()); 294 295 if (!staleQualifiedExports.isEmpty()) { 296 for (String pn : staleQualifiedExports) { 297 Set<String> targets = new HashSet<>(root.exports().get(pn)); 298 targets.removeAll(qualifiedExports.get(pn)); 299 unused.put(pn, targets); 300 } 301 } 302 return unused; 303 } 304 } 305 306 private boolean matches(ModuleDescriptor md, ModuleDescriptor other) { 307 // build requires transitive from ModuleDescriptor 308 Set<ModuleDescriptor.Requires> reqTransitive = md.requires().stream() 309 .filter(req -> req.modifiers().contains(TRANSITIVE)) 310 .collect(toSet()); 311 Set<ModuleDescriptor.Requires> otherReqTransitive = other.requires().stream() 312 .filter(req -> req.modifiers().contains(TRANSITIVE)) 313 .collect(toSet()); 314 315 if (!reqTransitive.equals(otherReqTransitive)) { 316 trace("mismatch requires transitive: %s%n", reqTransitive); 317 return false; 318 } 319 320 Set<ModuleDescriptor.Requires> unused = md.requires().stream() 321 .filter(req -> !other.requires().contains(req)) 322 .collect(Collectors.toSet()); 323 324 if (!unused.isEmpty()) { 325 trace("mismatch requires: %s%n", unused); 326 return false; 327 } 328 return true; 329 } 330 331 // ---- for testing purpose 332 public ModuleDescriptor[] descriptors(String name) { 333 ModuleDeps moduleDeps = modules.keySet().stream() 334 .filter(m -> m.name().equals(name)) 335 .map(modules::get) 336 .findFirst().get(); 337 338 ModuleDescriptor[] descriptors = new ModuleDescriptor[3]; 339 descriptors[0] = moduleDeps.root.descriptor(); 340 descriptors[1] = moduleDeps.descriptor(); 341 descriptors[2] = moduleDeps.reduced(); 342 return descriptors; 343 } 344 345 public Map<String, Set<String>> unusedQualifiedExports(String name) { 346 ModuleDeps moduleDeps = modules.keySet().stream() 347 .filter(m -> m.name().equals(name)) 348 .map(modules::get) 349 .findFirst().get(); 350 return moduleDeps.unusedQualifiedExports; 351 } 352} 353