Scan.java revision 3736:68754738ba9c
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
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    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}
625