blob: d6bb137b4b6df36d9b3a0461ceb1dc16fb0395da [file] [log] [blame]
/*
* 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 com.android.SdkConstants.ANDROID_PKG_PREFIX
import com.android.SdkConstants.ANDROID_URI
import com.android.SdkConstants.ATTR_CLASS
import com.android.SdkConstants.ATTR_FRAGMENT
import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.AUTO_URI
import com.android.SdkConstants.CLASS_ACTIVITY
import com.android.SdkConstants.CLASS_APPLICATION
import com.android.SdkConstants.CLASS_BROADCASTRECEIVER
import com.android.SdkConstants.CLASS_CONTENTPROVIDER
import com.android.SdkConstants.CLASS_FRAGMENT
import com.android.SdkConstants.CLASS_SERVICE
import com.android.SdkConstants.CLASS_V4_FRAGMENT
import com.android.SdkConstants.CLASS_VIEW
import com.android.SdkConstants.TAG_ACTIVITY
import com.android.SdkConstants.TAG_APPLICATION
import com.android.SdkConstants.TAG_HEADER
import com.android.SdkConstants.TAG_ITEM
import com.android.SdkConstants.TAG_PROVIDER
import com.android.SdkConstants.TAG_RECEIVER
import com.android.SdkConstants.TAG_SERVICE
import com.android.SdkConstants.TAG_STRING
import com.android.SdkConstants.VIEW_FRAGMENT
import com.android.SdkConstants.VIEW_TAG
import com.android.resources.ResourceFolderType
import com.android.resources.ResourceFolderType.DRAWABLE
import com.android.resources.ResourceFolderType.LAYOUT
import com.android.resources.ResourceFolderType.MENU
import com.android.resources.ResourceFolderType.TRANSITION
import com.android.resources.ResourceFolderType.VALUES
import com.android.resources.ResourceFolderType.XML
import com.android.tools.lint.checks.AppCompatResourceDetector.ATTR_ACTION_PROVIDER_CLASS
import com.android.tools.lint.checks.AppCompatResourceDetector.ATTR_ACTION_VIEW_CLASS
import com.android.tools.lint.client.api.JavaEvaluator
import com.android.tools.lint.client.api.LintClient.Companion.isStudio
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ClassScanner
import com.android.tools.lint.detector.api.Context
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.LintFix
import com.android.tools.lint.detector.api.Location
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.XmlContext
import com.android.tools.lint.detector.api.getInternalName
import com.android.utils.SdkUtils.endsWith
import com.intellij.psi.CommonClassNames
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiModifier
import com.intellij.psi.util.PsiUtil
import org.jetbrains.uast.kotlin.KotlinUClass
import org.w3c.dom.Attr
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.util.Locale
/** Checks to ensure that classes referenced in the manifest actually exist and are included */
class MissingClassDetector : LayoutDetector(), ClassScanner {
/**
* Prevent checking the same class more than once since it can be referenced
* repeatedly. The value in the map is true if the class is okay and false if it
* is not.
*/
private var checkedClasses: MutableMap<String, Boolean> = mutableMapOf()
// ---- Implements XmlScanner ----
override fun getApplicableElements(): Collection<String>? {
return ALL
}
override fun appliesTo(folderType: ResourceFolderType): Boolean =
folderType == VALUES ||
folderType == LAYOUT ||
folderType == XML ||
folderType == DRAWABLE ||
folderType == MENU ||
folderType == TRANSITION
override fun visitElement(
context: XmlContext,
element: Element
) {
val tag = element.tagName
@Suppress("NON_EXHAUSTIVE_WHEN")
when (context.resourceFolderType) {
null -> { // Manifest file
if (TAG_APPLICATION == tag ||
TAG_ACTIVITY == tag ||
TAG_SERVICE == tag ||
TAG_RECEIVER == tag ||
TAG_PROVIDER == tag
) {
val attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME) ?: return
val pkg = context.project.getPackage()
checkClassReference(
context,
pkg,
attr.value,
attr,
element,
requireInstantiatable = true,
expectedParent = when (tag) {
TAG_ACTIVITY -> CLASS_ACTIVITY
TAG_SERVICE -> CLASS_SERVICE
TAG_RECEIVER -> CLASS_BROADCASTRECEIVER
TAG_PROVIDER -> CLASS_CONTENTPROVIDER
TAG_APPLICATION -> CLASS_APPLICATION
else -> null
}
)
}
}
VALUES -> {
// Check class name in analytics references
// Only look for fully qualified tracker names in analytics files
if (tag == TAG_STRING && endsWith(context.file.path, "analytics.xml")) {
val attr = element.getAttributeNode(ATTR_NAME) ?: return
checkClassReference(context, null, attr.value, attr, element)
}
}
LAYOUT -> {
when {
tag.indexOf('.') > 0 -> {
checkClassReference(
context, null, tag, element, element,
// Already doing hierarchy checks in Studio, don't duplicate effort
expectedParent = if (isStudio) null else CLASS_VIEW
)
}
tag == VIEW_TAG -> {
val attr = element.getAttributeNode(ATTR_CLASS) ?: return
checkClassReference(
context, null, attr.value, attr, element,
expectedParent = if (isStudio) null else CLASS_VIEW
)
}
tag == VIEW_FRAGMENT -> {
val attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME)
?: element.getAttributeNode(ATTR_CLASS)
?: return
checkClassReference(
context, null, attr.value, attr, element,
requireInstantiatable = true,
expectedParent = CLASS_FRAGMENT
)
}
}
}
DRAWABLE -> {
when {
tag.indexOf('.') > 0 -> {
checkClassReference(context, null, tag, element, element)
}
tag == "drawable" -> {
val attr = element.getAttributeNode(ATTR_CLASS)
?: return
checkClassReference(
context, null, attr.value, attr, element,
expectedParent = "android.graphics.drawable.Drawable"
)
}
}
}
TRANSITION -> {
if (tag == "transition" || tag == "pathMotion") {
val attr = element.getAttributeNode(ATTR_CLASS)
?: return
val expectedParent = if (tag == "transition")
"android.transition.Transition"
else
"android.transition.PathMotion"
checkClassReference(
context, null, attr.value, attr, element, expectedParent = expectedParent
)
}
}
XML -> {
if (tag == TAG_HEADER) {
val attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_FRAGMENT) ?: return
checkClassReference(
context, null, attr.value, attr, element,
requireInstantiatable = true, expectedParent = CLASS_FRAGMENT
)
}
}
MENU -> {
if (tag == TAG_ITEM) {
val view = element.getAttributeNodeNS(AUTO_URI, ATTR_ACTION_VIEW_CLASS)
?: element.getAttributeNodeNS(ANDROID_URI, ATTR_ACTION_VIEW_CLASS)
if (view != null) {
checkClassReference(
context, null, view.value, view, element,
expectedParent = CLASS_VIEW
)
}
val provider = element.getAttributeNodeNS(AUTO_URI, ATTR_ACTION_PROVIDER_CLASS)
?: element.getAttributeNodeNS(ANDROID_URI, ATTR_ACTION_PROVIDER_CLASS)
?: return
// Consider checking for one of the action provider parent classes
checkClassReference(context, null, provider.value, provider, element)
}
}
}
}
private fun checkClassReference(
context: XmlContext,
pkg: String?,
className: String,
classNameNode: Node,
element: Element,
requireInstantiatable: Boolean = false,
expectedParent: String? = null
) {
if (className.isEmpty()) {
return
}
val fqcn: String
val dotIndex = className.indexOf('.')
if (dotIndex <= 0) {
if (pkg == null) {
return // not a manifest file; no implicit package
}
fqcn = if (dotIndex == 0) {
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:
"$pkg.$className"
}
} else {
// else: the class name is already a fully qualified class name
fqcn = className
}
if (fqcn.startsWith(ANDROID_PKG_PREFIX) ||
fqcn.startsWith("com.android.internal.")
) {
return
}
var ok = checkedClasses[fqcn]
if (ok == null) {
val parser = context.client.getUastParser(context.mainProject)
val evaluator = parser.evaluator
val cls = evaluator.findClass(fqcn.replace('$', '.'))
if (cls != null) {
ok = true
if (requireInstantiatable) {
checkInstantiatable(context, evaluator, cls, fqcn, classNameNode)
}
checkInnerClassReference(context, cls, className, classNameNode, element)
if (expectedParent != null) {
checkExpectedParent(context, evaluator, classNameNode, cls, expectedParent)
}
} else {
ok = false
}
checkedClasses[fqcn] = ok
}
if (ok == false) {
val location = getRefLocation(context, classNameNode)
reportMissing(location, fqcn, context)
}
}
private fun checkExpectedParent(
context: XmlContext,
evaluator: JavaEvaluator,
nameNode: Node,
cls: PsiClass,
expectedParent: String
) {
if (!evaluator.inheritsFrom(cls, expectedParent, false)) {
if (expectedParent == CLASS_FRAGMENT) {
checkExpectedParent(context, evaluator, nameNode, cls, CLASS_V4_FRAGMENT.oldName())
return
} else if (expectedParent == CLASS_V4_FRAGMENT.oldName()) {
checkExpectedParent(context, evaluator, nameNode, cls, CLASS_V4_FRAGMENT.newName())
return
}
// Safety check: make sure we can actually resolve the super classes; if not, we
// can have false positives. This can happen when you extend Kotlin classes in
// a different module with a source dependency (e.g. in Gradle with checkDependencies
// true) since those source files aren't fed to the top down analyzer. See b/158128960.
var curr = cls.superClass ?: return
while (true) {
val qualifiedName = curr.qualifiedName
if (qualifiedName == expectedParent) {
return
} else if (qualifiedName == CommonClassNames.JAVA_LANG_OBJECT) {
break
}
curr = curr.superClass ?: return
}
val message =
if (expectedParent.contains("Fragment")) {
"`${cls.name}` must be a fragment"
} else {
"`${cls.name}` must extend $expectedParent"
}
context.report(INSTANTIATABLE, getRefLocation(context, nameNode), message)
}
}
private fun checkInnerClassReference(
context: XmlContext,
cls: PsiClass,
className: String,
nameNode: Node,
element: Element
) {
val name = cls.name
if (cls.containingClass == null || name == null || className.contains("$")) {
return
}
val full = getInternalName(cls)?.replace('/', '.') ?: return
val fixed = full.substring(full.length - className.length)
val message =
"Use '$' instead of '.' for inner classes; replace \"$className\" with \"$fixed\""
val location = getRefLocation(context, nameNode)
val fix = LintFix.create().replace().text(className).with(fixed).autoFix().build()
context.report(INNERCLASS, element, location, message, fix)
}
/** Make sure [cls] is instantiatable */
private fun checkInstantiatable(
context: XmlContext,
evaluator: JavaEvaluator,
cls: PsiClass,
fqcn: String,
nameNode: Node
) {
if (evaluator.isPrivate(cls)) {
val message = "This class should be public (`$fqcn`)"
context.report(INSTANTIATABLE, getRefLocation(context, nameNode), message)
} else if (cls.containingClass != null && !evaluator.isStatic(cls)) {
val message = "This inner class should be static (`$fqcn`)"
context.report(INSTANTIATABLE, getRefLocation(context, nameNode), message)
} else {
val constructors = cls.constructors
if (constructors.isEmpty() && hasImplicitDefaultConstructor(cls)) {
return
}
for (constructor in cls.constructors) {
if (constructor.parameterList.isEmpty) {
if (evaluator.isPrivate(constructor)) {
val message =
"The default constructor must be public in `$fqcn`"
context.report(
INSTANTIATABLE,
getRefLocation(context, nameNode), message
)
return
} else {
return
}
}
}
val message =
"This class should provide a default constructor (a public constructor with no arguments) (`$fqcn`)"
context.report(INSTANTIATABLE, getRefLocation(context, nameNode), message)
}
}
private fun getRefLocation(
context: XmlContext,
nameNode: Node
): Location {
return if (nameNode is Attr) {
context.getValueLocation(nameNode)
} else {
context.getLocation(nameNode)
}
}
private fun hasImplicitDefaultConstructor(psiClass: PsiClass): Boolean {
if (psiClass is KotlinUClass && psiClass.sourcePsi == null) {
// Top level kt classes (FooKt for Foo.kt) do not have implicit default constructor
return false
}
val constructors = psiClass.constructors
if (constructors.isEmpty() && !psiClass.isInterface && !psiClass.isAnnotationType && !psiClass.isEnum) {
if (PsiUtil.hasDefaultConstructor(psiClass)) {
return true
}
// The above method isn't always right; for example, for the ContactsContract.Presence class
// in the framework, which looks like this:
// @Deprecated
// public static final class Presence extends StatusUpdates {
// }
// javac makes a default constructor:
// public final class android.provider.ContactsContract$Presence extends android.provider.ContactsContract$StatusUpdates {
// public android.provider.ContactsContract$Presence();
// }
// but the above method returns false. So add some of our own heuristics:
if (psiClass.hasModifierProperty(PsiModifier.FINAL) &&
!psiClass.hasModifierProperty(PsiModifier.ABSTRACT) &&
psiClass.hasModifierProperty(PsiModifier.PUBLIC)
) {
return true
}
}
return false
}
private fun reportMissing(
location: Location,
fqcn: String,
context: Context
) {
val parentFile = location.file.parentFile
val target =
if (parentFile != null) {
val parent = parentFile.name
when (val type = ResourceFolderType.getFolderType(parent)) {
null -> "manifest"
LAYOUT -> "layout file"
XML -> "preference header file"
VALUES -> "analytics file"
else -> {
"${type.getName().toLowerCase(Locale.US)} file"
}
}
} else {
"the manifest"
}
val message =
"Class referenced in the $target, `$fqcn`, was not found in the project or the libraries"
context.report(MISSING, location, message)
}
companion object {
val IMPLEMENTATION = Implementation(
MissingClassDetector::class.java,
Scope.MANIFEST_AND_RESOURCE_SCOPE,
Scope.MANIFEST_SCOPE,
Scope.RESOURCE_FILE_SCOPE
)
/** Manifest or layout referenced classes missing from the project or libraries */
@JvmField
val MISSING =
Issue.create(
id = "MissingClass",
briefDescription = "Missing registered class",
explanation = """
If a class is referenced in the manifest or in a layout file, 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 XML references
properly.
""",
category = Category.CORRECTNESS,
priority = 8,
severity = Severity.ERROR,
moreInfo = "https://developer.android.com/guide/topics/manifest/manifest-intro.html",
androidSpecific = true,
implementation = IMPLEMENTATION
)
/** Are activity, service, receiver etc subclasses instantiatable? */
@JvmField
val INSTANTIATABLE =
Issue.create(
id = "Instantiatable",
briefDescription = "Registered class is not instantiatable",
explanation = """
Activities, services, broadcast receivers etc. registered in the \
manifest file (or for custom views, in a layout 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 = Category.CORRECTNESS,
priority = 6,
severity = Severity.FATAL,
androidSpecific = true,
implementation = IMPLEMENTATION
)
/** Is the right character used for inner class separators? */
@JvmField
val INNERCLASS =
Issue.create(
id = "InnerclassSeparator",
briefDescription = "Inner classes should use `${"$"}` rather than `.`",
explanation = """
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.
(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 = Category.CORRECTNESS,
priority = 3,
severity = Severity.WARNING,
androidSpecific = true,
implementation = IMPLEMENTATION
)
}
}