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