| /* |
| * 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 { |
| /** 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)) { |
| 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(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); |
| } |
| } |
| } |
| } |