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