blob: a9e34ee69b58b17048fbd21eb9e4333226ff4d4d [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_URI
import com.android.SdkConstants.APP_PREFIX
import com.android.SdkConstants.AUTO_URI
import com.android.SdkConstants.TOOLS_PREFIX
import com.android.SdkConstants.TOOLS_URI
import com.android.SdkConstants.URI_PREFIX
import com.android.SdkConstants.XMLNS_ANDROID
import com.android.SdkConstants.XMLNS_PREFIX
import com.android.ide.common.rendering.api.ResourceNamespace
import com.android.resources.ResourceFolderType
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.ResourceXmlDetector
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.isEditableTo
import com.android.utils.XmlUtils.getFirstSubTag
import com.android.utils.XmlUtils.getNextTag
import org.w3c.dom.Attr
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.util.HashMap
/** Checks for various issues related to XML namespaces */
/** Constructs a new [NamespaceDetector] */
class NamespaceDetector : ResourceXmlDetector() {
private var unusedNamespaces: MutableMap<String, Attr>? = null
override fun visitDocument(context: XmlContext, document: Document) {
var haveCustomNamespace = false
val root = document.documentElement
val attributes = root.attributes
val n = attributes.length
for (i in 0 until n) {
val item = attributes.item(i)
val prefix = item.nodeName
if (prefix.startsWith(XMLNS_PREFIX)) {
val value = item.nodeValue
if (value != ANDROID_URI) {
val attribute = item as Attr
if (value.startsWith(URI_PREFIX)) {
haveCustomNamespace = true
val namespaces = unusedNamespaces ?: run {
val new = HashMap<String, Attr>()
unusedNamespaces = new
new
}
namespaces[prefix.substring(XMLNS_PREFIX.length)] = attribute
} else if (value.startsWith("urn:")) {
continue
} else if (!value.startsWith("http://")) {
if (context.isEnabled(TYPO) &&
// In XML there can be random XML documents from users
// with arbitrary schemas; let them use https if they want
context.resourceFolderType != ResourceFolderType.XML
) {
var fix: LintFix? = null
if (value.startsWith("https://")) {
fix = LintFix.create()
.replace()
.text("https")
.with("http")
.name("Replace with http://${value.substring(8)}")
.build()
}
context.report(
TYPO,
attribute,
context.getValueLocation(attribute),
//noinspection LintImplUnexpectedDomain
"Suspicious namespace: should start with `http://`",
fix
)
}
continue
} else if (value != AUTO_URI &&
value.contains("auto") &&
value.startsWith("http://schemas.android.com/")
) {
context.report(
RES_AUTO,
attribute,
context.getValueLocation(attribute),
"Suspicious namespace: Did you mean `$AUTO_URI`?"
)
} else if (value == TOOLS_URI && (prefix == XMLNS_ANDROID || prefix.endsWith(
APP_PREFIX
) && prefix == XMLNS_PREFIX + APP_PREFIX)
) {
context.report(
TYPO,
attribute,
context.getValueLocation(attribute),
"Suspicious namespace and prefix combination"
)
}
if (!context.isEnabled(TYPO)) {
continue
}
val name = attribute.name
if (name != XMLNS_ANDROID && name != XMLNS_A) {
// See if it looks like a typo
val resIndex = value.indexOf("/res/")
if (resIndex != -1 && value.length + 5 > URI_PREFIX.length) {
val urlPrefix = value.substring(0, resIndex + 5)
if (urlPrefix != URI_PREFIX && isEditableTo(URI_PREFIX, urlPrefix, 3)) {
val correctUri = URI_PREFIX + value.substring(resIndex + 5)
context.report(
TYPO,
attribute,
context.getValueLocation(attribute),
"Possible typo in URL: was `\"$value\"`, should " +
"probably be `\"$correctUri\"`"
)
}
}
continue
}
if (name == XMLNS_A) {
// For the "android" prefix we always assume that the namespace prefix
// should be our expected prefix, but for the "a" prefix we make sure
// that it's at least "close"; if you're bound it to something completely
// different, don't complain.
if (!isEditableTo(ANDROID_URI, value, 4)) {
continue
}
}
if (value.equals(ANDROID_URI, ignoreCase = true)) {
context.report(
TYPO,
attribute,
context.getValueLocation(attribute),
"URI is case sensitive: was `\"$value\"`, expected `\"$ANDROID_URI\"`"
)
} else {
context.report(
TYPO,
attribute,
context.getValueLocation(attribute),
"Unexpected namespace URI bound to the `\"android\"` " +
"prefix, was `$value`, expected `$ANDROID_URI`"
)
}
} else if (prefix != XMLNS_ANDROID && (prefix.endsWith(TOOLS_PREFIX) && prefix == XMLNS_PREFIX + TOOLS_PREFIX || prefix.endsWith(
APP_PREFIX
) && prefix == XMLNS_PREFIX + APP_PREFIX)
) {
val attribute = item as Attr
context.report(
TYPO,
attribute,
context.getValueLocation(attribute),
"Suspicious namespace and prefix combination"
)
}
}
}
if (haveCustomNamespace) {
val project = context.project
val checkCustomAttrs =
project.resourceNamespace == ResourceNamespace.RES_AUTO && (context.isEnabled(
CUSTOM_VIEW
) && project.isLibrary || context.isEnabled(RES_AUTO) && project.isGradleProject)
if (checkCustomAttrs) {
checkCustomNamespace(context, root)
}
val checkUnused = context.isEnabled(UNUSED)
if (checkUnused) {
checkUnused(root)
val namespaces = unusedNamespaces
if (namespaces != null && !namespaces.isEmpty()) {
for ((prefix, attribute) in namespaces) {
context.report(
UNUSED,
attribute,
context.getLocation(attribute),
"Unused namespace `$prefix`"
)
}
}
}
}
if (context.isEnabled(REDUNDANT)) {
var child = getFirstSubTag(root)
while (child != null) {
checkRedundant(context, child)
child = getNextTag(child)
}
}
}
private fun checkUnused(element: Element) {
val attributes = element.attributes
val n = attributes.length
for (i in 0 until n) {
val attribute = attributes.item(i) as Attr
val prefix = attribute.prefix
if (prefix != null) {
unusedNamespaces?.remove(prefix)
}
}
var child = getFirstSubTag(element)
while (child != null) {
checkUnused(child)
child = getNextTag(child)
}
}
private fun checkRedundant(context: XmlContext, element: Element) {
// This method will not be called on the document element
val attributes = element.attributes
val n = attributes.length
for (i in 0 until n) {
val attribute = attributes.item(i) as Attr
val name = attribute.name
if (name.startsWith(XMLNS_PREFIX)) {
// See if this attribute is already set on the document element
val root = element.ownerDocument.documentElement
val redundant = root.getAttribute(name) == attribute.value
if (redundant) {
val fix =
fix().name("Delete namespace").set().remove(name).build()
context.report(
REDUNDANT, attribute, context.getLocation(attribute),
"This namespace declaration is redundant", fix
)
}
}
}
var child = getFirstSubTag(element)
while (child != null) {
checkRedundant(context, child)
child = getNextTag(child)
}
}
private fun checkCustomNamespace(context: XmlContext, element: Element) {
val attributes = element.attributes
val n = attributes.length
for (i in 0 until n) {
val attribute = attributes.item(i) as Attr
if (attribute.name.startsWith(XMLNS_PREFIX)) {
val uri = attribute.value
if (uri != null &&
!uri.isEmpty() &&
uri.startsWith(URI_PREFIX) &&
uri != ANDROID_URI
) {
if (context.project.isGradleProject) {
context.report(
RES_AUTO,
attribute,
context.getValueLocation(attribute),
"In Gradle projects, always use `$AUTO_URI` for custom " +
"attributes"
)
} else {
context.report(
CUSTOM_VIEW,
attribute,
context.getValueLocation(attribute),
"When using a custom namespace attribute in a library " +
"project, use the namespace `\"$AUTO_URI\"` instead"
)
}
}
}
}
}
companion object {
private val IMPLEMENTATION = Implementation(
NamespaceDetector::class.java,
Scope.MANIFEST_AND_RESOURCE_SCOPE,
Scope.RESOURCE_FILE_SCOPE,
Scope.MANIFEST_SCOPE
)
/** Typos in the namespace */
@JvmField
val TYPO = Issue.create(
id = "NamespaceTypo",
briefDescription = "Misspelled namespace declaration",
explanation = """
Accidental misspellings in namespace declarations can lead to some very obscure \
error messages. This check looks for potential misspellings to help track these \
down.""",
category = Category.CORRECTNESS,
priority = 8,
severity = Severity.FATAL,
implementation = IMPLEMENTATION
)
/** Unused namespace declarations */
@JvmField
val UNUSED = Issue.create(
id = "UnusedNamespace",
briefDescription = "Unused namespace",
explanation = """
Unused namespace declarations take up space and require processing that is \
not necessary""",
category = Category.PERFORMANCE,
priority = 1,
severity = Severity.WARNING,
implementation = IMPLEMENTATION
)
/** Unused namespace declarations */
@JvmField
val REDUNDANT = Issue.create(
id = "RedundantNamespace",
briefDescription = "Redundant namespace",
explanation = """
In Android XML documents, only specify the namespace on the root/document \
element. Namespace declarations elsewhere in the document are typically \
accidental leftovers from copy/pasting XML from other files or documentation.""",
category = Category.PERFORMANCE,
priority = 1,
severity = Severity.WARNING,
implementation = IMPLEMENTATION
)
/** Using custom namespace attributes in a library project */
@JvmField
val CUSTOM_VIEW = Issue.create(
id = "LibraryCustomView",
briefDescription = "Custom views in libraries should use res-auto-namespace",
explanation = """
When using a custom view with custom attributes in a library project, the \
layout must use the special namespace $AUTO_URI instead of a URI which includes \
the library project's own package. This will be used to automatically adjust \
the namespace of the attributes when the library resources are merged into \
the application project.""",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.FATAL,
implementation = IMPLEMENTATION
)
/** Unused namespace declarations */
@JvmField
val RES_AUTO = Issue.create(
id = "ResAuto",
briefDescription = "Hardcoded Package in Namespace",
explanation = """
In Gradle projects, the actual package used in the final APK can vary; for \
example,you can add a `.debug` package suffix in one version and not the other. \
Therefore, you should **not** hardcode the application package in the resource; \
instead, use the special namespace `http://schemas.android.com/apk/res-auto` \
which will cause the tools to figure out the right namespace for the resource \
regardless of the actual package used during the build.""",
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.FATAL,
implementation = IMPLEMENTATION
)
private const val XMLNS_A = "xmlns:a"
}
}