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