blob: 8e326e6f5c6d44866f6e43e2845bf202b7f74524 [file] [log] [blame]
/*
* Copyright (C) 2018 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.ATTR_GRAPH
import com.android.SdkConstants.ATTR_ID
import com.android.SdkConstants.ATTR_START_DESTINATION
import com.android.SdkConstants.AUTO_URI
import com.android.SdkConstants.TAG_INCLUDE
import com.android.SdkConstants.TAG_NAVIGATION
import com.android.ide.common.rendering.api.ResourceNamespace
import com.android.resources.ResourceFolderType
import com.android.resources.ResourceType
import com.android.resources.ResourceUrl
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.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.stripIdPrefix
import org.w3c.dom.Element
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
/**
* Check to make sure the startDestination attribute on navigation elements is set and valid.
*/
class StartDestinationDetector : ResourceXmlDetector() {
companion object Issues {
@JvmField
val ISSUE = Issue.create(
id = "InvalidNavigation",
briefDescription = "No start destination specified",
explanation = """
All `<navigation>` elements must have a start destination specified, and it must \
be a direct child of that `<navigation>`.
""",
category = Category.CORRECTNESS,
priority = 3,
severity = Severity.WARNING,
implementation = Implementation(
StartDestinationDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
)
)
}
override fun appliesTo(folderType: ResourceFolderType): Boolean =
folderType == ResourceFolderType.NAVIGATION
override fun getApplicableElements() = listOf(TAG_NAVIGATION)
override fun visitElement(context: XmlContext, element: Element) {
val children = element.childNodes
// If there are no children, don't show the warning yet.
if ((0 until children.length).none { children.item(it) is Element }) return
val destinationAttr = element.getAttributeNodeNS(AUTO_URI, ATTR_START_DESTINATION)
val destinationAttrValue = destinationAttr?.value
// smart cast to non-null doesn't seem to work with isNullOrBlank
if (destinationAttrValue == null || destinationAttrValue.isBlank()) {
context.report(
ISSUE,
element,
context.getNameLocation(element),
"No start destination specified"
)
} else {
// TODO(namespaces): Support namespaces in ids
val url = ResourceUrl.parse(destinationAttrValue)
if (url == null || url.type != ResourceType.ID) {
context.report(
ISSUE,
element,
context.getNameLocation(element),
"`startDestination` must be an id"
)
return
}
for (i in 0 until children.length) {
val child = children.item(i) as? Element ?: continue
if (child.tagName == TAG_INCLUDE) {
val includedGraph = child.getAttributeNS(AUTO_URI, ATTR_GRAPH)
val includedUrl = ResourceUrl.parse(includedGraph) ?: continue
val repository =
context.client.getResourceRepository(context.project, true, true)
?: continue
val items = repository.getResources(
ResourceNamespace.TODO(),
includedUrl.type,
includedUrl.name
)
for (item in items) {
val source = item.source ?: continue
try {
val parser = context.client.createXmlPullParser(source)
if (parser != null && checkId(parser, url.name)) {
return
}
} catch (ignore: XmlPullParserException) {
// Users might be editing these files in the IDE; don't flag
} catch (ignore: IOException) {
// Users might be editing these files in the IDE; don't flag
}
}
} else {
val childId = child.getAttributeNS(ANDROID_URI, ATTR_ID)
val childUrl = ResourceUrl.parse(childId) ?: continue
if (url.name == childUrl.name) {
return
}
}
}
context.report(
ISSUE,
element,
context.getValueLocation(destinationAttr),
"Invalid start destination $destinationAttrValue"
)
}
}
private fun checkId(parser: XmlPullParser, target: String): Boolean {
while (true) {
when (parser.next()) {
XmlPullParser.START_TAG ->
return stripIdPrefix(parser.getAttributeValue(ANDROID_URI, ATTR_ID)) == target
XmlPullParser.END_TAG, XmlPullParser.END_DOCUMENT -> return false
}
}
}
}