1 /* 2 * Copyright (c) 2016, 2018, 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/Character.isJavaLetter(C)Z 84 * print void java.lang.Character.isJavaLetter(char)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", "11", "12", "13"); 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"); 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 a supported release version, like "8" 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 boolean hasModules; 367 boolean hasJavaSE_EE; 368 369 try { 370 int releaseNum = Integer.parseInt(release); 371 372 hasModules = releaseNum >= 9; 373 hasJavaSE_EE = hasModules && releaseNum <= 10; 374 } catch (NumberFormatException ex) { 375 hasModules = true; 376 hasJavaSE_EE = false; 377 } 378 379 options.addAll(List.of("--release", release)); 380 381 if (hasModules) { 382 List<String> rootMods = hasJavaSE_EE ? List.of("java.se", "java.se.ee") 383 : List.of("java.se"); 384 TraverseProc proc = new TraverseProc(rootMods); 385 JavaCompiler.CompilationTask task = 386 compiler.getTask(null, fm, this, 387 // options 388 List.of("--add-modules", String.join(",", rootMods), 389 "--release", release), 390 // classes 391 List.of("java.lang.Object"), 392 null); 393 task.setProcessors(List.of(proc)); 394 if (!task.call()) { 395 return false; 396 } 397 Map<PackageElement, List<TypeElement>> types = proc.getPublicTypes(); 398 options.add("--add-modules"); 399 options.add(String.join(",", rootMods)); 400 return doClassNames( 401 types.values().stream() 402 .flatMap(List::stream) 403 .map(TypeElement::toString) 404 .collect(toList())); 405 } else { 406 JDKPlatformProvider pp = new JDKPlatformProvider(); 407 if (StreamSupport.stream(pp.getSupportedPlatformNames().spliterator(), 408 false) 409 .noneMatch(n -> n.equals(release))) { 410 return false; 411 } 412 JavaFileManager fm = pp.getPlatform(release, "").getFileManager(); 413 List<String> classNames = new ArrayList<>(); 414 for (JavaFileObject fo : fm.list(StandardLocation.PLATFORM_CLASS_PATH, 415 "", 416 EnumSet.of(Kind.CLASS), 417 true)) { 418 classNames.add(fm.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, fo)); 419 } 420 421 options.add("-Xlint:-options"); 422 423 return doClassNames(classNames); 424 } 425 } 426 427 /** 428 * An enum denoting the mode in which the tool is running. 429 * Different modes correspond to the different process* methods. 430 * The exception is UNKNOWN, which indicates that a mode wasn't 431 * specified on the command line, which is an error. 432 */ 433 static enum LoadMode { 434 CLASSES, DIR, JAR, OLD_JDK, JDK9, SELF, RELEASE, LOAD_CSV 435 } 436 437 static enum ScanMode { 438 ARGS, LIST, PRINT_CSV 439 } 440 441 /** 442 * A checked exception that's thrown if a command-line syntax error 443 * is detected. 444 */ 445 static class UsageException extends Exception { 446 private static final long serialVersionUID = 3611828659572908743L; 447 } 448 449 /** 450 * Convenience method to throw UsageException if a condition is false. 451 * 452 * @param cond the condition that's required to be true 453 * @throws UsageException 454 */ 455 void require(boolean cond) throws UsageException { 456 if (!cond) { 457 throw new UsageException(); 458 } 459 } 460 461 /** 462 * Constructs an instance of the finder tool. 463 * 464 * @param out the stream to which the tool's output is sent 465 * @param err the stream to which error messages are sent 466 */ 467 Main(PrintStream out, PrintStream err) { 468 this.out = out; 469 this.err = err; 470 compiler = ToolProvider.getSystemJavaCompiler(); 471 fm = compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8); 472 } 473 474 /** 475 * Prints the diagnostic to the err stream. 476 * 477 * Specified by the DiagnosticListener interface. 478 * 479 * @param diagnostic the tool diagnostic to print 480 */ 481 @Override 482 public void report(Diagnostic<? extends JavaFileObject> diagnostic) { 483 err.println(diagnostic); 484 } 485 486 /** 487 * Parses arguments and performs the requested processing. 488 * 489 * @param argArray command-line arguments 490 * @return true on success, false on error 491 */ 492 boolean run(String... argArray) { 493 Queue<String> args = new ArrayDeque<>(Arrays.asList(argArray)); 494 LoadMode loadMode = LoadMode.RELEASE; 495 ScanMode scanMode = ScanMode.ARGS; 496 String dir = null; 497 String jar = null; 498 String jdkHome = null; 499 String release = Integer.toString(Runtime.version().feature()); 500 List<String> loadClasses = new ArrayList<>(); 501 String csvFile = null; 502 503 try { 504 while (!args.isEmpty()) { 505 String a = args.element(); 506 if (a.startsWith("-")) { 507 args.remove(); 508 switch (a) { 509 case "--class-path": 510 classPath.clear(); 511 Arrays.stream(args.remove().split(File.pathSeparator)) 512 .map(File::new) 513 .forEachOrdered(classPath::add); 514 break; 515 case "--for-removal": 516 forRemoval = true; 517 break; 518 case "--full-version": 519 out.println(System.getProperty("java.vm.version")); 520 return false; 521 case "--help": 522 case "-h": 523 case "-?": 524 printHelp(out); 525 out.println(); 526 out.println(Messages.get("main.help")); 527 return true; 528 case "-l": 529 case "--list": 530 require(scanMode == ScanMode.ARGS); 531 scanMode = ScanMode.LIST; 532 break; 533 case "--release": 534 loadMode = LoadMode.RELEASE; 535 release = args.remove(); 536 if (!validReleases.contains(release)) { 537 throw new UsageException(); 538 } 539 break; 540 case "-v": 541 case "--verbose": 542 verbose = true; 543 break; 544 case "--version": 545 out.println(System.getProperty("java.version")); 546 return false; 547 case "--Xcompiler-arg": 548 options.add(args.remove()); 549 break; 550 case "--Xcsv-comment": 551 comments.add(args.remove()); 552 break; 553 case "--Xhelp": 554 out.println(Messages.get("main.xhelp")); 555 return false; 556 case "--Xload-class": 557 loadMode = LoadMode.CLASSES; 558 loadClasses.add(args.remove()); 559 break; 560 case "--Xload-csv": 561 loadMode = LoadMode.LOAD_CSV; 562 csvFile = args.remove(); 563 break; 564 case "--Xload-dir": 565 loadMode = LoadMode.DIR; 566 dir = args.remove(); 567 break; 568 case "--Xload-jar": 569 loadMode = LoadMode.JAR; 570 jar = args.remove(); 571 break; 572 case "--Xload-jdk9": 573 loadMode = LoadMode.JDK9; 574 jdkHome = args.remove(); 575 break; 576 case "--Xload-old-jdk": 577 loadMode = LoadMode.OLD_JDK; 578 jdkHome = args.remove(); 579 break; 580 case "--Xload-self": 581 loadMode = LoadMode.SELF; 582 break; 583 case "--Xprint-csv": 584 require(scanMode == ScanMode.ARGS); 585 scanMode = ScanMode.PRINT_CSV; 586 break; 587 default: 588 throw new UsageException(); 589 } 590 } else { 591 break; 592 } 593 } 594 595 if ((scanMode == ScanMode.ARGS) == args.isEmpty()) { 596 throw new UsageException(); 597 } 598 599 if ( forRemoval && loadMode == LoadMode.RELEASE && 600 releasesWithoutForRemoval.contains(release)) { 601 throw new UsageException(); 602 } 603 604 boolean success = false; 605 606 switch (loadMode) { 607 case CLASSES: 608 success = doClassNames(loadClasses); 609 break; 610 case DIR: 611 success = processDirectory(dir, loadClasses); 612 break; 613 case JAR: 614 success = processJarFile(jar, loadClasses); 615 break; 616 case JDK9: 617 require(!args.isEmpty()); 618 success = processJdk9(jdkHome, loadClasses); 619 break; 620 case LOAD_CSV: 621 deprList = DeprDB.loadFromFile(csvFile); 622 success = true; 623 break; 624 case OLD_JDK: 625 success = processOldJdk(jdkHome, loadClasses); 626 break; 627 case RELEASE: 628 success = processRelease(release, loadClasses); 629 break; 630 case SELF: 631 success = processSelf(loadClasses); 632 break; 633 default: 634 throw new UsageException(); 635 } 636 637 if (!success) { 638 return false; 639 } 640 } catch (NoSuchElementException | UsageException ex) { 641 printHelp(err); 642 return false; 643 } catch (IOException ioe) { 644 if (verbose) { 645 ioe.printStackTrace(err); 646 } else { 647 err.println(ioe); 648 } 649 return false; 650 } 651 652 // now the scanning phase 653 654 boolean scanStatus = true; 655 656 switch (scanMode) { 657 case LIST: 658 for (DeprData dd : deprList) { 659 if (!forRemoval || dd.isForRemoval()) { 660 out.println(Pretty.print(dd)); 661 } 662 } 663 break; 664 case PRINT_CSV: 665 out.println("#jdepr1"); 666 comments.forEach(s -> out.println("# " + s)); 667 for (DeprData dd : deprList) { 668 CSV.write(out, dd.kind, dd.typeName, dd.nameSig, dd.since, dd.forRemoval); 669 } 670 break; 671 case ARGS: 672 DeprDB db = DeprDB.loadFromList(deprList); 673 List<String> cp = classPath.stream() 674 .map(File::toString) 675 .collect(toList()); 676 Scan scan = new Scan(out, err, cp, db, verbose); 677 678 for (String a : args) { 679 boolean s; 680 if (a.endsWith(".jar")) { 681 s = scan.scanJar(a); 682 } else if (a.endsWith(".class")) { 683 s = scan.processClassFile(a); 684 } else if (Files.isDirectory(Paths.get(a))) { 685 s = scan.scanDir(a); 686 } else { 687 s = scan.processClassName(a.replace('.', '/')); 688 } 689 scanStatus = scanStatus && s; 690 } 691 break; 692 } 693 694 return scanStatus; 695 } 696 697 private void printHelp(PrintStream out) { 698 JDKPlatformProvider pp = new JDKPlatformProvider(); 699 String supportedReleases = 700 String.join("|", pp.getSupportedPlatformNames()); 701 out.println(Messages.get("main.usage", supportedReleases)); 702 } 703 704 /** 705 * Programmatic main entry point: initializes the tool instance to 706 * use stdout and stderr; runs the tool, passing command-line args; 707 * returns an exit status. 708 * 709 * @return true on success, false otherwise 710 */ 711 public static boolean call(PrintStream out, PrintStream err, String... args) { 712 return new Main(out, err).run(args); 713 } 714 715 /** 716 * Calls the main entry point and exits the JVM with an exit 717 * status determined by the return status. 718 */ 719 public static void main(String[] args) { 720 System.exit(call(System.out, System.err, args) ? 0 : 1); 721 } 722 }