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