Analyzer.java revision 3170:dc017a37aac5
1/* 2 * Copyright (c) 2013, 2014, 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 java.io.PrintStream; 29import java.util.Comparator; 30import java.util.HashMap; 31import java.util.HashSet; 32import java.util.List; 33import java.util.Map; 34import java.util.Objects; 35import java.util.Set; 36import java.util.stream.Collectors; 37import java.util.stream.Stream; 38 39import com.sun.tools.classfile.Dependency.Location; 40 41/** 42 * Dependency Analyzer. 43 */ 44public class Analyzer { 45 /** 46 * Type of the dependency analysis. Appropriate level of data 47 * will be stored. 48 */ 49 public enum Type { 50 SUMMARY, 51 PACKAGE, 52 CLASS, 53 VERBOSE 54 } 55 56 /** 57 * Filter to be applied when analyzing the dependencies from the given archives. 58 * Only the accepted dependencies are recorded. 59 */ 60 interface Filter { 61 boolean accepts(Location origin, Archive originArchive, Location target, Archive targetArchive); 62 } 63 64 protected final Type type; 65 protected final Filter filter; 66 protected final Map<Archive, ArchiveDeps> results = new HashMap<>(); 67 protected final Map<Location, Archive> map = new HashMap<>(); 68 private static final Archive NOT_FOUND 69 = new Archive(JdepsTask.getMessage("artifact.not.found")); 70 71 /** 72 * Constructs an Analyzer instance. 73 * 74 * @param type Type of the dependency analysis 75 * @param filter 76 */ 77 public Analyzer(Type type, Filter filter) { 78 this.type = type; 79 this.filter = filter; 80 } 81 82 /** 83 * Performs the dependency analysis on the given archives. 84 */ 85 public boolean run(List<Archive> archives) { 86 // build a map from Location to Archive 87 buildLocationArchiveMap(archives); 88 89 // traverse and analyze all dependencies 90 for (Archive archive : archives) { 91 ArchiveDeps deps = new ArchiveDeps(archive, type); 92 archive.visitDependences(deps); 93 results.put(archive, deps); 94 } 95 return true; 96 } 97 98 protected void buildLocationArchiveMap(List<Archive> archives) { 99 // build a map from Location to Archive 100 for (Archive archive: archives) { 101 for (Location l: archive.getClasses()) { 102 if (!map.containsKey(l)) { 103 map.put(l, archive); 104 } else { 105 // duplicated class warning? 106 } 107 } 108 } 109 } 110 111 public boolean hasDependences(Archive archive) { 112 if (results.containsKey(archive)) { 113 return results.get(archive).dependencies().size() > 0; 114 } 115 return false; 116 } 117 118 public Set<String> dependences(Archive source) { 119 ArchiveDeps result = results.get(source); 120 return result.dependencies().stream() 121 .map(Dep::target) 122 .collect(Collectors.toSet()); 123 } 124 125 public interface Visitor { 126 /** 127 * Visits a recorded dependency from origin to target which can be 128 * a fully-qualified classname, a package name, a module or 129 * archive name depending on the Analyzer's type. 130 */ 131 public void visitDependence(String origin, Archive originArchive, 132 String target, Archive targetArchive); 133 } 134 135 /** 136 * Visit the dependencies of the given source. 137 * If the requested level is SUMMARY, it will visit the required archives list. 138 */ 139 public void visitDependences(Archive source, Visitor v, Type level) { 140 if (level == Type.SUMMARY) { 141 final ArchiveDeps result = results.get(source); 142 final Set<Archive> reqs = result.requires(); 143 Stream<Archive> stream = reqs.stream(); 144 if (reqs.isEmpty()) { 145 if (hasDependences(source)) { 146 // If reqs.isEmpty() and we have dependences, then it means 147 // that the dependences are from 'source' onto itself. 148 stream = Stream.of(source); 149 } 150 } 151 stream.sorted(Comparator.comparing(Archive::getName)) 152 .forEach(archive -> { 153 Profile profile = result.getTargetProfile(archive); 154 v.visitDependence(source.getName(), source, 155 profile != null ? profile.profileName() : archive.getName(), archive); 156 }); 157 } else { 158 ArchiveDeps result = results.get(source); 159 if (level != type) { 160 // requesting different level of analysis 161 result = new ArchiveDeps(source, level); 162 source.visitDependences(result); 163 } 164 result.dependencies().stream() 165 .sorted(Comparator.comparing(Dep::origin) 166 .thenComparing(Dep::target)) 167 .forEach(d -> v.visitDependence(d.origin(), d.originArchive(), d.target(), d.targetArchive())); 168 } 169 } 170 171 public void visitDependences(Archive source, Visitor v) { 172 visitDependences(source, v, type); 173 } 174 175 /** 176 * ArchiveDeps contains the dependencies for an Archive that can have one or 177 * more classes. 178 */ 179 class ArchiveDeps implements Archive.Visitor { 180 protected final Archive archive; 181 protected final Set<Archive> requires; 182 protected final Set<Dep> deps; 183 protected final Type level; 184 private Profile profile; 185 ArchiveDeps(Archive archive, Type level) { 186 this.archive = archive; 187 this.deps = new HashSet<>(); 188 this.requires = new HashSet<>(); 189 this.level = level; 190 } 191 192 Set<Dep> dependencies() { 193 return deps; 194 } 195 196 Set<Archive> requires() { 197 return requires; 198 } 199 200 Profile getTargetProfile(Archive target) { 201 if (target instanceof Module) { 202 return Profile.getProfile((Module) target); 203 } else { 204 return null; 205 } 206 } 207 208 Archive findArchive(Location t) { 209 Archive target = archive.getClasses().contains(t) ? archive : map.get(t); 210 if (target == null) { 211 map.put(t, target = NOT_FOUND); 212 } 213 return target; 214 } 215 216 // return classname or package name depedning on the level 217 private String getLocationName(Location o) { 218 if (level == Type.CLASS || level == Type.VERBOSE) { 219 return o.getClassName(); 220 } else { 221 String pkg = o.getPackageName(); 222 return pkg.isEmpty() ? "<unnamed>" : pkg; 223 } 224 } 225 226 @Override 227 public void visit(Location o, Location t) { 228 Archive targetArchive = findArchive(t); 229 if (filter.accepts(o, archive, t, targetArchive)) { 230 addDep(o, t); 231 if (archive != targetArchive && !requires.contains(targetArchive)) { 232 requires.add(targetArchive); 233 } 234 } 235 if (targetArchive instanceof Module) { 236 Profile p = Profile.getProfile(t.getPackageName()); 237 if (profile == null || (p != null && p.compareTo(profile) > 0)) { 238 profile = p; 239 } 240 } 241 } 242 243 private Dep curDep; 244 protected Dep addDep(Location o, Location t) { 245 String origin = getLocationName(o); 246 String target = getLocationName(t); 247 Archive targetArchive = findArchive(t); 248 if (curDep != null && 249 curDep.origin().equals(origin) && 250 curDep.originArchive() == archive && 251 curDep.target().equals(target) && 252 curDep.targetArchive() == targetArchive) { 253 return curDep; 254 } 255 256 Dep e = new Dep(origin, archive, target, targetArchive); 257 if (deps.contains(e)) { 258 for (Dep e1 : deps) { 259 if (e.equals(e1)) { 260 curDep = e1; 261 } 262 } 263 } else { 264 deps.add(e); 265 curDep = e; 266 } 267 return curDep; 268 } 269 } 270 271 /* 272 * Class-level or package-level dependency 273 */ 274 class Dep { 275 final String origin; 276 final Archive originArchive; 277 final String target; 278 final Archive targetArchive; 279 280 Dep(String origin, Archive originArchive, String target, Archive targetArchive) { 281 this.origin = origin; 282 this.originArchive = originArchive; 283 this.target = target; 284 this.targetArchive = targetArchive; 285 } 286 287 String origin() { 288 return origin; 289 } 290 291 Archive originArchive() { 292 return originArchive; 293 } 294 295 String target() { 296 return target; 297 } 298 299 Archive targetArchive() { 300 return targetArchive; 301 } 302 303 @Override 304 @SuppressWarnings("unchecked") 305 public boolean equals(Object o) { 306 if (o instanceof Dep) { 307 Dep d = (Dep) o; 308 return this.origin.equals(d.origin) && 309 this.originArchive == d.originArchive && 310 this.target.equals(d.target) && 311 this.targetArchive == d.targetArchive; 312 } 313 return false; 314 } 315 316 @Override 317 public int hashCode() { 318 int hash = 7; 319 hash = 67*hash + Objects.hashCode(this.origin) 320 + Objects.hashCode(this.originArchive) 321 + Objects.hashCode(this.target) 322 + Objects.hashCode(this.targetArchive); 323 return hash; 324 } 325 326 public String toString() { 327 return String.format("%s (%s) -> %s (%s)%n", 328 origin, originArchive.getName(), 329 target, targetArchive.getName()); 330 } 331 } 332 333 static Analyzer getExportedAPIsAnalyzer() { 334 return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.reexportsFilter, true); 335 } 336 337 static Analyzer getModuleAccessAnalyzer() { 338 return new ModuleAccessAnalyzer(ModuleAccessAnalyzer.accessCheckFilter, false); 339 } 340 341 private static class ModuleAccessAnalyzer extends Analyzer { 342 private final boolean apionly; 343 ModuleAccessAnalyzer(Filter filter, boolean apionly) { 344 super(Type.VERBOSE, filter); 345 this.apionly = apionly; 346 } 347 /** 348 * Verify module access 349 */ 350 public boolean run(List<Archive> archives) { 351 // build a map from Location to Archive 352 buildLocationArchiveMap(archives); 353 354 // traverse and analyze all dependencies 355 int count = 0; 356 for (Archive archive : archives) { 357 ArchiveDeps checker = new ArchiveDeps(archive, type); 358 archive.visitDependences(checker); 359 count += checker.dependencies().size(); 360 // output if any error 361 Module m = (Module)archive; 362 printDependences(System.err, m, checker.dependencies()); 363 results.put(archive, checker); 364 } 365 return count == 0; 366 } 367 368 private void printDependences(PrintStream out, Module m, Set<Dep> deps) { 369 if (deps.isEmpty()) 370 return; 371 372 String msg = apionly ? "API reference:" : "inaccessible reference:"; 373 deps.stream().sorted(Comparator.comparing(Dep::origin) 374 .thenComparing(Dep::target)) 375 .forEach(d -> out.format("%s %s (%s) -> %s (%s)%n", msg, 376 d.origin(), d.originArchive().getName(), 377 d.target(), d.targetArchive().getName())); 378 if (apionly) { 379 out.format("Dependences missing re-exports=\"true\" attribute:%n"); 380 deps.stream() 381 .map(Dep::targetArchive) 382 .map(Archive::getName) 383 .distinct() 384 .sorted() 385 .forEach(d -> out.format(" %s -> %s%n", m.name(), d)); 386 } 387 } 388 389 private static Module findModule(Archive archive) { 390 if (Module.class.isInstance(archive)) { 391 return (Module) archive; 392 } else { 393 return null; 394 } 395 } 396 397 // returns true if target is accessible by origin 398 private static boolean canAccess(Location o, Archive originArchive, Location t, Archive targetArchive) { 399 Module origin = findModule(originArchive); 400 Module target = findModule(targetArchive); 401 402 if (targetArchive == Analyzer.NOT_FOUND) { 403 return false; 404 } 405 406 // unnamed module 407 // ## should check public type? 408 if (target == null) 409 return true; 410 411 // module-private 412 if (origin == target) 413 return true; 414 415 return target.isAccessibleTo(t.getClassName(), origin); 416 } 417 418 static final Filter accessCheckFilter = new Filter() { 419 @Override 420 public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) { 421 return !canAccess(o, originArchive, t, targetArchive); 422 } 423 }; 424 425 static final Filter reexportsFilter = new Filter() { 426 @Override 427 public boolean accepts(Location o, Archive originArchive, Location t, Archive targetArchive) { 428 Module origin = findModule(originArchive); 429 Module target = findModule(targetArchive); 430 if (!origin.isExportedPackage(o.getPackageName())) { 431 // filter non-exported classes 432 return false; 433 } 434 435 boolean accessible = canAccess(o, originArchive, t, targetArchive); 436 if (!accessible) 437 return true; 438 439 String mn = target.name(); 440 // skip checking re-exports for java.base 441 if (origin == target || "java.base".equals(mn)) 442 return false; 443 444 assert origin.requires().containsKey(mn); // otherwise, should not be accessible 445 if (origin.requires().get(mn)) { 446 return false; 447 } 448 return true; 449 } 450 }; 451 } 452} 453