Path: blob/master/src/jdk.jdeps/share/classes/com/sun/tools/jdeprscan/Main.java
41161 views
/*1* Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package com.sun.tools.jdeprscan;2627import java.io.File;28import java.io.IOException;29import java.io.PrintStream;30import java.net.URI;31import java.nio.charset.StandardCharsets;32import java.nio.file.Files;33import java.nio.file.FileSystems;34import java.nio.file.Path;35import java.nio.file.Paths;36import java.util.ArrayDeque;37import java.util.ArrayList;38import java.util.Arrays;39import java.util.Collection;40import java.util.EnumSet;41import java.util.HashSet;42import java.util.List;43import java.util.Map;44import java.util.NoSuchElementException;45import java.util.Set;46import java.util.Queue;47import java.util.stream.Collectors;48import java.util.stream.IntStream;49import java.util.stream.Stream;50import java.util.stream.StreamSupport;51import java.util.jar.JarEntry;52import java.util.jar.JarFile;5354import javax.tools.Diagnostic;55import javax.tools.DiagnosticListener;56import javax.tools.JavaCompiler;57import javax.tools.JavaFileManager;58import javax.tools.JavaFileObject;59import javax.tools.JavaFileObject.Kind;60import javax.tools.StandardJavaFileManager;61import javax.tools.StandardLocation;62import javax.tools.ToolProvider;6364import com.sun.tools.javac.file.JavacFileManager;65import com.sun.tools.javac.platform.JDKPlatformProvider;6667import com.sun.tools.jdeprscan.scan.Scan;6869import static java.util.stream.Collectors.*;7071import javax.lang.model.element.PackageElement;72import javax.lang.model.element.TypeElement;7374/**75* Deprecation Scanner tool. Loads API deprecation information from the76* JDK image, or optionally, from a jar file or class hierarchy. Then scans77* a class library for usages of those APIs.78*79* TODO:80* - audit error handling throughout, but mainly in scan package81* - handling of covariant overrides82* - handling of override of method found in multiple superinterfaces83* - convert type/method/field output to Java source like syntax, e.g.84* instead of java/lang/Character.isJavaLetter(C)Z85* print void java.lang.Character.isJavaLetter(char)boolean86* - more example output in man page87* - more rigorous GNU style option parsing; use joptsimple?88*89* FUTURES:90* - add module support: --add-modules, --module-path, module arg91* - load deprecation declarations from a designated class library instead92* of the JDK93* - load deprecation declarations from a module94* - scan a module (but a modular jar can be treated just a like an ordinary jar)95* - multi-version jar96*/97public class Main implements DiagnosticListener<JavaFileObject> {98final PrintStream out;99final PrintStream err;100final List<File> bootClassPath = new ArrayList<>();101final List<File> classPath = new ArrayList<>();102final List<File> systemModules = new ArrayList<>();103final List<String> options = new ArrayList<>();104final List<String> comments = new ArrayList<>();105106// Valid releases need to match what the compiler supports.107// Keep these updated manually until there's a compiler API108// that allows querying of supported releases.109final Set<String> releasesWithoutForRemoval = Set.of("6", "7", "8");110final Set<String> releasesWithForRemoval = // "9", "10", "11", ...111IntStream.rangeClosed(9, Runtime.version().feature())112.mapToObj(Integer::toString)113.collect(Collectors.toUnmodifiableSet());114115final Set<String> validReleases;116{117Set<String> temp = new HashSet<>(releasesWithoutForRemoval);118temp.addAll(releasesWithForRemoval);119validReleases = Set.of(temp.toArray(new String[0]));120}121122boolean verbose = false;123boolean forRemoval = false;124125final JavaCompiler compiler;126final StandardJavaFileManager fm;127128List<DeprData> deprList; // non-null after successful load phase129130/**131* Processes a collection of class names. Names should fully qualified132* names in the form "pkg.pkg.pkg.classname".133*134* @param classNames collection of fully qualified classnames to process135* @return true for success, false for failure136* @throws IOException if an I/O error occurs137*/138boolean doClassNames(Collection<String> classNames) throws IOException {139if (verbose) {140out.println("List of classes to process:");141classNames.forEach(out::println);142out.println("End of class list.");143}144145// TODO: not sure this is necessary...146if (fm instanceof JavacFileManager) {147((JavacFileManager)fm).setSymbolFileEnabled(false);148}149150fm.setLocation(StandardLocation.CLASS_PATH, classPath);151if (!bootClassPath.isEmpty()) {152fm.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath);153}154155if (!systemModules.isEmpty()) {156fm.setLocation(StandardLocation.SYSTEM_MODULES, systemModules);157}158159LoadProc proc = new LoadProc();160JavaCompiler.CompilationTask task =161compiler.getTask(null, fm, this, options, classNames, null);162task.setProcessors(List.of(proc));163boolean r = task.call();164if (r) {165if (forRemoval) {166deprList = proc.getDeprecations().stream()167.filter(DeprData::isForRemoval)168.toList();169} else {170deprList = proc.getDeprecations();171}172}173return r;174}175176/**177* Processes a stream of filenames (strings). The strings are in the178* form pkg/pkg/pkg/classname.class relative to the root of a package179* hierarchy.180*181* @param filenames a Stream of filenames to process182* @return true for success, false for failure183* @throws IOException if an I/O error occurs184*/185boolean doFileNames(Stream<String> filenames) throws IOException {186return doClassNames(187filenames.filter(name -> name.endsWith(".class"))188.filter(name -> !name.endsWith("package-info.class"))189.filter(name -> !name.endsWith("module-info.class"))190.map(s -> s.replaceAll("\\.class$", ""))191.map(s -> s.replace(File.separatorChar, '.'))192.toList());193}194195/**196* Replaces all but the first occurrence of '/' with '.'. Assumes197* that the name is in the format module/pkg/pkg/classname.class.198* That is, the name should contain at least one '/' character199* separating the module name from the package-class name.200*201* @param filename the input filename202* @return the modular classname203*/204String convertModularFileName(String filename) {205int slash = filename.indexOf('/');206return filename.substring(0, slash)207+ "/"208+ filename.substring(slash+1).replace('/', '.');209}210211/**212* Processes a stream of filenames (strings) including a module prefix.213* The strings are in the form module/pkg/pkg/pkg/classname.class relative214* to the root of a directory containing modules. The strings are processed215* into module-qualified class names of the form216* "module/pkg.pkg.pkg.classname".217*218* @param filenames a Stream of filenames to process219* @return true for success, false for failure220* @throws IOException if an I/O error occurs221*/222boolean doModularFileNames(Stream<String> filenames) throws IOException {223return doClassNames(224filenames.filter(name -> name.endsWith(".class"))225.filter(name -> !name.endsWith("package-info.class"))226.filter(name -> !name.endsWith("module-info.class"))227.map(s -> s.replaceAll("\\.class$", ""))228.map(this::convertModularFileName)229.toList());230}231232/**233* Processes named class files in the given directory. The directory234* should be the root of a package hierarchy. If classNames is235* empty, walks the directory hierarchy to find all classes.236*237* @param dirname the name of the directory to process238* @param classNames the names of classes to process239* @return true for success, false for failure240* @throws IOException if an I/O error occurs241*/242boolean processDirectory(String dirname, Collection<String> classNames) throws IOException {243if (!Files.isDirectory(Paths.get(dirname))) {244err.printf("%s: not a directory%n", dirname);245return false;246}247248classPath.add(0, new File(dirname));249250if (classNames.isEmpty()) {251Path base = Paths.get(dirname);252int baseCount = base.getNameCount();253try (Stream<Path> paths = Files.walk(base)) {254Stream<String> files =255paths.filter(p -> p.getNameCount() > baseCount)256.map(p -> p.subpath(baseCount, p.getNameCount()))257.map(Path::toString);258return doFileNames(files);259}260} else {261return doClassNames(classNames);262}263}264265/**266* Processes all class files in the given jar file.267*268* @param jarname the name of the jar file to process269* @return true for success, false for failure270* @throws IOException if an I/O error occurs271*/272boolean doJarFile(String jarname) throws IOException {273try (JarFile jf = new JarFile(jarname)) {274Stream<String> files =275jf.stream()276.map(JarEntry::getName);277return doFileNames(files);278}279}280281/**282* Processes named class files from the given jar file,283* or all classes if classNames is empty.284*285* @param jarname the name of the jar file to process286* @param classNames the names of classes to process287* @return true for success, false for failure288* @throws IOException if an I/O error occurs289*/290boolean processJarFile(String jarname, Collection<String> classNames) throws IOException {291classPath.add(0, new File(jarname));292293if (classNames.isEmpty()) {294return doJarFile(jarname);295} else {296return doClassNames(classNames);297}298}299300/**301* Processes named class files from rt.jar of a JDK version 7 or 8.302* If classNames is empty, processes all classes.303*304* @param jdkHome the path to the "home" of the JDK to process305* @param classNames the names of classes to process306* @return true for success, false for failure307* @throws IOException if an I/O error occurs308*/309boolean processOldJdk(String jdkHome, Collection<String> classNames) throws IOException {310String RTJAR = jdkHome + "/jre/lib/rt.jar";311String CSJAR = jdkHome + "/jre/lib/charsets.jar";312313bootClassPath.add(0, new File(RTJAR));314bootClassPath.add(1, new File(CSJAR));315options.add("-source");316options.add("8");317318if (classNames.isEmpty()) {319return doJarFile(RTJAR);320} else {321return doClassNames(classNames);322}323}324325/**326* Processes listed classes given a JDK 9 home.327*/328boolean processJdk9(String jdkHome, Collection<String> classes) throws IOException {329systemModules.add(new File(jdkHome));330return doClassNames(classes);331}332333/**334* Processes the class files from the currently running JDK,335* using the jrt: filesystem.336*337* @return true for success, false for failure338* @throws IOException if an I/O error occurs339*/340boolean processSelf(Collection<String> classes) throws IOException {341options.add("--add-modules");342options.add("java.se");343344if (classes.isEmpty()) {345Path modules = FileSystems.getFileSystem(URI.create("jrt:/"))346.getPath("/modules");347348// names are /modules/<modulename>/pkg/.../Classname.class349try (Stream<Path> paths = Files.walk(modules)) {350Stream<String> files =351paths.filter(p -> p.getNameCount() > 2)352.map(p -> p.subpath(1, p.getNameCount()))353.map(Path::toString);354return doModularFileNames(files);355}356} else {357return doClassNames(classes);358}359}360361/**362* Process classes from a particular JDK release, using only information363* in this JDK.364*365* @param release a supported release version, like "8" or "10".366* @param classes collection of classes to process, may be empty367* @return success value368*/369boolean processRelease(String release, Collection<String> classes) throws IOException {370boolean hasModules;371boolean hasJavaSE_EE;372373try {374int releaseNum = Integer.parseInt(release);375376hasModules = releaseNum >= 9;377hasJavaSE_EE = hasModules && releaseNum <= 10;378} catch (NumberFormatException ex) {379hasModules = true;380hasJavaSE_EE = false;381}382383options.addAll(List.of("--release", release));384385if (hasModules) {386List<String> rootMods = hasJavaSE_EE ? List.of("java.se", "java.se.ee")387: List.of("java.se");388TraverseProc proc = new TraverseProc(rootMods);389JavaCompiler.CompilationTask task =390compiler.getTask(null, fm, this,391// options392List.of("--add-modules", String.join(",", rootMods),393"--release", release),394// classes395List.of("java.lang.Object"),396null);397task.setProcessors(List.of(proc));398if (!task.call()) {399return false;400}401Map<PackageElement, List<TypeElement>> types = proc.getPublicTypes();402options.add("--add-modules");403options.add(String.join(",", rootMods));404return doClassNames(405types.values().stream()406.flatMap(List::stream)407.map(TypeElement::toString)408.toList());409} else {410JDKPlatformProvider pp = new JDKPlatformProvider();411if (StreamSupport.stream(pp.getSupportedPlatformNames().spliterator(),412false)413.noneMatch(n -> n.equals(release))) {414return false;415}416JavaFileManager fm = pp.getPlatform(release, "").getFileManager();417List<String> classNames = new ArrayList<>();418for (JavaFileObject fo : fm.list(StandardLocation.PLATFORM_CLASS_PATH,419"",420EnumSet.of(Kind.CLASS),421true)) {422classNames.add(fm.inferBinaryName(StandardLocation.PLATFORM_CLASS_PATH, fo));423}424425options.add("-Xlint:-options");426427return doClassNames(classNames);428}429}430431/**432* An enum denoting the mode in which the tool is running.433* Different modes correspond to the different process* methods.434* The exception is UNKNOWN, which indicates that a mode wasn't435* specified on the command line, which is an error.436*/437static enum LoadMode {438CLASSES, DIR, JAR, OLD_JDK, JDK9, SELF, RELEASE, LOAD_CSV439}440441static enum ScanMode {442ARGS, LIST, PRINT_CSV443}444445/**446* A checked exception that's thrown if a command-line syntax error447* is detected.448*/449static class UsageException extends Exception {450private static final long serialVersionUID = 3611828659572908743L;451}452453/**454* Convenience method to throw UsageException if a condition is false.455*456* @param cond the condition that's required to be true457* @throws UsageException458*/459void require(boolean cond) throws UsageException {460if (!cond) {461throw new UsageException();462}463}464465/**466* Constructs an instance of the finder tool.467*468* @param out the stream to which the tool's output is sent469* @param err the stream to which error messages are sent470*/471Main(PrintStream out, PrintStream err) {472this.out = out;473this.err = err;474compiler = ToolProvider.getSystemJavaCompiler();475fm = compiler.getStandardFileManager(this, null, StandardCharsets.UTF_8);476}477478/**479* Prints the diagnostic to the err stream.480*481* Specified by the DiagnosticListener interface.482*483* @param diagnostic the tool diagnostic to print484*/485@Override486public void report(Diagnostic<? extends JavaFileObject> diagnostic) {487err.println(diagnostic);488}489490/**491* Parses arguments and performs the requested processing.492*493* @param argArray command-line arguments494* @return true on success, false on error495*/496boolean run(String... argArray) {497Queue<String> args = new ArrayDeque<>(Arrays.asList(argArray));498LoadMode loadMode = LoadMode.RELEASE;499ScanMode scanMode = ScanMode.ARGS;500String dir = null;501String jar = null;502String jdkHome = null;503String release = Integer.toString(Runtime.version().feature());504List<String> loadClasses = new ArrayList<>();505String csvFile = null;506507try {508while (!args.isEmpty()) {509String a = args.element();510if (a.startsWith("-")) {511args.remove();512switch (a) {513case "--class-path":514classPath.clear();515Arrays.stream(args.remove().split(File.pathSeparator))516.map(File::new)517.forEachOrdered(classPath::add);518break;519case "--for-removal":520forRemoval = true;521break;522case "--full-version":523out.println(System.getProperty("java.vm.version"));524return false;525case "--help":526case "-h":527case "-?":528printHelp(out);529out.println();530out.println(Messages.get("main.help"));531return true;532case "-l":533case "--list":534require(scanMode == ScanMode.ARGS);535scanMode = ScanMode.LIST;536break;537case "--release":538loadMode = LoadMode.RELEASE;539release = args.remove();540if (!validReleases.contains(release)) {541throw new UsageException();542}543break;544case "-v":545case "--verbose":546verbose = true;547break;548case "--version":549out.println(System.getProperty("java.version"));550return false;551case "--Xcompiler-arg":552options.add(args.remove());553break;554case "--Xcsv-comment":555comments.add(args.remove());556break;557case "--Xhelp":558out.println(Messages.get("main.xhelp"));559return false;560case "--Xload-class":561loadMode = LoadMode.CLASSES;562loadClasses.add(args.remove());563break;564case "--Xload-csv":565loadMode = LoadMode.LOAD_CSV;566csvFile = args.remove();567break;568case "--Xload-dir":569loadMode = LoadMode.DIR;570dir = args.remove();571break;572case "--Xload-jar":573loadMode = LoadMode.JAR;574jar = args.remove();575break;576case "--Xload-jdk9":577loadMode = LoadMode.JDK9;578jdkHome = args.remove();579break;580case "--Xload-old-jdk":581loadMode = LoadMode.OLD_JDK;582jdkHome = args.remove();583break;584case "--Xload-self":585loadMode = LoadMode.SELF;586break;587case "--Xprint-csv":588require(scanMode == ScanMode.ARGS);589scanMode = ScanMode.PRINT_CSV;590break;591default:592throw new UsageException();593}594} else {595break;596}597}598599if ((scanMode == ScanMode.ARGS) == args.isEmpty()) {600throw new UsageException();601}602603if ( forRemoval && loadMode == LoadMode.RELEASE &&604releasesWithoutForRemoval.contains(release)) {605throw new UsageException();606}607608boolean success = false;609610switch (loadMode) {611case CLASSES:612success = doClassNames(loadClasses);613break;614case DIR:615success = processDirectory(dir, loadClasses);616break;617case JAR:618success = processJarFile(jar, loadClasses);619break;620case JDK9:621require(!args.isEmpty());622success = processJdk9(jdkHome, loadClasses);623break;624case LOAD_CSV:625deprList = DeprDB.loadFromFile(csvFile);626success = true;627break;628case OLD_JDK:629success = processOldJdk(jdkHome, loadClasses);630break;631case RELEASE:632success = processRelease(release, loadClasses);633break;634case SELF:635success = processSelf(loadClasses);636break;637default:638throw new UsageException();639}640641if (!success) {642return false;643}644} catch (NoSuchElementException | UsageException ex) {645printHelp(err);646return false;647} catch (IOException ioe) {648if (verbose) {649ioe.printStackTrace(err);650} else {651err.println(ioe);652}653return false;654}655656// now the scanning phase657658boolean scanStatus = true;659660switch (scanMode) {661case LIST:662for (DeprData dd : deprList) {663if (!forRemoval || dd.isForRemoval()) {664out.println(Pretty.print(dd));665}666}667break;668case PRINT_CSV:669out.println("#jdepr1");670comments.forEach(s -> out.println("# " + s));671for (DeprData dd : deprList) {672CSV.write(out, dd.kind, dd.typeName, dd.nameSig, dd.since, dd.forRemoval);673}674break;675case ARGS:676DeprDB db = DeprDB.loadFromList(deprList);677List<String> cp = classPath.stream()678.map(File::toString)679.toList();680Scan scan = new Scan(out, err, cp, db, verbose);681682for (String a : args) {683boolean s;684if (a.endsWith(".jar")) {685s = scan.scanJar(a);686} else if (a.endsWith(".class")) {687s = scan.processClassFile(a);688} else if (Files.isDirectory(Paths.get(a))) {689s = scan.scanDir(a);690} else {691s = scan.processClassName(a.replace('.', '/'));692}693scanStatus = scanStatus && s;694}695break;696}697698return scanStatus;699}700701private void printHelp(PrintStream out) {702JDKPlatformProvider pp = new JDKPlatformProvider();703String supportedReleases =704String.join("|", pp.getSupportedPlatformNames());705out.println(Messages.get("main.usage", supportedReleases));706}707708/**709* Programmatic main entry point: initializes the tool instance to710* use stdout and stderr; runs the tool, passing command-line args;711* returns an exit status.712*713* @return true on success, false otherwise714*/715public static boolean call(PrintStream out, PrintStream err, String... args) {716return new Main(out, err).run(args);717}718719/**720* Calls the main entry point and exits the JVM with an exit721* status determined by the return status.722*/723public static void main(String[] args) {724System.exit(call(System.out, System.err, args) ? 0 : 1);725}726}727728729