| /* |
| * Copyright (C) 2011 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_URI; |
| |
| 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.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.Lint; |
| import com.android.tools.lint.detector.api.Location; |
| 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.google.common.collect.Maps; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| |
| /** Checks for unreachable states in an Android state list definition */ |
| public class StateListDetector extends ResourceXmlDetector { |
| /** The main issue discovered by this detector */ |
| public static final Issue ISSUE = |
| Issue.create( |
| "StateListReachable", |
| "Unreachable state in a `<selector>`", |
| "In a selector, only the last child in the state list should omit a " |
| + "state qualifier. If not, all subsequent items in the list will be ignored " |
| + "since the given item will match all.", |
| Category.CORRECTNESS, |
| 5, |
| Severity.WARNING, |
| new Implementation(StateListDetector.class, Scope.RESOURCE_FILE_SCOPE)); |
| |
| private static final String STATE_PREFIX = "state_"; |
| |
| /** Constructs a new {@link StateListDetector} */ |
| public StateListDetector() {} |
| |
| @Override |
| public boolean appliesTo(@NonNull ResourceFolderType folderType) { |
| return folderType == ResourceFolderType.DRAWABLE; |
| } |
| |
| @Override |
| public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { |
| // TODO: Look for views that don't specify |
| // Display the error token somewhere so it can be suppressed |
| // Emit warning at the end "run with --help to learn how to suppress types of errors/checks"; |
| // ("...and this message.") |
| |
| Element root = document.getDocumentElement(); |
| if (root != null && root.getTagName().equals("selector")) { |
| List<Element> children = Lint.getChildren(root); |
| Map<Element, Set<String>> states = Maps.newHashMapWithExpectedSize(children.size()); |
| |
| for (Element child : children) { |
| NamedNodeMap attributes = child.getAttributes(); |
| Set<String> stateNames = new HashSet<>(attributes.getLength()); |
| states.put(child, stateNames); |
| |
| for (int j = 0; j < attributes.getLength(); j++) { |
| Attr attribute = (Attr) attributes.item(j); |
| String name = attribute.getLocalName(); |
| if (name == null) { |
| continue; |
| } |
| if (name.startsWith(STATE_PREFIX)) { |
| stateNames.add(name + '=' + attribute.getValue()); |
| } else { |
| String namespaceUri = attribute.getNamespaceURI(); |
| if (namespaceUri != null |
| && !namespaceUri.isEmpty() |
| && !ANDROID_URI.equals(namespaceUri)) { |
| // There is a custom attribute on this item. |
| // This could be a state, see |
| // http://code.google.com/p/android/issues/detail?id=22339 |
| // so don't flag this one. |
| stateNames.add(attribute.getName() + '=' + attribute.getValue()); |
| } |
| } |
| } |
| } |
| |
| // See if for each state, any subsequent state fully contains all the same |
| // state requirements |
| |
| for (int i = 0; i < children.size() - 1; i++) { |
| Element prev = children.get(i); |
| Set<String> prevStates = states.get(prev); |
| assert prevStates != null : prev; |
| for (int j = i + 1; j < children.size(); j++) { |
| Element current = children.get(j); |
| Set<String> currentStates = states.get(current); |
| assert currentStates != null : current; |
| if (currentStates.containsAll(prevStates)) { |
| Location location = context.getLocation(current); |
| Location secondary = context.getLocation(prev); |
| secondary.setMessage("Earlier item which masks item"); |
| location.setSecondary(secondary); |
| context.report( |
| ISSUE, |
| current, |
| location, |
| String.format( |
| "This item is unreachable because a previous item (item #%1$d) is a more general match than this one", |
| i + 1)); |
| // Don't keep reporting errors for all the remaining cases in this file |
| return; |
| } |
| } |
| } |
| } |
| } |
| } |