Package com.android.tools.lint.checks

Source Code of com.android.tools.lint.checks.MissingClassDetector

/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.tools.lint.checks;

import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_CLASS;
import static com.android.SdkConstants.ATTR_FRAGMENT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_HEADER;
import static com.android.SdkConstants.TAG_PROVIDER;
import static com.android.SdkConstants.TAG_RECEIVER;
import static com.android.SdkConstants.TAG_SERVICE;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.VIEW_FRAGMENT;
import static com.android.SdkConstants.VIEW_TAG;
import static com.android.resources.ResourceFolderType.LAYOUT;
import static com.android.resources.ResourceFolderType.VALUES;
import static com.android.resources.ResourceFolderType.XML;

import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.SdkUtils;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Checks to ensure that classes referenced in the manifest actually exist and are included
*
*/
public class MissingClassDetector extends LayoutDetector implements ClassScanner {

    /** Package prefix indicating an Amazon control */
    private static final String AMAZON_CONTROL_PREFIX = "amazon/";            //$NON-NLS-1$
    /** Package prefix indicating a Euclid control */
    private static final String EUCLID_CONTROL_PREFIX = "com/amazon/euclid/"; //$NON-NLS-1$

    /** Manifest-referenced classes missing from the project or libraries */
    public static final Issue MISSING = Issue.create(
            "MissingRegistered", //$NON-NLS-1$
            "Missing registered class",
            "Ensures that classes referenced in the manifest are present in the project or libraries",

            "If a class is referenced in the manifest, it must also exist in the project (or in one " +
            "of the libraries included by the project. This check helps uncover typos in " +
            "registration names, or attempts to rename or move classes without updating the " +
            "manifest file properly.",

            Category.CORRECTNESS,
            8,
            Severity.ERROR,
            new Implementation(
                    MissingClassDetector.class,
                    EnumSet.of(Scope.MANIFEST, Scope.CLASS_FILE,
                            Scope.JAVA_LIBRARIES, Scope.RESOURCE_FILE)))
            .addMoreInfo("http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$

    /** Are activity, service, receiver etc subclasses instantiatable? */
    public static final Issue INSTANTIATABLE = Issue.create(
            "Instantiatable", //$NON-NLS-1$
            "Registered class is not instantiatable",
            "Ensures that classes registered in the manifest file are instantiatable",

            "Activities, services, broadcast receivers etc. registered in the manifest file " +
            "must be \"instantiatable\" by the system, which means that the class must be " +
            "public, it must have an empty public constructor, and if it's an inner class, " +
            "it must be a static inner class.",

            Category.CORRECTNESS,
            6,
            Severity.FATAL,
            new Implementation(
                    MissingClassDetector.class,
                    Scope.CLASS_FILE_SCOPE));

    /** Is the right character used for inner class separators? */
    public static final Issue INNERCLASS = Issue.create(
            "InnerclassSeparator", //$NON-NLS-1$
            "Inner classes should use `$` rather than `.`",
            "Ensures that inner classes are referenced using '$' instead of '.' in class names",

            "When you reference an inner class in a manifest file, you must use '$' instead of '.' " +
            "as the separator character, i.e. Outer$Inner instead of Outer.Inner.\n" +
            "\n" +
            "(If you get this warning for a class which is not actually an inner class, it's " +
            "because you are using uppercase characters in your package name, which is not " +
            "conventional.)",

            Category.CORRECTNESS,
            3,
            Severity.WARNING,
            new Implementation(
                    MissingClassDetector.class,
                    Scope.MANIFEST_SCOPE));

    private Map<String, Location.Handle> mReferencedClasses;
    private Set<String> mCustomViews;
    private boolean mHaveClasses;

    /** Constructs a new {@link MissingClassDetector} */
    public MissingClassDetector() {
    }

    @NonNull
    @Override
    public Speed getSpeed() {
        return Speed.FAST;
    }

    // ---- Implements XmlScanner ----

    @Override
    public Collection<String> getApplicableElements() {
        return ALL;
    }

    @Override
    public boolean appliesTo(@NonNull ResourceFolderType folderType) {
        return folderType == VALUES || folderType == LAYOUT || folderType == XML;
    }

    @Override
    public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
        String pkg = null;
        Node classNameNode;
        String className;
        String tag = element.getTagName();
        ResourceFolderType folderType = context.getResourceFolderType();
        if (folderType == VALUES) {
            if (!tag.equals(TAG_STRING)) {
                return;
            }
            Attr attr = element.getAttributeNode(ATTR_NAME);
            if (attr == null) {
                return;
            }
            className = attr.getValue();
            classNameNode = attr;
        } else if (folderType == LAYOUT) {
            if (tag.indexOf('.') > 0) {
                className = tag;
                classNameNode = element;
            } else if (tag.equals(VIEW_FRAGMENT) || tag.equals(VIEW_TAG)) {
                Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
                if (attr == null) {
                    attr = element.getAttributeNode(ATTR_CLASS);
                }
                if (attr == null) {
                    return;
                }
                className = attr.getValue();
                classNameNode = attr;
            } else {
                return;
            }
        } else if (folderType == XML) {
            if (!tag.equals(TAG_HEADER)) {
                return;
            }
            Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_FRAGMENT);
            if (attr == null) {
                return;
            }
            className = attr.getValue();
            classNameNode = attr;
        } else {
            // Manifest file
            if (TAG_APPLICATION.equals(tag)
                    || TAG_ACTIVITY.equals(tag)
                    || TAG_SERVICE.equals(tag)
                    || TAG_RECEIVER.equals(tag)
                    || TAG_PROVIDER.equals(tag)) {
                Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
                if (attr == null) {
                    return;
                }
                className = attr.getValue();
                classNameNode = attr;
                pkg = context.getMainProject().getPackage();
            } else {
                return;
            }
        }
        if (className.isEmpty()) {
            return;
        }

        String fqcn;
        int dotIndex = className.indexOf('.');
        if (dotIndex <= 0) {
            if (pkg == null) {
                return; // value file
            }
            if (dotIndex == 0) {
                fqcn = pkg + className;
            } else {
                // According to the <activity> manifest element documentation, this is not
                // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html )
                // but it appears in manifest files and appears to be supported by the runtime
                // so handle this in code as well:
                fqcn = pkg + '.' + className;
            }
        } else { // else: the class name is already a fully qualified class name
            fqcn = className;
            // Only look for fully qualified tracker names in analytics files
            if (folderType == VALUES
                    && !SdkUtils.endsWith(context.file.getPath(), "analytics.xml")) { //$NON-NLS-1$
                return;
            }
        }

        String signature = ClassContext.getInternalName(fqcn);
        if (signature.isEmpty() || signature.startsWith(ANDROID_PKG_PREFIX)) {
            return;
        }

        if (!context.getProject().getReportIssues()) {
            // If this is a library project not being analyzed, ignore it
            return;
        }

        Handle handle = null;
        if (!context.getDriver().isSuppressed(context, MISSING, element)) {
            if (mReferencedClasses == null) {
                mReferencedClasses = Maps.newHashMapWithExpectedSize(16);
                mCustomViews = Sets.newHashSetWithExpectedSize(8);
            }

            handle = context.createLocationHandle(element);
            mReferencedClasses.put(signature, handle);
            if (folderType == LAYOUT && !tag.equals(VIEW_FRAGMENT)) {
                mCustomViews.add(ClassContext.getInternalName(className));
            }
        }

        if (signature.indexOf('$') != -1) {
            if (pkg != null && className.indexOf('$') == -1 && className.indexOf('.', 1) > 0) {
                boolean haveUpperCase = false;
                for (int i = 0, n = pkg.length(); i < n; i++) {
                    if (Character.isUpperCase(pkg.charAt(i))) {
                        haveUpperCase = true;
                        break;
                    }
                }
                if (!haveUpperCase) {
                    String message = "Use '$' instead of '.' for inner classes " +
                            "(or use only lowercase letters in package names)";
                    Location location = context.getLocation(classNameNode);
                    context.report(INNERCLASS, element, location, message, null);
                }
            }

            // The internal name contains a $ which means it's an inner class.
            // The conversion from fqcn to internal name is a bit ambiguous:
            // "a.b.C.D" usually means "inner class D in class C in package a.b".
            // However, it can (see issue 31592) also mean class D in package "a.b.C".
            // To make sure we don't falsely complain that foo/Bar$Baz doesn't exist,
            // in case the user has actually created a package named foo/Bar and a proper
            // class named Baz, we register *both* into the reference map.
            // When generating errors we'll look for these an rip them back out if
            // it looks like one of the two variations have been seen.
            if (handle != null) {
                // Assume that each successive $ is really a capitalized package name
                // instead. In other words, for A$B$C$D (assumed to be class A with
                // inner classes A.B, A.B.C and A.B.C.D) generate the following possible
                // referenced classes A/B$C$D (class B in package A with inner classes C and C.D),
                // A/B/C$D and A/B/C/D
                while (true) {
                    int index = signature.indexOf('$');
                    if (index == -1) {
                        break;
                    }
                    signature = signature.substring(0, index) + '/'
                            + signature.substring(index + 1);
                    mReferencedClasses.put(signature, handle);
                    if (folderType == LAYOUT && !tag.equals(VIEW_FRAGMENT)) {
                        mCustomViews.add(signature);
                    }
                }
            }
        }
    }

    @Override
    public void afterCheckProject(@NonNull Context context) {
        if (!context.getProject().isLibrary() && mHaveClasses
                && mReferencedClasses != null && !mReferencedClasses.isEmpty()
                && context.getDriver().getScope().contains(Scope.CLASS_FILE)) {
            List<String> classes = new ArrayList<String>(mReferencedClasses.keySet());
            Collections.sort(classes);
            for (String owner : classes) {
                Location.Handle handle = mReferencedClasses.get(owner);
                String fqcn = ClassContext.getFqcn(owner);

                String signature = ClassContext.getInternalName(fqcn);
                if (!signature.equals(owner)) {
                    if (!mReferencedClasses.containsKey(signature)) {
                        continue;
                    }
                } else if (signature.indexOf('$') != -1) {
                    signature = signature.replace('$', '/');
                    if (!mReferencedClasses.containsKey(signature)) {
                        continue;
                    }
                }
                mReferencedClasses.remove(owner);

                // Ignore usages of platform libraries
                if (owner.startsWith("android/") || owner.startsWith(AMAZON_CONTROL_PREFIX) || owner.startsWith(EUCLID_CONTROL_PREFIX)) { //$NON-NLS-1$
                    continue;
                }

                String message = String.format(
                        "Class referenced in the manifest, %1$s, was not found in the " +
                                "project or the libraries", fqcn);
                Location location = handle.resolve();
                File parentFile = location.getFile().getParentFile();
                if (parentFile != null) {
                    String parent = parentFile.getName();
                    ResourceFolderType type = ResourceFolderType.getFolderType(parent);
                    if (type == LAYOUT) {
                        message = String.format(
                            "Class referenced in the layout file, %1$s, was not found in "
                                + "the project or the libraries", fqcn);
                    } else if (type == XML) {
                        message = String.format(
                                "Class referenced in the preference header file, %1$s, was not "
                                        + "found in the project or the libraries", fqcn);

                    } else if (type == VALUES) {
                        message = String.format(
                                "Class referenced in the analytics file, %1$s, was not "
                                        + "found in the project or the libraries", fqcn);
                    }
                }

                context.report(MISSING, location, message, null);
            }
        }
    }

    // ---- Implements ClassScanner ----

    @Override
    public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
        if (!mHaveClasses && !context.isFromClassLibrary()
                && context.getProject() == context.getMainProject()) {
            mHaveClasses = true;
        }
        String curr = classNode.name;
        if (mReferencedClasses != null && mReferencedClasses.containsKey(curr)) {
            boolean isCustomView = mCustomViews.contains(curr);
            removeReferences(curr);

            // Ensure that the class is public, non static and has a null constructor!

            if ((classNode.access & Opcodes.ACC_PUBLIC) == 0) {
                context.report(INSTANTIATABLE, context.getLocation(classNode), String.format(
                        "This class should be public (%1$s)",
                            ClassContext.createSignature(classNode.name, null, null)),
                        null);
                return;
            }

            if (classNode.name.indexOf('$') != -1 && !LintUtils.isStaticInnerClass(classNode)) {
                context.report(INSTANTIATABLE, context.getLocation(classNode), String.format(
                        "This inner class should be static (%1$s)",
                            ClassContext.createSignature(classNode.name, null, null)),
                        null);
                return;
            }

            boolean hasDefaultConstructor = false;
            @SuppressWarnings("rawtypes") // ASM API
            List methodList = classNode.methods;
            for (Object m : methodList) {
                MethodNode method = (MethodNode) m;
                if (method.name.equals(CONSTRUCTOR_NAME)) {
                    if (method.desc.equals("()V")) { //$NON-NLS-1$
                        // The constructor must be public
                        if ((method.access & Opcodes.ACC_PUBLIC) != 0) {
                            hasDefaultConstructor = true;
                        } else {
                            context.report(INSTANTIATABLE, context.getLocation(method, classNode),
                                    "The default constructor must be public",
                                    null);
                            // Also mark that we have a constructor so we don't complain again
                            // below since we've already emitted a more specific error related
                            // to the default constructor
                            hasDefaultConstructor = true;
                        }
                    }
                }
            }

            if (!hasDefaultConstructor && !isCustomView && !context.isFromClassLibrary()
                    && context.getProject().getReportIssues()) {
                context.report(INSTANTIATABLE, context.getLocation(classNode), String.format(
                        "This class should provide a default constructor (a public " +
                        "constructor with no arguments) (%1$s)",
                            ClassContext.createSignature(classNode.name, null, null)),
                        null);
            }
        }
    }

    private void removeReferences(String curr) {
        mReferencedClasses.remove(curr);

        // Since "A.B.C" is ambiguous whether it's referencing a class in package A.B or
        // an inner class C in package A, we insert multiple possible references when we
        // encounter the A.B.C reference; now that we've seen the actual class we need to
        // remove all the possible permutations we've added such that the permutations
        // don't count as unreferenced classes.
        int index = curr.lastIndexOf('/');
        if (index == -1) {
            return;
        }
        boolean hasCapitalizedPackageName = false;
        for (int i = index - 1; i >= 0; i--) {
            char c = curr.charAt(i);
            if (Character.isUpperCase(c)) {
                hasCapitalizedPackageName = true;
                break;
            }
        }
        if (!hasCapitalizedPackageName) {
            // No path ambiguity
            return;
        }

        while (true) {
            index = curr.lastIndexOf('/');
            if (index == -1) {
                break;
            }
            curr = curr.substring(0, index) + '$' + curr.substring(index + 1);
            mReferencedClasses.remove(curr);
        }
    }
}
TOP

Related Classes of com.android.tools.lint.checks.MissingClassDetector

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.