1 /* 2 * Copyright (c) 2016, 2017, 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 26 package com.sun.tools.jdeprscan; 27 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.PrintStream; 31 import java.net.URI; 32 import java.nio.charset.StandardCharsets; 33 import java.nio.file.Files; 34 import java.nio.file.FileSystems; 35 import java.nio.file.Path; 36 import java.nio.file.Paths; 37 import java.util.ArrayDeque; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.Collection; 41 import java.util.EnumSet; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.NoSuchElementException; 46 import java.util.Set; 47 import java.util.Queue; 48 import java.util.stream.Stream; 49 import java.util.stream.StreamSupport; 50 import java.util.jar.JarEntry; 51 import java.util.jar.JarFile; 52 53 import javax.tools.Diagnostic; 54 import javax.tools.DiagnosticListener; 55 import javax.tools.JavaCompiler; 56 import javax.tools.JavaFileManager; 57 import javax.tools.JavaFileObject; 58 import javax.tools.JavaFileObject.Kind; 59 import javax.tools.StandardJavaFileManager; 60 import javax.tools.StandardLocation; 61 import javax.tools.ToolProvider; 62 63 import com.sun.tools.javac.file.JavacFileManager; 64 import com.sun.tools.javac.platform.JDKPlatformProvider; 65 66 import com.sun.tools.jdeprscan.scan.Scan; 67 68 import static java.util.stream.Collectors.*; 69 70 import javax.lang.model.element.PackageElement; 71 import javax.lang.model.element.TypeElement; 72 73 /** 74 * Deprecation Scanner tool. Loads API deprecation information from the 75 * JDK image, or optionally, from a jar file or class hierarchy. Then scans 76 * a class library for usages of those APIs. 77 * 78 * TODO: 79 * - audit error handling throughout, but mainly in scan package 80 * - handling of covariant overrides 81 * - handling of override of method found in multiple superinterfaces 82 * - convert type/method/field output to Java source like syntax, e.g. 83 * instead of java/lang/Runtime.runFinalizersOnExit(Z)V 84 * print void java.lang.Runtime.runFinalizersOnExit(boolean) 85 * - more example output in man page 86 * - more rigorous GNU style option parsing; use joptsimple? 87 * 88 * FUTURES: 89 * - add module support: --add-modules, --module-path, module arg 90 * - load deprecation declarations from a designated class library instead 91 * of the JDK 92 * - load deprecation declarations from a module 93 * - scan a module (but a modular jar can be treated just a like an ordinary jar) 94 * - multi-version jar 95 */ 96 public class Main implements DiagnosticListener<JavaFileObject> { 97 final PrintStream out; 98 final PrintStream err; 99 final List<File> bootClassPath = new ArrayList<>(); 100 final List<File> classPath = new ArrayList<>(); 101 final List<File> systemModules = new ArrayList<>(); 102 final List<String> options = new ArrayList<>(); 103 final List<String> comments = new ArrayList<>(); 104 105 // Valid releases need to match what the compiler supports. 106 // Keep these updated manually until there's a compiler API 107 // that allows querying of supported releases. 108 final Set<String> releasesWithoutForRemoval = Set.of("6", "7", "8"); 109 final Set<String> releasesWithForRemoval = Set.of("9", "10"); 110 111 final Set<String> validReleases; 112 { 113 Set<String> temp = new HashSet<>(releasesWithoutForRemoval); 114 temp.addAll(releasesWithForRemoval); 115 validReleases = Set.of(temp.toArray(new String[0])); 116 } 117 118 boolean verbose = false; 119 boolean forRemoval = false; 120 121 final JavaCompiler compiler; 122 final StandardJavaFileManager fm; 123 124 List<DeprData> deprList; // non-null after successful load phase 125 126 /** 127 * Processes a collection of class names. Names should fully qualified 128 * names in the form "pkg.pkg.pkg.classname". 129 * 130 * @param classNames collection of fully qualified classnames to process 131 * @return true for success, false for failure 132 * @throws IOException if an I/O error occurs 133 */ 134 boolean doClassNames(Collection<String> classNames) throws IOException { 135 if (verbose) { 136 out.println("List of classes to process:"); 137 classNames.forEach(out::println); 138 out.println("End of class list."); 139 } 140 141 // TODO: not sure this is necessary... 142 if (fm instanceof JavacFileManager) { 143 ((JavacFileManager)fm).setSymbolFileEnabled(false); 144 } 145 146 fm.setLocation(StandardLocation.CLASS_PATH, classPath); 147 if (!bootClassPath.isEmpty()) { 148 fm.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath); 149 } 150 151 if (!systemModules.isEmpty()) { 152 fm.setLocation(StandardLocation.SYSTEM_MODULES, systemModules); 153 } 154 155 LoadProc proc = new LoadProc(); 156 JavaCompiler.CompilationTask task = 157 compiler.getTask(null, fm, this, options, classNames, null); 158 task.setProcessors(List.of(proc)); 159 boolean r = task.call(); 160 if (r) { 161 if (forRemoval) { 162 deprList = proc.getDeprecations().stream() 163 .filter(DeprData::isForRemoval) 164 .collect(toList()); 165 } else { 166 deprList = proc.getDeprecations(); 167 } 168 } 169 return r; 170 } 171 172 /** 173 * Processes a stream of filenames (strings). The strings are in the 174 * form pkg/pkg/pkg/classname.class relative to the root of a package 175 * hierarchy. 176 * 177 * @param filenames a Stream of filenames to process 178 * @return true for success, false for failure 179 * @throws IOException if an I/O error occurs 180 */ 181 boolean doFileNames(Stream<String> filenames) throws IOException { 182 return doClassNames( 183 filenames.filter(name -> name.endsWith(".class")) 184 .filter(name -> !name.endsWith("package-info.class")) 185 .filter(name -> !name.endsWith("module-info.class")) 186 .map(s -> s.replaceAll("\\.class$", "")) 187 .map(s -> s.replace(File.separatorChar, '.')) 188 .collect(toList())); 189 } 190 191 /** 192 * Replaces all but the first occurrence of '/' with '.'. Assumes 193 * that the name is in the format module/pkg/pkg/classname.class. 194 * That is, the name should contain at least one '/' character 195 * separating the module name from the package-class name. 196 * 197 * @param filename the input filename 198 * @return the modular classname 199 */ 200 String convertModularFileName(String filename) { 201 int slash = filename.indexOf('/'); 202 return filename.substring(0, slash) 203 + "/" 204 + filename.substring(slash+1).replace('/', '.'); 205 } 206 207 /** 208 * Processes a stream of filenames (strings) including a module prefix. 209 * The strings are in the form module/pkg/pkg/pkg/classname.class relative 210 * to the root of a directory containing modules. The strings are processed 211 * into module-qualified class names of the form 212 * "module/pkg.pkg.pkg.classname". 213 * 214 * @param filenames a Stream of filenames to process 215 * @return true for success, false for failure 216 * @throws IOException if an I/O error occurs 217 */ 218 boolean doModularFileNames(Stream<String> filenames) throws IOException { 219 return doClassNames( 220 filenames.filter(name -> name.endsWith(".class")) 221 .filter(name -> !name.endsWith("package-info.class")) 222 .filter(name -> !name.endsWith("module-info.class")) 223 .map(s -> s.replaceAll("\\.class$", "")) 224 .map(this::convertModularFileName) 225 .collect(toList())); 226 } 227 228 /** 229 * Processes named class files in the given directory. The directory 230 * should be the root of a package hierarchy. If classNames is 231 * empty, walks the directory hierarchy to find all classes. 232 * 233 * @param dirname the name of the directory to process 234 * @param classNames the names of classes to process 235 * @return true for success, false for failure 236 * @throws IOException if an I/O error occurs 237 */ 238 boolean processDirectory(String dirname, Collection<String> classNames) throws IOException { 239 if (!Files.isDirectory(Paths.get(dirname))) { 240 err.printf("%s: not a directory%n", dirname); 241 return false; 242 } 243 244 classPath.add(0, new File(dirname)); 245 246 if (classNames.isEmpty()) { 247 Path base = Paths.get(dirname); 248 int baseCount = base.getNameCount(); 249 try (Stream<Path> paths = Files.walk(base)) { 250 Stream<String> files = 251 paths.filter(p -> p.getNameCount() > baseCount) 252 .map(p -> p.subpath(baseCount, p.getNameCount())) 253 .map(Path::toString); 254 return doFileNames(files); 255 } 256 } else { 257 return doClassNames(classNames); 258 } 259 } 260 261 /** 262 * Processes all class files in the given jar file. 263 * 264 * @param jarname the name of the jar file to process 265 * @return true for success, false for failure 266 * @throws IOException if an I/O error occurs 267 */ 268 boolean doJarFile(String jarname) throws IOException { 269 try (JarFile jf = new JarFile(jarname)) { 270 Stream<String> files = 271 jf.stream() 272 .map(JarEntry::getName); 273 return doFileNames(files); 274 } 275 } 276 277 /** 278 * Processes named class files from the given jar file, 279 * or all classes if classNames is empty. 280 * 281 * @param jarname the name of the jar file to process 282 * @param classNames the names of classes to process 283 * @return true for success, false for failure 284 * @throws IOException if an I/O error occurs 285 */ 286 boolean processJarFile(String jarname, Collection<String> classNames) throws IOException { 287 classPath.add(0, new File(jarname)); 288 289 if (classNames.isEmpty()) { 290 return doJarFile(jarname); 291 } else { 292 return doClassNames(classNames); 293 } 294 } 295 296 /** 297 * Processes named class files from rt.jar of a JDK version 7 or 8. 298 * If classNames is empty, processes all classes. 299 * 300 * @param jdkHome the path to the "home" of the JDK to process 301 * @param classNames the names of classes to process 302 * @return true for success, false for failure 303 * @throws IOException if an I/O error occurs 304 */ 305 boolean processOldJdk(String jdkHome, Collection<String> classNames) throws IOException { 306 String RTJAR = jdkHome + "/jre/lib/rt.jar"; 307 String CSJAR = jdkHome + "/jre/lib/charsets.jar"; 308 309 bootClassPath.add(0, new File(RTJAR)); 310 bootClassPath.add(1, new File(CSJAR)); 311 options.add("-source"); 312 options.add("8"); 313 314 if (classNames.isEmpty()) { 315 return doJarFile(RTJAR); 316 } else { 317 return doClassNames(classNames); 318 } 319 } 320 321 /** 322 * Processes listed classes given a JDK 9 home. 323 */ 324 boolean processJdk9(String jdkHome, Collection<String> classes) throws IOException { 325 systemModules.add(new File(jdkHome)); 326 return doClassNames(classes); 327 } 328 329 /** 330 * Processes the class files from the currently running JDK, 331 * using the jrt: filesystem. 332 * 333 * @return true for success, false for failure 334 * @throws IOException if an I/O error occurs 335 */ 336 boolean processSelf(Collection<String> classes) throws IOException { 337 options.add("--add-modules"); 338 options.add("java.se.ee,jdk.xml.bind"); // TODO why jdk.xml.bind? 339 340 if (classes.isEmpty()) { 341 Path modules = FileSystems.getFileSystem(URI.create("jrt:/")) 342 .getPath("/modules"); 343 344 // names are /modules/<modulename>/pkg/.../Classname.class 345 try (Stream<Path> paths = Files.walk(modules)) { 346 Stream<String> files = 347 paths.filter(p -> p.getNameCount() > 2) 348 .map(p -> p.subpath(1, p.getNameCount())) 349 .map(Path::toString); 350 return doModularFileNames(files); 351 } 352 } else { 353 return doClassNames(classes); 354 } 355 } 356 357 /** 358 * Process classes from a particular JDK release, using only information 359 * in this JDK. 360 * 361 * @param release "6", "7", "8", "9", or "10" 362 * @param classes collection of classes to process, may be empty 363 * @return success value 364 */ 365 boolean processRelease(String release, Collection<String> classes) throws IOException { 366 options.addAll(List.of("--release", release)); 367 368 if (release.equals("9") || release.equals("10")) { 369 List<String> rootMods = List.of("java.se", "java.se.ee"); 370 TraverseProc proc = new TraverseProc(rootMods); 371 JavaCompiler.CompilationTask task = 372 compiler.getTask(null, fm, this, 373 // options 374 List.of("--add-modules", String.join(",", rootMods)), 375 // classes 376 List.of("java.lang.Object"), 377 null); 378 task.setProcessors(List.of(proc)); 379 if (!task.call()) { 380 return false; 381 } 382 Map<PackageElement, List<TypeElement>> types = proc.getPublicTypes(); 383 options.add("--add-modules"); 384 options.add(String.join(",", rootMods)); 385 return doClassNames( 386 types.values().stream() 387 .flatMap(List::stream) 388 .map(TypeElement::toString) 389 .collect(toList())); 390 } else { 391 JDKPlatformProvider pp = new JDKPlatformProvider(); 392 if (StreamSupport.stream(pp.getSupportedPlatformNames().spliterator(), 393 false) 394 .noneMatch(n -> n.equals(release))) { 395 return false; 396 } 397 JavaFileManager fm = pp.getPlatform(release, "").getFileManager(); 398 List<String> classNames = new ArrayList<>(); 399 for (JavaFileObject fo : fm.list(StandardLocation.PLATFORM_CLASS_PATH, 400 "", 401 EnumSet.of(Kind.CLASS), 402 true)) { 403 classNames.add(fm.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, fo)); 404 } 405 406 options.add("-Xlint:-options"); 407 408 return doClassNames(classNames); 409 } 410 } 411 412 /** 413 * An enum denoting the mode in which the tool is running. 414 * Different modes correspond to the different process* methods. 415 * The exception is UNKNOWN, which indicates that a mode wasn't 416 * specified on the command line, which is an error. 417 */ 418 static enum LoadMode { 419 CLASSES, DIR, JAR, OLD_JDK, JDK9, SELF, RELEASE, LOAD_CSV 420 } 421 422 static enum ScanMode { 423 ARGS, LIST, PRINT_CSV 424 } 425 426 /** 427 * A checked exception that's thrown if a command-line syntax error 428 * is detected. 429 */ 430 static class UsageException extends Exception { 431 private static final long serialVersionUID = 3611828659572908743L; 432 } 433 434 /** 435 * Convenience method to throw UsageException if a condition is false. 436 * 437 * @param cond the condition that's required to be true 438 * @throws UsageException 439 */ 440 void require(boolean cond) throws UsageException { 441 if (!cond) { 442 throw new UsageException(); 443 } 444 } 445 446 /** 447 * Constructs an instance of the finder tool. 448 * 449 * @param out the stream to which the tool's output is sent 450 * @param err the stream to which error messages are sent 451 */ 452 Main(PrintStream out, PrintStream err) { 453 this.out = out; 454 this.err = err; 455 compiler = ToolProvider.getSystemJavaCompiler(); 456 fm = compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8); 457 } 458 459 /** 460 * Prints the diagnostic to the err stream. 461 * 462 * Specified by the DiagnosticListener interface. 463 * 464 * @param diagnostic the tool diagnostic to print 465 */ 466 @Override 467 public void report(Diagnostic<? extends JavaFileObject> diagnostic) { 468 err.println(diagnostic); 469 } 470 471 /** 472 * Parses arguments and performs the requested processing. 473 * 474 * @param argArray command-line arguments 475 * @return true on success, false on error 476 */ 477 boolean run(String... argArray) { 478 Queue<String> args = new ArrayDeque<>(Arrays.asList(argArray)); 479 LoadMode loadMode = LoadMode.RELEASE; 480 ScanMode scanMode = ScanMode.ARGS; 481 String dir = null; 482 String jar = null; 483 String jdkHome = null; 484 String release = "10"; 485 List<String> loadClasses = new ArrayList<>(); 486 String csvFile = null; 487 488 try { 489 while (!args.isEmpty()) { 490 String a = args.element(); 491 if (a.startsWith("-")) { 492 args.remove(); 493 switch (a) { 494 case "--class-path": 495 classPath.clear(); 496 Arrays.stream(args.remove().split(File.pathSeparator)) 497 .map(File::new) 498 .forEachOrdered(classPath::add); 499 break; 500 case "--for-removal": 501 forRemoval = true; 502 break; 503 case "--full-version": 504 out.println(System.getProperty("java.vm.version")); 505 return false; 506 case "--help": 507 case "-h": 508 out.println(Messages.get("main.usage")); 509 out.println(); 510 out.println(Messages.get("main.help")); 511 return false; 512 case "-l": 513 case "--list": 514 require(scanMode == ScanMode.ARGS); 515 scanMode = ScanMode.LIST; 516 break; 517 case "--release": 518 loadMode = LoadMode.RELEASE; 519 release = args.remove(); 520 if (!validReleases.contains(release)) { 521 throw new UsageException(); 522 } 523 break; 524 case "-v": 525 case "--verbose": 526 verbose = true; 527 break; 528 case "--version": 529 out.println(System.getProperty("java.version")); 530 return false; 531 case "--Xcompiler-arg": 532 options.add(args.remove()); 533 break; 534 case "--Xcsv-comment": 535 comments.add(args.remove()); 536 break; 537 case "--Xhelp": 538 out.println(Messages.get("main.xhelp")); 539 return false; 540 case "--Xload-class": 541 loadMode = LoadMode.CLASSES; 542 loadClasses.add(args.remove()); 543 break; 544 case "--Xload-csv": 545 loadMode = LoadMode.LOAD_CSV; 546 csvFile = args.remove(); 547 break; 548 case "--Xload-dir": 549 loadMode = LoadMode.DIR; 550 dir = args.remove(); 551 break; 552 case "--Xload-jar": 553 loadMode = LoadMode.JAR; 554 jar = args.remove(); 555 break; 556 case "--Xload-jdk9": 557 loadMode = LoadMode.JDK9; 558 jdkHome = args.remove(); 559 break; 560 case "--Xload-old-jdk": 561 loadMode = LoadMode.OLD_JDK; 562 jdkHome = args.remove(); 563 break; 564 case "--Xload-self": 565 loadMode = LoadMode.SELF; 566 break; 567 case "--Xprint-csv": 568 require(scanMode == ScanMode.ARGS); 569 scanMode = ScanMode.PRINT_CSV; 570 break; 571 default: 572 throw new UsageException(); 573 } 574 } else { 575 break; 576 } 577 } 578 579 if ((scanMode == ScanMode.ARGS) == args.isEmpty()) { 580 throw new UsageException(); 581 } 582 583 if ( forRemoval && loadMode == LoadMode.RELEASE && 584 releasesWithoutForRemoval.contains(release)) { 585 throw new UsageException(); 586 } 587 588 boolean success = false; 589 590 switch (loadMode) { 591 case CLASSES: 592 success = doClassNames(loadClasses); 593 break; 594 case DIR: 595 success = processDirectory(dir, loadClasses); 596 break; 597 case JAR: 598 success = processJarFile(jar, loadClasses); 599 break; 600 case JDK9: 601 require(!args.isEmpty()); 602 success = processJdk9(jdkHome, loadClasses); 603 break; 604 case LOAD_CSV: 605 deprList = DeprDB.loadFromFile(csvFile); 606 success = true; 607 break; 608 case OLD_JDK: 609 success = processOldJdk(jdkHome, loadClasses); 610 break; 611 case RELEASE: 612 success = processRelease(release, loadClasses); 613 break; 614 case SELF: 615 success = processSelf(loadClasses); 616 break; 617 default: 618 throw new UsageException(); 619 } 620 621 if (!success) { 622 return false; 623 } 624 } catch (NoSuchElementException | UsageException ex) { 625 err.println(Messages.get("main.usage")); 626 return false; 627 } catch (IOException ioe) { 628 if (verbose) { 629 ioe.printStackTrace(err); 630 } else { 631 err.println(ioe); 632 } 633 return false; 634 } 635 636 // now the scanning phase 637 638 boolean scanStatus = true; 639 640 switch (scanMode) { 641 case LIST: 642 for (DeprData dd : deprList) { 643 if (!forRemoval || dd.isForRemoval()) { 644 out.println(Pretty.print(dd)); 645 } 646 } 647 break; 648 case PRINT_CSV: 649 out.println("#jdepr1"); 650 comments.forEach(s -> out.println("# " + s)); 651 for (DeprData dd : deprList) { 652 CSV.write(out, dd.kind, dd.typeName, dd.nameSig, dd.since, dd.forRemoval); 653 } 654 break; 655 case ARGS: 656 DeprDB db = DeprDB.loadFromList(deprList); 657 List<String> cp = classPath.stream() 658 .map(File::toString) 659 .collect(toList()); 660 Scan scan = new Scan(out, err, cp, db, verbose); 661 662 for (String a : args) { 663 boolean s; 664 if (a.endsWith(".jar")) { 665 s = scan.scanJar(a); 666 } else if (a.endsWith(".class")) { 667 s = scan.processClassFile(a); 668 } else if (Files.isDirectory(Paths.get(a))) { 669 s = scan.scanDir(a); 670 } else { 671 s = scan.processClassName(a.replace('.', '/')); 672 } 673 scanStatus = scanStatus && s; 674 } 675 break; 676 } 677 678 return scanStatus; 679 } 680 681 /** 682 * Programmatic main entry point: initializes the tool instance to 683 * use stdout and stderr; runs the tool, passing command-line args; 684 * returns an exit status. 685 * 686 * @return true on success, false otherwise 687 */ 688 public static boolean call(PrintStream out, PrintStream err, String... args) { 689 return new Main(out, err).run(args); 690 } 691 692 /** 693 * Calls the main entry point and exits the JVM with an exit 694 * status determined by the return status. 695 */ 696 public static void main(String[] args) { 697 System.exit(call(System.out, System.err, args) ? 0 : 1); 698 } 699 }