1 /*
   2  * Copyright (c) 2016, 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.scan;
  27 
  28 import java.io.IOException;
  29 import java.io.PrintStream;
  30 import java.nio.file.Files;
  31 import java.nio.file.NoSuchFileException;
  32 import java.nio.file.Path;
  33 import java.nio.file.Paths;
  34 import java.util.ArrayDeque;
  35 import java.util.Deque;
  36 import java.util.Enumeration;
  37 import java.util.List;
  38 import java.util.jar.JarEntry;
  39 import java.util.jar.JarFile;
  40 import java.util.regex.Matcher;
  41 import java.util.regex.Pattern;
  42 import java.util.stream.Collectors;
  43 import java.util.stream.Stream;
  44 
  45 import com.sun.tools.classfile.*;
  46 import com.sun.tools.jdeprscan.DeprData;
  47 import com.sun.tools.jdeprscan.DeprDB;
  48 import com.sun.tools.jdeprscan.Messages;
  49 
  50 import static com.sun.tools.classfile.AccessFlags.*;
  51 import static com.sun.tools.classfile.ConstantPool.*;
  52 
  53 /**
  54  * An object that represents the scanning phase of deprecation usage checking.
  55  * Given a deprecation database, scans the targeted directory hierarchy, jar
  56  * file, or individual class for uses of deprecated APIs.
  57  */
  58 public class Scan {
  59     final PrintStream out;
  60     final PrintStream err;
  61     final List<String> classPath;
  62     final DeprDB db;
  63     final boolean verbose;
  64 
  65     final ClassFinder finder;
  66     boolean errorOccurred = false;
  67 
  68     public Scan(PrintStream out,
  69                 PrintStream err,
  70                 List<String> classPath,
  71                 DeprDB db,
  72                 boolean verbose) {
  73         this.out = out;
  74         this.err = err;
  75         this.classPath = classPath;
  76         this.db = db;
  77         this.verbose = verbose;
  78 
  79         ClassFinder f = new ClassFinder(verbose);
  80 
  81         // TODO: this isn't quite right. If we've specified a release other than the current
  82         // one, we should instead add a reference to the symbol file for that release instead
  83         // of the current image. The problems are a) it's unclear how to get from a release
  84         // to paths that reference the symbol files, as this might be internal to the file
  85         // manager; and b) the symbol file includes .sig files, not class files, which ClassFile
  86         // might not be able to handle.
  87         f.addJrt();
  88 
  89         for (String name : classPath) {
  90             if (name.endsWith(".jar")) {
  91                 f.addJar(name);
  92             } else {
  93                 f.addDir(name);
  94             }
  95         }
  96 
  97         finder = f;
  98     }
  99 
 100     Pattern typePattern = Pattern.compile("\\[*L(.*);");
 101 
 102     // "flattens" an array type name to its component type
 103     // and a reference type "Lpkg/pkg/pkg/name;" to its base name
 104     // "pkg/pkg/pkg/name".
 105     // TODO: deal with primitive types
 106     String flatten(String typeName) {
 107         Matcher matcher = typePattern.matcher(typeName);
 108         if (matcher.matches()) {
 109             return matcher.group(1);
 110         } else {
 111             return typeName;
 112         }
 113     }
 114 
 115     String typeKind(ClassFile cf) {
 116         AccessFlags flags = cf.access_flags;
 117         if (flags.is(ACC_ENUM)) {
 118             return "enum";
 119         } else if (flags.is(ACC_ANNOTATION)) {
 120             return "@interface";
 121         } else if (flags.is(ACC_INTERFACE)) {
 122             return "interface";
 123         } else {
 124             return "class";
 125         }
 126     }
 127 
 128     String dep(boolean forRemoval) {
 129         return Messages.get(forRemoval ? "scan.dep.removal" : "scan.dep.normal");
 130     }
 131 
 132     void printType(String key, ClassFile cf, String cname, boolean r)
 133             throws ConstantPoolException {
 134         out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, dep(r)));
 135     }
 136 
 137     void printMethod(String key, ClassFile cf, String cname, String mname, String rtype,
 138                      boolean r) throws ConstantPoolException {
 139         out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, mname, rtype, dep(r)));
 140     }
 141 
 142     void printField(String key, ClassFile cf, String cname, String fname,
 143                      boolean r) throws ConstantPoolException {
 144         out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, fname, dep(r)));
 145     }
 146 
 147     void printFieldType(String key, ClassFile cf, String cname, String fname, String type,
 148                      boolean r) throws ConstantPoolException {
 149         out.println(Messages.get(key, typeKind(cf), cf.getName(), cname, fname, type, dep(r)));
 150     }
 151 
 152     void printHasField(ClassFile cf, String fname, String type, boolean r)
 153             throws ConstantPoolException {
 154         out.println(Messages.get("scan.out.hasfield", typeKind(cf), cf.getName(), fname, type, dep(r)));
 155     }
 156 
 157     void printHasMethodParmType(ClassFile cf, String mname, String parmType, boolean r)
 158             throws ConstantPoolException {
 159         out.println(Messages.get("scan.out.methodparmtype", typeKind(cf), cf.getName(), mname, parmType, dep(r)));
 160     }
 161 
 162     void printHasMethodRetType(ClassFile cf, String mname, String retType, boolean r)
 163             throws ConstantPoolException {
 164         out.println(Messages.get("scan.out.methodrettype", typeKind(cf), cf.getName(), mname, retType, dep(r)));
 165     }
 166 
 167     void printHasOverriddenMethod(ClassFile cf, String overridden, String mname, String desc, boolean r)
 168             throws ConstantPoolException {
 169         out.println(Messages.get("scan.out.methodoverride", typeKind(cf), cf.getName(), overridden,
 170                                  mname, desc, dep(r)));
 171     }
 172 
 173     void errorException(Exception ex) {
 174         errorOccurred = true;
 175         err.println(Messages.get("scan.err.exception", ex.toString()));
 176         if (verbose) {
 177             ex.printStackTrace(err);
 178         }
 179     }
 180 
 181     void errorNoClass(String className) {
 182         errorOccurred = true;
 183         err.println(Messages.get("scan.err.noclass", className));
 184     }
 185 
 186     void errorNoFile(String fileName) {
 187         errorOccurred = true;
 188         err.println(Messages.get("scan.err.nofile", fileName));
 189     }
 190 
 191     void errorNoMethod(String className, String methodName, String desc) {
 192         errorOccurred = true;
 193         err.println(Messages.get("scan.err.nomethod", className, methodName, desc));
 194     }
 195 
 196     /**
 197      * Checks whether a member (method or field) is present in a class.
 198      * The checkMethod parameter determines whether this checks for a method
 199      * or for a field.
 200      *
 201      * @param targetClass the ClassFile of the class to search
 202      * @param targetName the method or field's name
 203      * @param targetDesc the methods descriptor (ignored if checkMethod is false)
 204      * @param checkMethod true if checking for method, false if checking for field
 205      * @return boolean indicating whether the member is present
 206      * @throws ConstantPoolException if a constant pool entry cannot be found
 207      */
 208     boolean isMemberPresent(ClassFile targetClass,
 209                             String targetName,
 210                             String targetDesc,
 211                             boolean checkMethod)
 212             throws ConstantPoolException {
 213         if (checkMethod) {
 214             for (Method m : targetClass.methods) {
 215                 String mname = m.getName(targetClass.constant_pool);
 216                 String mdesc = targetClass.constant_pool.getUTF8Value(m.descriptor.index);
 217                 if (targetName.equals(mname) && targetDesc.equals(mdesc)) {
 218                     return true;
 219                 }
 220             }
 221         } else {
 222             for (Field f : targetClass.fields) {
 223                 String fname = f.getName(targetClass.constant_pool);
 224                 if (targetName.equals(fname)) {
 225                     return true;
 226                 }
 227             }
 228         }
 229         return false;
 230     }
 231 
 232     /**
 233      * Adds all interfaces from this class to the deque of interfaces.
 234      *
 235      * @param intfs the deque of interfaces
 236      * @param cf the ClassFile of this class
 237      * @throws ConstantPoolException if a constant pool entry cannot be found
 238      */
 239     void addInterfaces(Deque<String> intfs, ClassFile cf)
 240             throws ConstantPoolException {
 241         int count = cf.interfaces.length;
 242         for (int i = 0; i < count; i++) {
 243             intfs.addLast(cf.getInterfaceName(i));
 244         }
 245     }
 246 
 247     /**
 248      * Resolves a member by searching this class and all its superclasses and
 249      * implemented interfaces.
 250      *
 251      * TODO: handles a few too many cases; needs cleanup.
 252      *
 253      * TODO: refine error handling
 254      *
 255      * @param cf the ClassFile of this class
 256      * @param startClassName the name of the class at which to start searching
 257      * @param findName the member name to search for
 258      * @param findDesc the method descriptor to search for (ignored for fields)
 259      * @param resolveMethod true if resolving a method, false if resolving a field
 260      * @param checkStartClass true if the start class should be searched, false if
 261      *                        it should be skipped
 262      * @return the name of the class where the member resolved, or null
 263      * @throws ConstantPoolException if a constant pool entry cannot be found
 264      */
 265     String resolveMember(
 266             ClassFile cf, String startClassName, String findName, String findDesc,
 267             boolean resolveMethod, boolean checkStartClass)
 268             throws ConstantPoolException {
 269         ClassFile startClass;
 270 
 271         if (cf.getName().equals(startClassName)) {
 272             startClass = cf;
 273         } else {
 274             startClass = finder.find(startClassName);
 275             if (startClass == null) {
 276                 errorNoClass(startClassName);
 277                 return startClassName;
 278             }
 279         }
 280 
 281         // follow super_class until it's 0, meaning we've reached Object
 282         // accumulate interfaces of superclasses as we go along
 283 
 284         ClassFile curClass = startClass;
 285         Deque<String> intfs = new ArrayDeque<>();
 286         while (true) {
 287             if ((checkStartClass || curClass != startClass) &&
 288                     isMemberPresent(curClass, findName, findDesc, resolveMethod)) {
 289                 break;
 290             }
 291 
 292             if (curClass.super_class == 0) { // reached Object
 293                 curClass = null;
 294                 break;
 295             }
 296 
 297             String superName = curClass.getSuperclassName();
 298             curClass = finder.find(superName);
 299             if (curClass == null) {
 300                 errorNoClass(superName);
 301                 break;
 302             }
 303             addInterfaces(intfs, curClass);
 304         }
 305 
 306         // search interfaces: add all interfaces and superinterfaces to queue
 307         // search until it's empty
 308 
 309         if (curClass == null) {
 310             addInterfaces(intfs, startClass);
 311             while (intfs.size() > 0) {
 312                 String intf = intfs.removeFirst();
 313                 curClass = finder.find(intf);
 314                 if (curClass == null) {
 315                     errorNoClass(intf);
 316                     break;
 317                 }
 318 
 319                 if (isMemberPresent(curClass, findName, findDesc, resolveMethod)) {
 320                     break;
 321                 }
 322 
 323                 addInterfaces(intfs, curClass);
 324             }
 325         }
 326 
 327         if (curClass == null) {
 328             if (checkStartClass) {
 329                 errorNoMethod(startClassName, findName, findDesc);
 330                 return startClassName;
 331             } else {
 332                 // TODO: refactor this
 333                 // checkStartClass == false means we're checking for overrides
 334                 // so not being able to resolve a method simply means there's
 335                 // no overriding, which isn't an error
 336                 return null;
 337             }
 338         } else {
 339             String foundClassName = curClass.getName();
 340             return foundClassName;
 341         }
 342     }
 343 
 344     /**
 345      * Checks the superclass of this class.
 346      *
 347      * @param cf the ClassFile of this class
 348      * @throws ConstantPoolException if a constant pool entry cannot be found
 349      */
 350     void checkSuper(ClassFile cf) throws ConstantPoolException {
 351         String sname = cf.getSuperclassName();
 352         DeprData dd = db.getTypeDeprecated(sname);
 353         if (dd != null) {
 354             printType("scan.out.extends", cf, sname, dd.isForRemoval());
 355         }
 356     }
 357 
 358     /**
 359      * Checks the interfaces of this class.
 360      *
 361      * @param cf the ClassFile of this class
 362      * @throws ConstantPoolException if a constant pool entry cannot be found
 363      */
 364     void checkInterfaces(ClassFile cf) throws ConstantPoolException {
 365         int ni = cf.interfaces.length;
 366         for (int i = 0; i < ni; i++) {
 367             String iname = cf.getInterfaceName(i);
 368             DeprData dd = db.getTypeDeprecated(iname);
 369             if (dd != null) {
 370                 printType("scan.out.implements", cf, iname, dd.isForRemoval());
 371             }
 372         }
 373     }
 374 
 375     /**
 376      * Checks Class_info entries in the constant pool.
 377      *
 378      * @param cf the ClassFile of this class
 379      * @param entries constant pool entries collected from this class
 380      * @throws ConstantPoolException if a constant pool entry cannot be found
 381      */
 382     void checkClasses(ClassFile cf, CPEntries entries) throws ConstantPoolException {
 383         for (ConstantPool.CONSTANT_Class_info ci : entries.classes) {
 384             String className = ci.getName();
 385             DeprData dd = db.getTypeDeprecated(flatten(className));
 386             if (dd != null) {
 387                 printType("scan.out.usesclass", cf, className, dd.isForRemoval());
 388             }
 389         }
 390     }
 391 
 392     /**
 393      * Checks methods referred to from the constant pool.
 394      *
 395      * @param cf the ClassFile of this class
 396      * @param nti the NameAndType_info from a MethodRef or InterfaceMethodRef entry
 397      * @param clname the class name
 398      * @param msgKey message key for localization
 399      * @throws ConstantPoolException if a constant pool entry cannot be found
 400      */
 401     void checkMethodRef(ClassFile cf,
 402                         String clname,
 403                         CONSTANT_NameAndType_info nti,
 404                         String msgKey) throws ConstantPoolException {
 405         String name = nti.getName();
 406         String type = nti.getType();
 407         clname = resolveMember(cf, flatten(clname), name, type, true, true);
 408         DeprData dd = db.getMethodDeprecated(clname, name, type);
 409         if (dd != null) {
 410             printMethod(msgKey, cf, clname, name, type, dd.isForRemoval());
 411         }
 412     }
 413 
 414     /**
 415      * Checks fields referred to from the constant pool.
 416      *
 417      * @param cf the ClassFile of this class
 418      * @throws ConstantPoolException if a constant pool entry cannot be found
 419      */
 420     void checkFieldRef(ClassFile cf,
 421                        ConstantPool.CONSTANT_Fieldref_info fri) throws ConstantPoolException {
 422         String clname = fri.getClassName();
 423         CONSTANT_NameAndType_info nti = fri.getNameAndTypeInfo();
 424         String name = nti.getName();
 425         String type = nti.getType();
 426 
 427         clname = resolveMember(cf, flatten(clname), name, type, false, true);
 428         DeprData dd = db.getFieldDeprecated(clname, name);
 429         if (dd != null) {
 430             printField("scan.out.usesfield", cf, clname, name, dd.isForRemoval());
 431         }
 432     }
 433 
 434     /**
 435      * Checks the fields declared in this class.
 436      *
 437      * @param cf the ClassFile of this class
 438      * @throws ConstantPoolException if a constant pool entry cannot be found
 439      */
 440     void checkFields(ClassFile cf) throws ConstantPoolException {
 441         for (Field f : cf.fields) {
 442             String type = cf.constant_pool.getUTF8Value(f.descriptor.index);
 443             DeprData dd = db.getTypeDeprecated(flatten(type));
 444             if (dd != null) {
 445                 printHasField(cf, f.getName(cf.constant_pool), type, dd.isForRemoval());
 446             }
 447         }
 448     }
 449 
 450     /**
 451      * Checks the methods declared in this class.
 452      *
 453      * @param cf the ClassFile object of this class
 454      * @throws ConstantPoolException if a constant pool entry cannot be found
 455      */
 456     void checkMethods(ClassFile cf) throws ConstantPoolException {
 457         for (Method m : cf.methods) {
 458             String mname = m.getName(cf.constant_pool);
 459             String desc = cf.constant_pool.getUTF8Value(m.descriptor.index);
 460             MethodSig sig = MethodSig.fromDesc(desc);
 461             DeprData dd;
 462 
 463             for (String parm : sig.getParameters()) {
 464                 dd = db.getTypeDeprecated(flatten(parm));
 465                 if (dd != null) {
 466                     printHasMethodParmType(cf, mname, parm, dd.isForRemoval());
 467                 }
 468             }
 469 
 470             String ret = sig.getReturnType();
 471             dd = db.getTypeDeprecated(flatten(ret));
 472             if (dd != null) {
 473                 printHasMethodRetType(cf, mname, ret, dd.isForRemoval());
 474             }
 475 
 476             // check overrides
 477             String overridden = resolveMember(cf, cf.getName(), mname, desc, true, false);
 478             if (overridden != null) {
 479                 dd = db.getMethodDeprecated(overridden, mname, desc);
 480                 if (dd != null) {
 481                     printHasOverriddenMethod(cf, overridden, mname, desc, dd.isForRemoval());
 482                 }
 483             }
 484         }
 485     }
 486 
 487     /**
 488      * Processes a single class file.
 489      *
 490      * @param cf the ClassFile of the class
 491      * @throws ConstantPoolException if a constant pool entry cannot be found
 492      */
 493     void processClass(ClassFile cf) throws ConstantPoolException {
 494         if (verbose) {
 495             out.println(Messages.get("scan.process.class", cf.getName()));
 496         }
 497 
 498         CPEntries entries = CPEntries.loadFrom(cf);
 499 
 500         checkSuper(cf);
 501         checkInterfaces(cf);
 502         checkClasses(cf, entries);
 503 
 504         for (ConstantPool.CONSTANT_Methodref_info mri : entries.methodRefs) {
 505             String clname = mri.getClassName();
 506             CONSTANT_NameAndType_info nti = mri.getNameAndTypeInfo();
 507             checkMethodRef(cf, clname, nti, "scan.out.usesmethod");
 508         }
 509 
 510         for (ConstantPool.CONSTANT_InterfaceMethodref_info imri : entries.intfMethodRefs) {
 511             String clname = imri.getClassName();
 512             CONSTANT_NameAndType_info nti = imri.getNameAndTypeInfo();
 513             checkMethodRef(cf, clname, nti, "scan.out.usesintfmethod");
 514         }
 515 
 516         for (ConstantPool.CONSTANT_Fieldref_info fri : entries.fieldRefs) {
 517             checkFieldRef(cf, fri);
 518         }
 519 
 520         checkFields(cf);
 521         checkMethods(cf);
 522     }
 523 
 524     /**
 525      * Scans a jar file for uses of deprecated APIs.
 526      *
 527      * @param jarname the jar file to process
 528      * @return true on success, false on failure
 529      */
 530     public boolean scanJar(String jarname) {
 531         try (JarFile jf = new JarFile(jarname)) {
 532             out.println(Messages.get("scan.head.jar", jarname));
 533             finder.addJar(jarname);
 534             Enumeration<JarEntry> entries = jf.entries();
 535             while (entries.hasMoreElements()) {
 536                 JarEntry entry = entries.nextElement();
 537                 String name = entry.getName();
 538                 if (name.endsWith(".class")
 539                         && !name.endsWith("package-info.class")
 540                         && !name.endsWith("module-info.class")) {
 541                     processClass(ClassFile.read(jf.getInputStream(entry)));
 542                 }
 543             }
 544             return true;
 545         } catch (NoSuchFileException nsfe) {
 546             errorNoFile(jarname);
 547         } catch (IOException | ConstantPoolException ex) {
 548             errorException(ex);
 549         }
 550         return false;
 551     }
 552 
 553     /**
 554      * Scans class files in the named directory hierarchy for uses of deprecated APIs.
 555      *
 556      * @param dirname the directory hierarchy to process
 557      * @return true on success, false on failure
 558      */
 559     public boolean scanDir(String dirname) {
 560         Path base = Paths.get(dirname);
 561         int baseCount = base.getNameCount();
 562         finder.addDir(dirname);
 563         try (Stream<Path> paths = Files.walk(Paths.get(dirname))) {
 564             List<Path> classes =
 565                 paths.filter(p -> p.getNameCount() > baseCount)
 566                      .filter(path -> path.toString().endsWith(".class"))
 567                      .filter(path -> !path.toString().endsWith("package-info.class"))
 568                      .filter(path -> !path.toString().endsWith("module-info.class"))
 569                      .collect(Collectors.toList());
 570 
 571             out.println(Messages.get("scan.head.dir", dirname));
 572 
 573             for (Path p : classes) {
 574                 processClass(ClassFile.read(p));
 575             }
 576             return true;
 577         } catch (IOException | ConstantPoolException ex) {
 578             errorException(ex);
 579             return false;
 580         }
 581     }
 582 
 583     /**
 584      * Scans the named class for uses of deprecated APIs.
 585      *
 586      * @param className the class to scan
 587      * @return true on success, false on failure
 588      */
 589     public boolean processClassName(String className) {
 590         try {
 591             ClassFile cf = finder.find(className);
 592             if (cf == null) {
 593                 errorNoClass(className);
 594                 return false;
 595             } else {
 596                 processClass(cf);
 597                 return true;
 598             }
 599         } catch (ConstantPoolException ex) {
 600             errorException(ex);
 601             return false;
 602         }
 603     }
 604 
 605     /**
 606      * Scans the named class file for uses of deprecated APIs.
 607      *
 608      * @param fileName the class file to scan
 609      * @return true on success, false on failure
 610      */
 611     public boolean processClassFile(String fileName) {
 612         Path path = Paths.get(fileName);
 613         try {
 614             ClassFile cf = ClassFile.read(path);
 615             processClass(cf);
 616             return true;
 617         } catch (NoSuchFileException nsfe) {
 618             errorNoFile(fileName);
 619         } catch (IOException | ConstantPoolException ex) {
 620             errorException(ex);
 621         }
 622         return false;
 623     }
 624 }