| /* |
| * 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.scan; |
| |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.nio.file.Files; |
| import java.nio.file.NoSuchFileException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayDeque; |
| import java.util.Deque; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| import com.sun.tools.classfile.*; |
| import com.sun.tools.jdeprscan.DeprData; |
| import com.sun.tools.jdeprscan.DeprDB; |
| import com.sun.tools.jdeprscan.Messages; |
| |
| import static com.sun.tools.classfile.AccessFlags.*; |
| import static com.sun.tools.classfile.ConstantPool.*; |
| |
| /** |
| * An object that represents the scanning phase of deprecation usage checking. |
| * Given a deprecation database, scans the targeted directory hierarchy, jar |
| * file, or individual class for uses of deprecated APIs. |
| */ |
| public class Scan { |
| final PrintStream out; |
| final PrintStream err; |
| final List<String> classPath; |
| final DeprDB db; |
| final boolean verbose; |
| |
| final ClassFinder finder; |
| final Set<String> classesNotFound = new HashSet<>(); |
| boolean errorOccurred = false; |
| |
| public Scan(PrintStream out, |
| PrintStream err, |
| List<String> classPath, |
| DeprDB db, |
| boolean verbose) { |
| this.out = out; |
| this.err = err; |
| this.classPath = classPath; |
| this.db = db; |
| this.verbose = verbose; |
| |
| ClassFinder f = new ClassFinder(verbose); |
| |
| // TODO: this isn't quite right. If we've specified a release other than the current |
| // one, we should instead add a reference to the symbol file for that release instead |
| // of the current image. The problems are a) it's unclear how to get from a release |
| // to paths that reference the symbol files, as this might be internal to the file |
| // manager; and b) the symbol file includes .sig files, not class files, which ClassFile |
| // might not be able to handle. |
| f.addJrt(); |
| |
| for (String name : classPath) { |
| if (name.endsWith(".jar")) { |
| f.addJar(name); |
| } else { |
| f.addDir(name); |
| } |
| } |
| |
| finder = f; |
| } |
| |
| /** |
| * Given a descriptor type, extracts and returns the class name from it, if any. |
| * These types are obtained from field descriptors (JVMS 4.3.2) and method |
| * descriptors (JVMS 4.3.3). They have one of the following forms: |
| * |
| * I // or any other primitive, or V for void |
| * [I // array of primitives, including multi-dimensional |
| * Lname; // the named class |
| * [Lname; // array whose component is the named class (also multi-d) |
| * |
| * This method extracts and returns the class name, or returns empty for primitives, void, |
| * or array of primitives. |
| * |
| * Returns nullable reference instead of Optional because downstream |
| * processing can throw checked exceptions. |
| * |
| * @param descType the type from a descriptor |
| * @return the extracted class name, or null |
| */ |
| String nameFromDescType(String descType) { |
| Matcher matcher = descTypePattern.matcher(descType); |
| if (matcher.matches()) { |
| return matcher.group(1); |
| } else { |
| return null; |
| } |
| } |
| |
| Pattern descTypePattern = Pattern.compile("\\[*L(.*);"); |
| |
| /** |
| * Given a ref type name, extracts and returns the class name from it, if any. |
| * Ref type names are obtained from a Class_info structure (JVMS 4.4.1) and from |
| * Fieldref_info, Methodref_info, and InterfaceMethodref_info structures (JVMS 4.4.2). |
| * They represent named classes or array classes mentioned by name, and they |
| * represent class or interface types that have the referenced field or method |
| * as a member. They have one of the following forms: |
| * |
| * [I // array of primitives, including multi-dimensional |
| * name // the named class |
| * [Lname; // array whose component is the named class (also multi-d) |
| * |
| * Notably, a plain class name doesn't have the L prefix and ; suffix, and |
| * primitives and void do not occur. |
| * |
| * Returns nullable reference instead of Optional because downstream |
| * processing can throw checked exceptions. |
| * |
| * @param refType a reference type name |
| * @return the extracted class name, or null |
| */ |
| String nameFromRefType(String refType) { |
| Matcher matcher = refTypePattern.matcher(refType); |
| if (matcher.matches()) { |
| return matcher.group(1); |
| } else if (refType.startsWith("[")) { |
| return null; |
| } else { |
| return refType; |
| } |
| } |
| |
| Pattern refTypePattern = Pattern.compile("\\[+L(.*);"); |
| |
| String typeKind(ClassFile cf) { |
| AccessFlags flags = cf.access_flags; |
| if (flags.is(ACC_ENUM)) { |
| return "enum"; |
| } else if (flags.is(ACC_ANNOTATION)) { |
| return "@interface"; |
| } else if (flags.is(ACC_INTERFACE)) { |
| return "interface"; |
| } else { |
| return "class"; |
| } |
| } |
| |
| String dep(boolean forRemoval) { |
| return Messages.get(forRemoval ? "scan.dep.removal" : "scan.dep.normal"); |
| } |
| |
| void printType(String key, ClassFile cf, String cname, boolean r) |
| throws ConstantPoolException { |
| out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, dep(r))); |
| } |
| |
| void printMethod(String key, ClassFile cf, String cname, String mname, String rtype, |
| boolean r) throws ConstantPoolException { |
| out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, mname, rtype, dep(r))); |
| } |
| |
| void printField(String key, ClassFile cf, String cname, String fname, |
| boolean r) throws ConstantPoolException { |
| out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, fname, dep(r))); |
| } |
| |
| void printFieldType(String key, ClassFile cf, String cname, String fname, String type, |
| boolean r) throws ConstantPoolException { |
| out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, fname, type, dep(r))); |
| } |
| |
| void printHasField(ClassFile cf, String fname, String type, boolean r) |
| throws ConstantPoolException { |
| out.println(Messages.get("scan.out.hasfield", typeKind(cf), cf.getName(), fname, type, dep(r))); |
| } |
| |
| void printHasMethodParmType(ClassFile cf, String mname, String parmType, boolean r) |
| throws ConstantPoolException { |
| out.println(Messages.get("scan.out.methodparmtype", typeKind(cf), cf.getName(), mname, parmType, dep(r))); |
| } |
| |
| void printHasMethodRetType(ClassFile cf, String mname, String retType, boolean r) |
| throws ConstantPoolException { |
| out.println(Messages.get("scan.out.methodrettype", typeKind(cf), cf.getName(), mname, retType, dep(r))); |
| } |
| |
| void printHasOverriddenMethod(ClassFile cf, String overridden, String mname, String desc, boolean r) |
| throws ConstantPoolException { |
| out.println(Messages.get("scan.out.methodoverride", typeKind(cf), cf.getName(), overridden, |
| mname, desc, dep(r))); |
| } |
| |
| void errorException(Exception ex) { |
| errorOccurred = true; |
| err.println(Messages.get("scan.err.exception", ex.toString())); |
| if (verbose) { |
| ex.printStackTrace(err); |
| } |
| } |
| |
| void errorNoClass(String className) { |
| errorOccurred = true; |
| if (classesNotFound.add(className)) { |
| // print message only first time the class can't be found |
| err.println(Messages.get("scan.err.noclass", className)); |
| } |
| } |
| |
| void errorNoFile(String fileName) { |
| errorOccurred = true; |
| err.println(Messages.get("scan.err.nofile", fileName)); |
| } |
| |
| void errorNoMethod(String className, String methodName, String desc) { |
| errorOccurred = true; |
| err.println(Messages.get("scan.err.nomethod", className, methodName, desc)); |
| } |
| |
| /** |
| * Checks whether a member (method or field) is present in a class. |
| * The checkMethod parameter determines whether this checks for a method |
| * or for a field. |
| * |
| * @param targetClass the ClassFile of the class to search |
| * @param targetName the method or field's name |
| * @param targetDesc the methods descriptor (ignored if checkMethod is false) |
| * @param checkMethod true if checking for method, false if checking for field |
| * @return boolean indicating whether the member is present |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| boolean isMemberPresent(ClassFile targetClass, |
| String targetName, |
| String targetDesc, |
| boolean checkMethod) |
| throws ConstantPoolException { |
| if (checkMethod) { |
| for (Method m : targetClass.methods) { |
| String mname = m.getName(targetClass.constant_pool); |
| String mdesc = targetClass.constant_pool.getUTF8Value(m.descriptor.index); |
| if (targetName.equals(mname) && targetDesc.equals(mdesc)) { |
| return true; |
| } |
| } |
| } else { |
| for (Field f : targetClass.fields) { |
| String fname = f.getName(targetClass.constant_pool); |
| if (targetName.equals(fname)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Adds all interfaces from this class to the deque of interfaces. |
| * |
| * @param intfs the deque of interfaces |
| * @param cf the ClassFile of this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void addInterfaces(Deque<String> intfs, ClassFile cf) |
| throws ConstantPoolException { |
| int count = cf.interfaces.length; |
| for (int i = 0; i < count; i++) { |
| intfs.addLast(cf.getInterfaceName(i)); |
| } |
| } |
| |
| /** |
| * Resolves a member by searching this class and all its superclasses and |
| * implemented interfaces. |
| * |
| * TODO: handles a few too many cases; needs cleanup. |
| * |
| * TODO: refine error handling |
| * |
| * @param cf the ClassFile of this class |
| * @param startClassName the name of the class at which to start searching |
| * @param findName the member name to search for |
| * @param findDesc the method descriptor to search for (ignored for fields) |
| * @param resolveMethod true if resolving a method, false if resolving a field |
| * @param checkStartClass true if the start class should be searched, false if |
| * it should be skipped |
| * @return the name of the class where the member resolved, or null |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| String resolveMember( |
| ClassFile cf, String startClassName, String findName, String findDesc, |
| boolean resolveMethod, boolean checkStartClass) |
| throws ConstantPoolException { |
| ClassFile startClass; |
| |
| if (cf.getName().equals(startClassName)) { |
| startClass = cf; |
| } else { |
| startClass = finder.find(startClassName); |
| if (startClass == null) { |
| errorNoClass(startClassName); |
| return startClassName; |
| } |
| } |
| |
| // follow super_class until it's 0, meaning we've reached Object |
| // accumulate interfaces of superclasses as we go along |
| |
| ClassFile curClass = startClass; |
| Deque<String> intfs = new ArrayDeque<>(); |
| while (true) { |
| if ((checkStartClass || curClass != startClass) && |
| isMemberPresent(curClass, findName, findDesc, resolveMethod)) { |
| break; |
| } |
| |
| if (curClass.super_class == 0) { // reached Object |
| curClass = null; |
| break; |
| } |
| |
| String superName = curClass.getSuperclassName(); |
| curClass = finder.find(superName); |
| if (curClass == null) { |
| errorNoClass(superName); |
| break; |
| } |
| addInterfaces(intfs, curClass); |
| } |
| |
| // search interfaces: add all interfaces and superinterfaces to queue |
| // search until it's empty |
| |
| if (curClass == null) { |
| addInterfaces(intfs, startClass); |
| while (intfs.size() > 0) { |
| String intf = intfs.removeFirst(); |
| curClass = finder.find(intf); |
| if (curClass == null) { |
| errorNoClass(intf); |
| break; |
| } |
| |
| if (isMemberPresent(curClass, findName, findDesc, resolveMethod)) { |
| break; |
| } |
| |
| addInterfaces(intfs, curClass); |
| } |
| } |
| |
| if (curClass == null) { |
| if (checkStartClass) { |
| errorNoMethod(startClassName, findName, findDesc); |
| return startClassName; |
| } else { |
| // TODO: refactor this |
| // checkStartClass == false means we're checking for overrides |
| // so not being able to resolve a method simply means there's |
| // no overriding, which isn't an error |
| return null; |
| } |
| } else { |
| String foundClassName = curClass.getName(); |
| return foundClassName; |
| } |
| } |
| |
| /** |
| * Checks the superclass of this class. |
| * |
| * @param cf the ClassFile of this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkSuper(ClassFile cf) throws ConstantPoolException { |
| String sname = cf.getSuperclassName(); |
| DeprData dd = db.getTypeDeprecated(sname); |
| if (dd != null) { |
| printType("scan.out.extends", cf, sname, dd.isForRemoval()); |
| } |
| } |
| |
| /** |
| * Checks the interfaces of this class. |
| * |
| * @param cf the ClassFile of this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkInterfaces(ClassFile cf) throws ConstantPoolException { |
| int ni = cf.interfaces.length; |
| for (int i = 0; i < ni; i++) { |
| String iname = cf.getInterfaceName(i); |
| DeprData dd = db.getTypeDeprecated(iname); |
| if (dd != null) { |
| printType("scan.out.implements", cf, iname, dd.isForRemoval()); |
| } |
| } |
| } |
| |
| /** |
| * Checks Class_info entries in the constant pool. |
| * |
| * @param cf the ClassFile of this class |
| * @param entries constant pool entries collected from this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkClasses(ClassFile cf, CPEntries entries) throws ConstantPoolException { |
| for (ConstantPool.CONSTANT_Class_info ci : entries.classes) { |
| String name = nameFromRefType(ci.getName()); |
| if (name != null) { |
| DeprData dd = db.getTypeDeprecated(name); |
| if (dd != null) { |
| printType("scan.out.usesclass", cf, name, dd.isForRemoval()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks methods referred to from the constant pool. |
| * |
| * @param cf the ClassFile of this class |
| * @param clname the class name |
| * @param nti the NameAndType_info from a MethodRef or InterfaceMethodRef entry |
| * @param msgKey message key for localization |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkMethodRef(ClassFile cf, |
| String clname, |
| CONSTANT_NameAndType_info nti, |
| String msgKey) throws ConstantPoolException { |
| String name = nti.getName(); |
| String type = nti.getType(); |
| clname = nameFromRefType(clname); |
| if (clname != null) { |
| clname = resolveMember(cf, clname, name, type, true, true); |
| DeprData dd = db.getMethodDeprecated(clname, name, type); |
| if (dd != null) { |
| printMethod(msgKey, cf, clname, name, type, dd.isForRemoval()); |
| } |
| } |
| } |
| |
| /** |
| * Checks fields referred to from the constant pool. |
| * |
| * @param cf the ClassFile of this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkFieldRef(ClassFile cf, |
| ConstantPool.CONSTANT_Fieldref_info fri) throws ConstantPoolException { |
| String clname = nameFromRefType(fri.getClassName()); |
| CONSTANT_NameAndType_info nti = fri.getNameAndTypeInfo(); |
| String name = nti.getName(); |
| String type = nti.getType(); |
| |
| if (clname != null) { |
| clname = resolveMember(cf, clname, name, type, false, true); |
| DeprData dd = db.getFieldDeprecated(clname, name); |
| if (dd != null) { |
| printField("scan.out.usesfield", cf, clname, name, dd.isForRemoval()); |
| } |
| } |
| } |
| |
| /** |
| * Checks the fields declared in this class. |
| * |
| * @param cf the ClassFile of this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkFields(ClassFile cf) throws ConstantPoolException { |
| for (Field f : cf.fields) { |
| String type = nameFromDescType(cf.constant_pool.getUTF8Value(f.descriptor.index)); |
| if (type != null) { |
| DeprData dd = db.getTypeDeprecated(type); |
| if (dd != null) { |
| printHasField(cf, f.getName(cf.constant_pool), type, dd.isForRemoval()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks the methods declared in this class. |
| * |
| * @param cf the ClassFile object of this class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void checkMethods(ClassFile cf) throws ConstantPoolException { |
| for (Method m : cf.methods) { |
| String mname = m.getName(cf.constant_pool); |
| String desc = cf.constant_pool.getUTF8Value(m.descriptor.index); |
| MethodSig sig = MethodSig.fromDesc(desc); |
| DeprData dd; |
| |
| for (String parm : sig.getParameters()) { |
| parm = nameFromDescType(parm); |
| if (parm != null) { |
| dd = db.getTypeDeprecated(parm); |
| if (dd != null) { |
| printHasMethodParmType(cf, mname, parm, dd.isForRemoval()); |
| } |
| } |
| } |
| |
| String ret = nameFromDescType(sig.getReturnType()); |
| if (ret != null) { |
| dd = db.getTypeDeprecated(ret); |
| if (dd != null) { |
| printHasMethodRetType(cf, mname, ret, dd.isForRemoval()); |
| } |
| } |
| |
| // check overrides |
| String overridden = resolveMember(cf, cf.getName(), mname, desc, true, false); |
| if (overridden != null) { |
| dd = db.getMethodDeprecated(overridden, mname, desc); |
| if (dd != null) { |
| printHasOverriddenMethod(cf, overridden, mname, desc, dd.isForRemoval()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Processes a single class file. |
| * |
| * @param cf the ClassFile of the class |
| * @throws ConstantPoolException if a constant pool entry cannot be found |
| */ |
| void processClass(ClassFile cf) throws ConstantPoolException { |
| if (verbose) { |
| out.println(Messages.get("scan.process.class", cf.getName())); |
| } |
| |
| CPEntries entries = CPEntries.loadFrom(cf); |
| |
| checkSuper(cf); |
| checkInterfaces(cf); |
| checkClasses(cf, entries); |
| |
| for (ConstantPool.CONSTANT_Methodref_info mri : entries.methodRefs) { |
| String clname = mri.getClassName(); |
| CONSTANT_NameAndType_info nti = mri.getNameAndTypeInfo(); |
| checkMethodRef(cf, clname, nti, "scan.out.usesmethod"); |
| } |
| |
| for (ConstantPool.CONSTANT_InterfaceMethodref_info imri : entries.intfMethodRefs) { |
| String clname = imri.getClassName(); |
| CONSTANT_NameAndType_info nti = imri.getNameAndTypeInfo(); |
| checkMethodRef(cf, clname, nti, "scan.out.usesintfmethod"); |
| } |
| |
| for (ConstantPool.CONSTANT_Fieldref_info fri : entries.fieldRefs) { |
| checkFieldRef(cf, fri); |
| } |
| |
| checkFields(cf); |
| checkMethods(cf); |
| } |
| |
| /** |
| * Scans a jar file for uses of deprecated APIs. |
| * |
| * @param jarname the jar file to process |
| * @return true on success, false on failure |
| */ |
| public boolean scanJar(String jarname) { |
| try (JarFile jf = new JarFile(jarname)) { |
| out.println(Messages.get("scan.head.jar", jarname)); |
| finder.addJar(jarname); |
| Enumeration<JarEntry> entries = jf.entries(); |
| while (entries.hasMoreElements()) { |
| JarEntry entry = entries.nextElement(); |
| String name = entry.getName(); |
| if (name.endsWith(".class") |
| && !name.endsWith("package-info.class") |
| && !name.endsWith("module-info.class")) { |
| processClass(ClassFile.read(jf.getInputStream(entry))); |
| } |
| } |
| return true; |
| } catch (NoSuchFileException nsfe) { |
| errorNoFile(jarname); |
| } catch (IOException | ConstantPoolException ex) { |
| errorException(ex); |
| } |
| return false; |
| } |
| |
| /** |
| * Scans class files in the named directory hierarchy for uses of deprecated APIs. |
| * |
| * @param dirname the directory hierarchy to process |
| * @return true on success, false on failure |
| */ |
| public boolean scanDir(String dirname) { |
| Path base = Paths.get(dirname); |
| int baseCount = base.getNameCount(); |
| finder.addDir(dirname); |
| try (Stream<Path> paths = Files.walk(Paths.get(dirname))) { |
| List<Path> classes = |
| paths.filter(p -> p.getNameCount() > baseCount) |
| .filter(path -> path.toString().endsWith(".class")) |
| .filter(path -> !path.toString().endsWith("package-info.class")) |
| .filter(path -> !path.toString().endsWith("module-info.class")) |
| .collect(Collectors.toList()); |
| |
| out.println(Messages.get("scan.head.dir", dirname)); |
| |
| for (Path p : classes) { |
| processClass(ClassFile.read(p)); |
| } |
| return true; |
| } catch (IOException | ConstantPoolException ex) { |
| errorException(ex); |
| return false; |
| } |
| } |
| |
| /** |
| * Scans the named class for uses of deprecated APIs. |
| * |
| * @param className the class to scan |
| * @return true on success, false on failure |
| */ |
| public boolean processClassName(String className) { |
| try { |
| ClassFile cf = finder.find(className); |
| if (cf == null) { |
| errorNoClass(className); |
| return false; |
| } else { |
| processClass(cf); |
| return true; |
| } |
| } catch (ConstantPoolException ex) { |
| errorException(ex); |
| return false; |
| } |
| } |
| |
| /** |
| * Scans the named class file for uses of deprecated APIs. |
| * |
| * @param fileName the class file to scan |
| * @return true on success, false on failure |
| */ |
| public boolean processClassFile(String fileName) { |
| Path path = Paths.get(fileName); |
| try { |
| ClassFile cf = ClassFile.read(path); |
| processClass(cf); |
| return true; |
| } catch (NoSuchFileException nsfe) { |
| errorNoFile(fileName); |
| } catch (IOException | ConstantPoolException ex) { |
| errorException(ex); |
| } |
| return false; |
| } |
| } |