blob: 155f1be1d9091d07ad033455a17069fff7ff9317 [file] [log] [blame]
/*
* Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.jdeprscan;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Queue;
import java.util.stream.Stream;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.jdeprscan.scan.Scan;
import static java.util.stream.Collectors.*;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
/**
* Deprecation Scanner tool. Loads API deprecation information from the
* JDK image, or optionally, from a jar file or class hierarchy. Then scans
* a class library for usages of those APIs.
*
* TODO:
* - audit error handling throughout, but mainly in scan package
* - handling of covariant overrides
* - handling of override of method found in multiple superinterfaces
* - convert type/method/field output to Java source like syntax, e.g.
* instead of java/lang/Runtime.runFinalizersOnExit(Z)V
* print void java.lang.Runtime.runFinalizersOnExit(boolean)
* - more example output in man page
* - more rigorous GNU style option parsing; use joptsimple?
*
* FUTURES:
* - add module support: --add-modules, --module-path, module arg
* - load deprecation declarations from a designated class library instead
* of the JDK
* - load deprecation declarations from a module
* - scan a module (but a modular jar can be treated just a like an ordinary jar)
* - multi-version jar
*/
public class Main implements DiagnosticListener<JavaFileObject> {
final PrintStream out;
final PrintStream err;
final List<File> bootClassPath = new ArrayList<>();
final List<File> classPath = new ArrayList<>();
final List<File> systemModules = new ArrayList<>();
final List<String> options = new ArrayList<>();
final List<String> comments = new ArrayList<>();
// Valid releases need to match what the compiler supports.
// Keep these updated manually until there's a compiler API
// that allows querying of supported releases.
final Set<String> releasesWithoutForRemoval = Set.of("6", "7", "8");
final Set<String> releasesWithForRemoval = Set.of("9", "10");
final Set<String> validReleases;
{
Set<String> temp = new HashSet<>(releasesWithoutForRemoval);
temp.addAll(releasesWithForRemoval);
validReleases = Set.of(temp.toArray(new String[0]));
}
boolean verbose = false;
boolean forRemoval = false;
final JavaCompiler compiler;
final StandardJavaFileManager fm;
List<DeprData> deprList; // non-null after successful load phase
/**
* Processes a collection of class names. Names should fully qualified
* names in the form "pkg.pkg.pkg.classname".
*
* @param classNames collection of fully qualified classnames to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean doClassNames(Collection<String> classNames) throws IOException {
if (verbose) {
out.println("List of classes to process:");
classNames.forEach(out::println);
out.println("End of class list.");
}
// TODO: not sure this is necessary...
if (fm instanceof JavacFileManager) {
((JavacFileManager)fm).setSymbolFileEnabled(false);
}
fm.setLocation(StandardLocation.CLASS_PATH, classPath);
if (!bootClassPath.isEmpty()) {
fm.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath);
}
if (!systemModules.isEmpty()) {
fm.setLocation(StandardLocation.SYSTEM_MODULES, systemModules);
}
LoadProc proc = new LoadProc();
JavaCompiler.CompilationTask task =
compiler.getTask(null, fm, this, options, classNames, null);
task.setProcessors(List.of(proc));
boolean r = task.call();
if (r) {
if (forRemoval) {
deprList = proc.getDeprecations().stream()
.filter(DeprData::isForRemoval)
.collect(toList());
} else {
deprList = proc.getDeprecations();
}
}
return r;
}
/**
* Processes a stream of filenames (strings). The strings are in the
* form pkg/pkg/pkg/classname.class relative to the root of a package
* hierarchy.
*
* @param filenames a Stream of filenames to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean doFileNames(Stream<String> filenames) throws IOException {
return doClassNames(
filenames.filter(name -> name.endsWith(".class"))
.filter(name -> !name.endsWith("package-info.class"))
.filter(name -> !name.endsWith("module-info.class"))
.map(s -> s.replaceAll("\\.class$", ""))
.map(s -> s.replace(File.separatorChar, '.'))
.collect(toList()));
}
/**
* Replaces all but the first occurrence of '/' with '.'. Assumes
* that the name is in the format module/pkg/pkg/classname.class.
* That is, the name should contain at least one '/' character
* separating the module name from the package-class name.
*
* @param filename the input filename
* @return the modular classname
*/
String convertModularFileName(String filename) {
int slash = filename.indexOf('/');
return filename.substring(0, slash)
+ "/"
+ filename.substring(slash+1).replace('/', '.');
}
/**
* Processes a stream of filenames (strings) including a module prefix.
* The strings are in the form module/pkg/pkg/pkg/classname.class relative
* to the root of a directory containing modules. The strings are processed
* into module-qualified class names of the form
* "module/pkg.pkg.pkg.classname".
*
* @param filenames a Stream of filenames to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean doModularFileNames(Stream<String> filenames) throws IOException {
return doClassNames(
filenames.filter(name -> name.endsWith(".class"))
.filter(name -> !name.endsWith("package-info.class"))
.filter(name -> !name.endsWith("module-info.class"))
.map(s -> s.replaceAll("\\.class$", ""))
.map(this::convertModularFileName)
.collect(toList()));
}
/**
* Processes named class files in the given directory. The directory
* should be the root of a package hierarchy. If classNames is
* empty, walks the directory hierarchy to find all classes.
*
* @param dirname the name of the directory to process
* @param classNames the names of classes to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean processDirectory(String dirname, Collection<String> classNames) throws IOException {
if (!Files.isDirectory(Paths.get(dirname))) {
err.printf("%s: not a directory%n", dirname);
return false;
}
classPath.add(0, new File(dirname));
if (classNames.isEmpty()) {
Path base = Paths.get(dirname);
int baseCount = base.getNameCount();
try (Stream<Path> paths = Files.walk(base)) {
Stream<String> files =
paths.filter(p -> p.getNameCount() > baseCount)
.map(p -> p.subpath(baseCount, p.getNameCount()))
.map(Path::toString);
return doFileNames(files);
}
} else {
return doClassNames(classNames);
}
}
/**
* Processes all class files in the given jar file.
*
* @param jarname the name of the jar file to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean doJarFile(String jarname) throws IOException {
try (JarFile jf = new JarFile(jarname)) {
Stream<String> files =
jf.stream()
.map(JarEntry::getName);
return doFileNames(files);
}
}
/**
* Processes named class files from the given jar file,
* or all classes if classNames is empty.
*
* @param jarname the name of the jar file to process
* @param classNames the names of classes to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean processJarFile(String jarname, Collection<String> classNames) throws IOException {
classPath.add(0, new File(jarname));
if (classNames.isEmpty()) {
return doJarFile(jarname);
} else {
return doClassNames(classNames);
}
}
/**
* Processes named class files from rt.jar of a JDK version 7 or 8.
* If classNames is empty, processes all classes.
*
* @param jdkHome the path to the "home" of the JDK to process
* @param classNames the names of classes to process
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean processOldJdk(String jdkHome, Collection<String> classNames) throws IOException {
String RTJAR = jdkHome + "/jre/lib/rt.jar";
String CSJAR = jdkHome + "/jre/lib/charsets.jar";
bootClassPath.add(0, new File(RTJAR));
bootClassPath.add(1, new File(CSJAR));
options.add("-source");
options.add("8");
if (classNames.isEmpty()) {
return doJarFile(RTJAR);
} else {
return doClassNames(classNames);
}
}
/**
* Processes listed classes given a JDK 9 home.
*/
boolean processJdk9(String jdkHome, Collection<String> classes) throws IOException {
systemModules.add(new File(jdkHome));
return doClassNames(classes);
}
/**
* Processes the class files from the currently running JDK,
* using the jrt: filesystem.
*
* @return true for success, false for failure
* @throws IOException if an I/O error occurs
*/
boolean processSelf(Collection<String> classes) throws IOException {
options.add("--add-modules");
options.add("java.se.ee,jdk.xml.bind"); // TODO why jdk.xml.bind?
if (classes.isEmpty()) {
Path modules = FileSystems.getFileSystem(URI.create("jrt:/"))
.getPath("/modules");
// names are /modules/<modulename>/pkg/.../Classname.class
try (Stream<Path> paths = Files.walk(modules)) {
Stream<String> files =
paths.filter(p -> p.getNameCount() > 2)
.map(p -> p.subpath(1, p.getNameCount()))
.map(Path::toString);
return doModularFileNames(files);
}
} else {
return doClassNames(classes);
}
}
/**
* Process classes from a particular JDK release, using only information
* in this JDK.
*
* @param release "6", "7", "8", "9", or "10"
* @param classes collection of classes to process, may be empty
* @return success value
*/
boolean processRelease(String release, Collection<String> classes) throws IOException {
options.addAll(List.of("--release", release));
if (release.equals("9") || release.equals("10")) {
List<String> rootMods = List.of("java.se", "java.se.ee");
TraverseProc proc = new TraverseProc(rootMods);
JavaCompiler.CompilationTask task =
compiler.getTask(null, fm, this,
// options
List.of("--add-modules", String.join(",", rootMods)),
// classes
List.of("java.lang.Object"),
null);
task.setProcessors(List.of(proc));
if (!task.call()) {
return false;
}
Map<PackageElement, List<TypeElement>> types = proc.getPublicTypes();
options.add("--add-modules");
options.add(String.join(",", rootMods));
return doClassNames(
types.values().stream()
.flatMap(List::stream)
.map(TypeElement::toString)
.collect(toList()));
} else {
// TODO: kind of a hack...
// Create a throwaway compilation task with options "--release N"
// which has the side effect of setting the file manager's
// PLATFORM_CLASS_PATH to the right value.
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm =
compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8);
JavaCompiler.CompilationTask task =
compiler.getTask(null, fm, this, List.of("--release", release), null, null);
List<Path> paths = new ArrayList<>();
for (Path p : fm.getLocationAsPaths(StandardLocation.PLATFORM_CLASS_PATH)) {
try (Stream<Path> str = Files.walk(p)) {
str.forEachOrdered(paths::add);
}
}
options.add("-Xlint:-options");
return doClassNames(
paths.stream()
.filter(path -> path.toString().endsWith(".sig"))
.map(path -> path.subpath(1, path.getNameCount()))
.map(Path::toString)
.map(s -> s.replaceAll("\\.sig$", ""))
.map(s -> s.replace('/', '.'))
.collect(toList()));
}
}
/**
* An enum denoting the mode in which the tool is running.
* Different modes correspond to the different process* methods.
* The exception is UNKNOWN, which indicates that a mode wasn't
* specified on the command line, which is an error.
*/
static enum LoadMode {
CLASSES, DIR, JAR, OLD_JDK, JDK9, SELF, RELEASE, LOAD_CSV
}
static enum ScanMode {
ARGS, LIST, PRINT_CSV
}
/**
* A checked exception that's thrown if a command-line syntax error
* is detected.
*/
static class UsageException extends Exception {
private static final long serialVersionUID = 3611828659572908743L;
}
/**
* Convenience method to throw UsageException if a condition is false.
*
* @param cond the condition that's required to be true
* @throws UsageException
*/
void require(boolean cond) throws UsageException {
if (!cond) {
throw new UsageException();
}
}
/**
* Constructs an instance of the finder tool.
*
* @param out the stream to which the tool's output is sent
* @param err the stream to which error messages are sent
*/
Main(PrintStream out, PrintStream err) {
this.out = out;
this.err = err;
compiler = ToolProvider.getSystemJavaCompiler();
fm = compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8);
}
/**
* Prints the diagnostic to the err stream.
*
* Specified by the DiagnosticListener interface.
*
* @param diagnostic the tool diagnostic to print
*/
@Override
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
err.println(diagnostic);
}
/**
* Parses arguments and performs the requested processing.
*
* @param argArray command-line arguments
* @return true on success, false on error
*/
boolean run(String... argArray) {
Queue<String> args = new ArrayDeque<>(Arrays.asList(argArray));
LoadMode loadMode = LoadMode.RELEASE;
ScanMode scanMode = ScanMode.ARGS;
String dir = null;
String jar = null;
String jdkHome = null;
String release = "10";
List<String> loadClasses = new ArrayList<>();
String csvFile = null;
try {
while (!args.isEmpty()) {
String a = args.element();
if (a.startsWith("-")) {
args.remove();
switch (a) {
case "--class-path":
classPath.clear();
Arrays.stream(args.remove().split(File.pathSeparator))
.map(File::new)
.forEachOrdered(classPath::add);
break;
case "--for-removal":
forRemoval = true;
break;
case "--full-version":
out.println(System.getProperty("java.vm.version"));
return false;
case "--help":
case "-h":
out.println(Messages.get("main.usage"));
out.println();
out.println(Messages.get("main.help"));
return false;
case "-l":
case "--list":
require(scanMode == ScanMode.ARGS);
scanMode = ScanMode.LIST;
break;
case "--release":
loadMode = LoadMode.RELEASE;
release = args.remove();
if (!validReleases.contains(release)) {
throw new UsageException();
}
break;
case "-v":
case "--verbose":
verbose = true;
break;
case "--version":
out.println(System.getProperty("java.version"));
return false;
case "--Xcompiler-arg":
options.add(args.remove());
break;
case "--Xcsv-comment":
comments.add(args.remove());
break;
case "--Xhelp":
out.println(Messages.get("main.xhelp"));
return false;
case "--Xload-class":
loadMode = LoadMode.CLASSES;
loadClasses.add(args.remove());
break;
case "--Xload-csv":
loadMode = LoadMode.LOAD_CSV;
csvFile = args.remove();
break;
case "--Xload-dir":
loadMode = LoadMode.DIR;
dir = args.remove();
break;
case "--Xload-jar":
loadMode = LoadMode.JAR;
jar = args.remove();
break;
case "--Xload-jdk9":
loadMode = LoadMode.JDK9;
jdkHome = args.remove();
break;
case "--Xload-old-jdk":
loadMode = LoadMode.OLD_JDK;
jdkHome = args.remove();
break;
case "--Xload-self":
loadMode = LoadMode.SELF;
break;
case "--Xprint-csv":
require(scanMode == ScanMode.ARGS);
scanMode = ScanMode.PRINT_CSV;
break;
default:
throw new UsageException();
}
} else {
break;
}
}
if ((scanMode == ScanMode.ARGS) == args.isEmpty()) {
throw new UsageException();
}
if ( forRemoval && loadMode == LoadMode.RELEASE &&
releasesWithoutForRemoval.contains(release)) {
throw new UsageException();
}
boolean success = false;
switch (loadMode) {
case CLASSES:
success = doClassNames(loadClasses);
break;
case DIR:
success = processDirectory(dir, loadClasses);
break;
case JAR:
success = processJarFile(jar, loadClasses);
break;
case JDK9:
require(!args.isEmpty());
success = processJdk9(jdkHome, loadClasses);
break;
case LOAD_CSV:
deprList = DeprDB.loadFromFile(csvFile);
success = true;
break;
case OLD_JDK:
success = processOldJdk(jdkHome, loadClasses);
break;
case RELEASE:
success = processRelease(release, loadClasses);
break;
case SELF:
success = processSelf(loadClasses);
break;
default:
throw new UsageException();
}
if (!success) {
return false;
}
} catch (NoSuchElementException | UsageException ex) {
err.println(Messages.get("main.usage"));
return false;
} catch (IOException ioe) {
if (verbose) {
ioe.printStackTrace(err);
} else {
err.println(ioe);
}
return false;
}
// now the scanning phase
boolean scanStatus = true;
switch (scanMode) {
case LIST:
for (DeprData dd : deprList) {
if (!forRemoval || dd.isForRemoval()) {
out.println(Pretty.print(dd));
}
}
break;
case PRINT_CSV:
out.println("#jdepr1");
comments.forEach(s -> out.println("# " + s));
for (DeprData dd : deprList) {
CSV.write(out, dd.kind, dd.typeName, dd.nameSig, dd.since, dd.forRemoval);
}
break;
case ARGS:
DeprDB db = DeprDB.loadFromList(deprList);
List<String> cp = classPath.stream()
.map(File::toString)
.collect(toList());
Scan scan = new Scan(out, err, cp, db, verbose);
for (String a : args) {
boolean s;
if (a.endsWith(".jar")) {
s = scan.scanJar(a);
} else if (a.endsWith(".class")) {
s = scan.processClassFile(a);
} else if (Files.isDirectory(Paths.get(a))) {
s = scan.scanDir(a);
} else {
s = scan.processClassName(a.replace('.', '/'));
}
scanStatus = scanStatus && s;
}
break;
}
return scanStatus;
}
/**
* Programmatic main entry point: initializes the tool instance to
* use stdout and stderr; runs the tool, passing command-line args;
* returns an exit status.
*
* @return true on success, false otherwise
*/
public static boolean call(PrintStream out, PrintStream err, String... args) {
return new Main(out, err).run(args);
}
/**
* Calls the main entry point and exits the JVM with an exit
* status determined by the return status.
*/
public static void main(String[] args) {
System.exit(call(System.out, System.err, args) ? 0 : 1);
}
}