| /* |
| * 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 |
| } |
| } |
| } |
| } |