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 }