DepsAnalyzer.java revision 3628:047d4d42b466
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 26package com.sun.tools.jdeps; 27 28import com.sun.tools.classfile.Dependency.Location; 29import java.io.IOException; 30import java.util.ArrayList; 31import java.util.Deque; 32import java.util.HashSet; 33import java.util.LinkedHashSet; 34import java.util.LinkedList; 35import java.util.List; 36import java.util.Map; 37import java.util.Optional; 38import java.util.Set; 39import java.util.concurrent.ConcurrentLinkedDeque; 40import java.util.stream.Collectors; 41import java.util.stream.Stream; 42 43import static com.sun.tools.jdeps.Analyzer.Type.CLASS; 44import static com.sun.tools.jdeps.Analyzer.Type.VERBOSE; 45import static com.sun.tools.jdeps.Module.trace; 46import static java.util.stream.Collectors.*; 47 48/** 49 * Dependency Analyzer. 50 * 51 * Type of filters: 52 * source filter: -include <pattern> 53 * target filter: -package, -regex, -requires 54 * 55 * The initial archive set for analysis includes 56 * 1. archives specified in the command line arguments 57 * 2. observable modules matching the source filter 58 * 3. classpath archives matching the source filter or target filter 59 * 4. --add-modules and -m root modules 60 */ 61public class DepsAnalyzer { 62 final JdepsConfiguration configuration; 63 final JdepsFilter filter; 64 final JdepsWriter writer; 65 final Analyzer.Type verbose; 66 final boolean apiOnly; 67 68 final DependencyFinder finder; 69 final Analyzer analyzer; 70 final List<Archive> rootArchives = new ArrayList<>(); 71 72 // parsed archives 73 final Set<Archive> archives = new LinkedHashSet<>(); 74 75 public DepsAnalyzer(JdepsConfiguration config, 76 JdepsFilter filter, 77 JdepsWriter writer, 78 Analyzer.Type verbose, 79 boolean apiOnly) { 80 this.configuration = config; 81 this.filter = filter; 82 this.writer = writer; 83 this.verbose = verbose; 84 this.apiOnly = apiOnly; 85 86 this.finder = new DependencyFinder(config, filter); 87 this.analyzer = new Analyzer(configuration, verbose, filter); 88 89 // determine initial archives to be analyzed 90 this.rootArchives.addAll(configuration.initialArchives()); 91 92 // if -include pattern is specified, add the matching archives on 93 // classpath to the root archives 94 if (filter.hasIncludePattern() || filter.hasTargetFilter()) { 95 configuration.getModules().values().stream() 96 .filter(source -> filter.include(source) && filter.matches(source)) 97 .forEach(this.rootArchives::add); 98 } 99 100 // class path archives 101 configuration.classPathArchives().stream() 102 .filter(filter::matches) 103 .forEach(this.rootArchives::add); 104 105 // Include the root modules for analysis 106 this.rootArchives.addAll(configuration.rootModules()); 107 108 trace("analyze root archives: %s%n", this.rootArchives); 109 } 110 111 /* 112 * Perform runtime dependency analysis 113 */ 114 public boolean run() throws IOException { 115 return run(false, 1); 116 } 117 118 /** 119 * Perform compile-time view or run-time view dependency analysis. 120 * 121 * @param compileTimeView 122 * @param maxDepth depth of recursive analysis. depth == 0 if -R is set 123 */ 124 public boolean run(boolean compileTimeView, int maxDepth) throws IOException { 125 try { 126 // parse each packaged module or classpath archive 127 if (apiOnly) { 128 finder.parseExportedAPIs(rootArchives.stream()); 129 } else { 130 finder.parse(rootArchives.stream()); 131 } 132 archives.addAll(rootArchives); 133 134 int depth = maxDepth > 0 ? maxDepth : Integer.MAX_VALUE; 135 136 // transitive analysis 137 if (depth > 1) { 138 if (compileTimeView) 139 transitiveArchiveDeps(depth-1); 140 else 141 transitiveDeps(depth-1); 142 } 143 144 Set<Archive> archives = archives(); 145 146 // analyze the dependencies collected 147 analyzer.run(archives, finder.locationToArchive()); 148 149 writer.generateOutput(archives, analyzer); 150 } finally { 151 finder.shutdown(); 152 } 153 return true; 154 } 155 156 /** 157 * Returns the archives for reporting that has matching dependences. 158 * 159 * If -requires is set, they should be excluded. 160 */ 161 Set<Archive> archives() { 162 if (filter.requiresFilter().isEmpty()) { 163 return archives.stream() 164 .filter(filter::include) 165 .filter(Archive::hasDependences) 166 .collect(Collectors.toSet()); 167 } else { 168 // use the archives that have dependences and not specified in -requires 169 return archives.stream() 170 .filter(filter::include) 171 .filter(source -> !filter.requiresFilter().contains(source)) 172 .filter(source -> 173 source.getDependencies() 174 .map(finder::locationToArchive) 175 .anyMatch(a -> a != source)) 176 .collect(Collectors.toSet()); 177 } 178 } 179 180 /** 181 * Returns the dependences, either class name or package name 182 * as specified in the given verbose level. 183 */ 184 Set<String> dependences() { 185 return analyzer.archives().stream() 186 .map(analyzer::dependences) 187 .flatMap(Set::stream) 188 .collect(Collectors.toSet()); 189 } 190 191 /** 192 * Returns the archives that contains the given locations and 193 * not parsed and analyzed. 194 */ 195 private Set<Archive> unresolvedArchives(Stream<Location> locations) { 196 return locations.filter(l -> !finder.isParsed(l)) 197 .distinct() 198 .map(configuration::findClass) 199 .flatMap(Optional::stream) 200 .filter(filter::include) 201 .collect(toSet()); 202 } 203 204 /* 205 * Recursively analyzes entire module/archives. 206 */ 207 private void transitiveArchiveDeps(int depth) throws IOException { 208 Stream<Location> deps = archives.stream() 209 .flatMap(Archive::getDependencies); 210 211 // start with the unresolved archives 212 Set<Archive> unresolved = unresolvedArchives(deps); 213 do { 214 // parse all unresolved archives 215 Set<Location> targets = apiOnly 216 ? finder.parseExportedAPIs(unresolved.stream()) 217 : finder.parse(unresolved.stream()); 218 archives.addAll(unresolved); 219 220 // Add dependencies to the next batch for analysis 221 unresolved = unresolvedArchives(targets.stream()); 222 } while (!unresolved.isEmpty() && depth-- > 0); 223 } 224 225 /* 226 * Recursively analyze the class dependences 227 */ 228 private void transitiveDeps(int depth) throws IOException { 229 Stream<Location> deps = archives.stream() 230 .flatMap(Archive::getDependencies); 231 232 Deque<Location> unresolved = deps.collect(Collectors.toCollection(LinkedList::new)); 233 ConcurrentLinkedDeque<Location> deque = new ConcurrentLinkedDeque<>(); 234 do { 235 Location target; 236 while ((target = unresolved.poll()) != null) { 237 if (finder.isParsed(target)) 238 continue; 239 240 Archive archive = configuration.findClass(target).orElse(null); 241 if (archive != null && filter.include(archive)) { 242 archives.add(archive); 243 244 String name = target.getName(); 245 Set<Location> targets = apiOnly 246 ? finder.parseExportedAPIs(archive, name) 247 : finder.parse(archive, name); 248 249 // build unresolved dependencies 250 targets.stream() 251 .filter(t -> !finder.isParsed(t)) 252 .forEach(deque::add); 253 } 254 } 255 unresolved = deque; 256 deque = new ConcurrentLinkedDeque<>(); 257 } while (!unresolved.isEmpty() && depth-- > 0); 258 } 259 260 // ----- for testing purpose ----- 261 262 public static enum Info { 263 REQUIRES, 264 REQUIRES_PUBLIC, 265 EXPORTED_API, 266 MODULE_PRIVATE, 267 QUALIFIED_EXPORTED_API, 268 INTERNAL_API, 269 JDK_INTERNAL_API, 270 JDK_REMOVED_INTERNAL_API 271 } 272 273 public static class Node { 274 public final String name; 275 public final String source; 276 public final Info info; 277 Node(String name, Info info) { 278 this(name, name, info); 279 } 280 Node(String name, String source, Info info) { 281 this.name = name; 282 this.source = source; 283 this.info = info; 284 } 285 286 @Override 287 public String toString() { 288 StringBuilder sb = new StringBuilder(); 289 if (info != Info.REQUIRES && info != Info.REQUIRES_PUBLIC) 290 sb.append(source).append("/"); 291 292 sb.append(name); 293 if (info == Info.QUALIFIED_EXPORTED_API) 294 sb.append(" (qualified)"); 295 else if (info == Info.JDK_INTERNAL_API) 296 sb.append(" (JDK internal)"); 297 else if (info == Info.INTERNAL_API) 298 sb.append(" (internal)"); 299 return sb.toString(); 300 } 301 302 @Override 303 public boolean equals(Object o) { 304 if (!(o instanceof Node)) 305 return false; 306 307 Node other = (Node)o; 308 return this.name.equals(other.name) && 309 this.source.equals(other.source) && 310 this.info.equals(other.info); 311 } 312 313 @Override 314 public int hashCode() { 315 int result = name.hashCode(); 316 result = 31 * result + source.hashCode(); 317 result = 31 * result + info.hashCode(); 318 return result; 319 } 320 } 321 322 /** 323 * Returns a graph of module dependences. 324 * 325 * Each Node represents a module and each edge is a dependence. 326 * No analysis on "requires public". 327 */ 328 public Graph<Node> moduleGraph() { 329 Graph.Builder<Node> builder = new Graph.Builder<>(); 330 331 archives().stream() 332 .forEach(m -> { 333 Node u = new Node(m.getName(), Info.REQUIRES); 334 builder.addNode(u); 335 analyzer.requires(m) 336 .map(req -> new Node(req.getName(), Info.REQUIRES)) 337 .forEach(v -> builder.addEdge(u, v)); 338 }); 339 return builder.build(); 340 } 341 342 /** 343 * Returns a graph of dependences. 344 * 345 * Each Node represents a class or package per the specified verbose level. 346 * Each edge indicates 347 */ 348 public Graph<Node> dependenceGraph() { 349 Graph.Builder<Node> builder = new Graph.Builder<>(); 350 351 archives().stream() 352 .map(analyzer.results::get) 353 .filter(deps -> !deps.dependencies().isEmpty()) 354 .flatMap(deps -> deps.dependencies().stream()) 355 .forEach(d -> addEdge(builder, d)); 356 return builder.build(); 357 } 358 359 private void addEdge(Graph.Builder<Node> builder, Analyzer.Dep dep) { 360 Archive source = dep.originArchive(); 361 Archive target = dep.targetArchive(); 362 String pn = dep.target(); 363 if ((verbose == CLASS || verbose == VERBOSE)) { 364 int i = dep.target().lastIndexOf('.'); 365 pn = i > 0 ? dep.target().substring(0, i) : ""; 366 } 367 final Info info; 368 if (source == target) { 369 info = Info.MODULE_PRIVATE; 370 } else if (!target.getModule().isNamed()) { 371 info = Info.EXPORTED_API; 372 } else if (target.getModule().isExported(pn)) { 373 info = Info.EXPORTED_API; 374 } else { 375 Module module = target.getModule(); 376 if (module == Analyzer.REMOVED_JDK_INTERNALS) { 377 info = Info.JDK_REMOVED_INTERNAL_API; 378 } else if (!source.getModule().isJDK() && module.isJDK()) 379 info = Info.JDK_INTERNAL_API; 380 // qualified exports or inaccessible 381 else if (module.isExported(pn, source.getModule().name())) 382 info = Info.QUALIFIED_EXPORTED_API; 383 else 384 info = Info.INTERNAL_API; 385 } 386 387 Node u = new Node(dep.origin(), source.getName(), info); 388 Node v = new Node(dep.target(), target.getName(), info); 389 builder.addEdge(u, v); 390 } 391 392} 393