com.android.tools.lint.checks.MissingClassDetector.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.lint.checks.MissingClassDetector.java

Source

/*
 * 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.ATTR_PACKAGE;
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 {
    /** 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.WARNING,
            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)) {
                Element root = element.getOwnerDocument().getDocumentElement();
                pkg = root.getAttribute(ATTR_PACKAGE);
                Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
                if (attr == null) {
                    return;
                }
                className = attr.getValue();
                classNameNode = attr;
            } 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(MISSING, element)) {
            if (mReferencedClasses == null) {
                mReferencedClasses = Maps.newHashMapWithExpectedSize(16);
                mCustomViews = Sets.newHashSetWithExpectedSize(8);
            }

            handle = context.parser.createLocationHandle(context, 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) {
                signature = signature.replace('$', '/');
                mReferencedClasses.put(signature, handle);
            }
        }
    }

    @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/")) { //$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);
            mReferencedClasses.remove(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);
            }
        }
    }
}