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