aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2011-12-13 17:00:24 -0800
committerAndroid (Google) Code Review <android-gerrit@google.com>2011-12-13 17:00:24 -0800
commitf82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8 (patch)
treee6acf249a12b2d07b6ce8b767d570f5c79bcd8b5
parentfacc66faa372cdf8b8890faeb66a4f0f3996de94 (diff)
parent88678fba5568a66c259ac51c6fa124455f16016a (diff)
downloadsdk-f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8.zip
sdk-f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8.tar.gz
sdk-f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8.tar.bz2
Merge "New lint rule for invalid @+id references"
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java37
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java12
-rw-r--r--lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintConstants.java5
-rw-r--r--lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java57
-rw-r--r--lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java2
-rw-r--r--lint/libs/lint_checks/src/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java5
-rw-r--r--lint/libs/lint_checks/src/com/android/tools/lint/checks/WrongIdDetector.java335
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WrongIdDetectorTest.java52
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/ids.xml6
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout1.xml45
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout2.xml13
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java14
13 files changed, 536 insertions, 54 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java
index ec6f26a..478593b 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java
@@ -200,43 +200,6 @@ public class AdtUtils {
}
/**
- * Computes the edit distance (number of insertions, deletions or substitutions
- * to edit one string into the other) between two strings. In particular,
- * this will compute the Levenshtein distance.
- * <p>
- * See http://en.wikipedia.org/wiki/Levenshtein_distance for details.
- *
- * @param s the first string to compare
- * @param t the second string to compare
- * @return the edit distance between the two strings
- */
- public static int editDistance(String s, String t) {
- int m = s.length();
- int n = t.length();
- int[][] d = new int[m + 1][n + 1];
- for (int i = 0; i <= m; i++) {
- d[i][0] = i;
- }
- for (int j = 0; j <= n; j++) {
- d[0][j] = j;
- }
- for (int j = 1; j <= n; j++) {
- for (int i = 1; i <= m; i++) {
- if (s.charAt(i - 1) == t.charAt(j - 1)) {
- d[i][j] = d[i - 1][j - 1];
- } else {
- int deletion = d[i - 1][j] + 1;
- int insertion = d[i][j - 1] + 1;
- int substitution = d[i - 1][j - 1] + 1;
- d[i][j] = Math.min(deletion, Math.min(insertion, substitution));
- }
- }
- }
-
- return d[m][n];
- }
-
- /**
* Returns the current editor (the currently visible and active editor), or null if
* not found
*
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
index 13a3fc4..931eac1 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
@@ -39,7 +39,6 @@ import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
-import com.android.ide.eclipse.adt.AdtUtils;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.IPageImageProvider;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
@@ -70,6 +69,7 @@ import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
+import com.android.tools.lint.detector.api.LintUtils;
import com.android.util.Pair;
import org.eclipse.core.resources.IFile;
@@ -1651,19 +1651,20 @@ public class GraphicalEditorPart extends EditorPart
// Look for typos and try to match with custom views and android views
String actualBase = actual.substring(actual.lastIndexOf('.') + 1);
+ int maxDistance = actualBase.length() >= 4 ? 2 : 1;
+
if (views.size() > 0) {
for (String suggested : views) {
String suggestedBase = suggested.substring(suggested.lastIndexOf('.') + 1);
String matchWith = compareWithPackage ? suggested : suggestedBase;
- int maxDistance = actualBase.length() >= 4 ? 2 : 1;
if (Math.abs(actualBase.length() - matchWith.length()) > maxDistance) {
// The string lengths differ more than the allowed edit distance;
// no point in even attempting to compute the edit distance (requires
// O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
continue;
}
- if (AdtUtils.editDistance(actualBase, matchWith) <= maxDistance) {
+ if (LintUtils.editDistance(actualBase, matchWith) <= maxDistance) {
// Suggest this class as a typo for the given class
String labelClass = (suggestedBase.equals(actual) || actual.indexOf('.') != -1)
? suggested : suggestedBase;
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java
index 65b374b..1eb1b4a 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/AdtUtilsTest.java
@@ -92,16 +92,4 @@ public class AdtUtilsTest extends TestCase {
assertSame("Foo", AdtUtils.capitalize("Foo"));
assertNull(null, AdtUtils.capitalize(null));
}
-
- public void testEditDistance() {
- // editing kitten to sitting has edit distance 3:
- // replace k with s
- // replace e with i
- // append g
- assertEquals(3, AdtUtils.editDistance("kitten", "sitting"));
-
- assertEquals(3, AdtUtils.editDistance("saturday", "sunday"));
- assertEquals(1, AdtUtils.editDistance("button", "bitton"));
- assertEquals(6, AdtUtils.editDistance("radiobutton", "bitton"));
- }
}
diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintConstants.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintConstants.java
index b0fbef6..5ed2d78 100644
--- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintConstants.java
+++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintConstants.java
@@ -112,10 +112,12 @@ public class LintConstants {
// Attributes: Resources
public static final String ATTR_NAME = "name"; //$NON-NLS-1$
+ public static final String ATTR_TYPE = "type"; //$NON-NLS-1$
public static final String ATTR_PARENT = "parent"; //$NON-NLS-1$
public static final String ATTR_TRANSLATABLE = "translatable"; //$NON-NLS-1$
// Attributes: Layout
+ public static final String ATTR_LAYOUT_PREFIX = "layout_"; //$NON-NLS-1$
public static final String ATTR_CLASS = "class"; //$NON-NLS-1$
public static final String ATTR_STYLE = "style"; //$NON-NLS-1$
@@ -191,6 +193,9 @@ public class LintConstants {
public static final String VALUE_MATCH_PARENT = "match_parent"; //$NON-NLS-1$
public static final String VALUE_VERTICAL = "vertical"; //$NON-NLS-1$
+ // Values: Resources
+ public static final String VALUE_ID = "id"; //$NON-NLS-1$
+
// Values: Drawables
public static final String VALUE_DISABLED = "disabled"; //$NON-NLS-1$
public static final String VALUE_CLAMP = "clamp"; //$NON-NLS-1$
diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java
index 55d1cf6..186a5f3 100644
--- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java
+++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/LintUtils.java
@@ -17,6 +17,8 @@
package com.android.tools.lint.detector.api;
import static com.android.tools.lint.detector.api.LintConstants.DOT_XML;
+import static com.android.tools.lint.detector.api.LintConstants.ID_RESOURCE_PREFIX;
+import static com.android.tools.lint.detector.api.LintConstants.NEW_ID_RESOURCE_PREFIX;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
@@ -149,4 +151,59 @@ public class LintUtils {
return childCount;
}
+
+ /**
+ * Returns the given id without an {@code @id/} or {@code @+id} prefix
+ *
+ * @param id the id to strip
+ * @return the stripped id, never null
+ */
+ public static String stripIdPrefix(String id) {
+ if (id == null) {
+ return "";
+ } else if (id.startsWith(NEW_ID_RESOURCE_PREFIX)) {
+ return id.substring(NEW_ID_RESOURCE_PREFIX.length());
+ } else if (id.startsWith(ID_RESOURCE_PREFIX)) {
+ return id.substring(ID_RESOURCE_PREFIX.length());
+ }
+
+ return id;
+ }
+
+ /**
+ * Computes the edit distance (number of insertions, deletions or substitutions
+ * to edit one string into the other) between two strings. In particular,
+ * this will compute the Levenshtein distance.
+ * <p>
+ * See http://en.wikipedia.org/wiki/Levenshtein_distance for details.
+ *
+ * @param s the first string to compare
+ * @param t the second string to compare
+ * @return the edit distance between the two strings
+ */
+ public static int editDistance(String s, String t) {
+ int m = s.length();
+ int n = t.length();
+ int[][] d = new int[m + 1][n + 1];
+ for (int i = 0; i <= m; i++) {
+ d[i][0] = i;
+ }
+ for (int j = 0; j <= n; j++) {
+ d[0][j] = j;
+ }
+ for (int j = 1; j <= n; j++) {
+ for (int i = 1; i <= m; i++) {
+ if (s.charAt(i - 1) == t.charAt(j - 1)) {
+ d[i][j] = d[i - 1][j - 1];
+ } else {
+ int deletion = d[i - 1][j] + 1;
+ int insertion = d[i][j - 1] + 1;
+ int substitution = d[i - 1][j - 1] + 1;
+ d[i][j] = Math.min(deletion, Math.min(insertion, substitution));
+ }
+ }
+ }
+
+ return d[m][n];
+ }
}
diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java
index b1644d9..cc2a799 100644
--- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java
+++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java
@@ -58,6 +58,8 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(FieldGetterDetector.ISSUE);
issues.add(DuplicateIdDetector.CROSS_LAYOUT);
issues.add(DuplicateIdDetector.WITHIN_LAYOUT);
+ issues.add(WrongIdDetector.UNKNOWN_ID);
+ issues.add(WrongIdDetector.UNKNOWN_ID_LAYOUT);
issues.add(StateListDetector.ISSUE);
issues.add(InefficientWeightDetector.INEFFICIENT_WEIGHT);
issues.add(InefficientWeightDetector.NESTED_WEIGHTS);
diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java
index 487f6b1..685e893 100644
--- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java
+++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java
@@ -43,6 +43,7 @@ import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_MARG
import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_MARGIN_RIGHT;
import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_MARGIN_TOP;
+import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_PREFIX;
import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_ROW;
import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_SPAN;
@@ -233,8 +234,8 @@ public class ObsoleteLayoutParamsDetector extends LayoutDetector {
@Override
public void visitAttribute(XmlContext context, Attr attribute) {
String name = attribute.getLocalName();
- if (name != null && name.startsWith("layout_") && //$NON-NLS-1$
- ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ if (name != null && name.startsWith(ATTR_LAYOUT_PREFIX)
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
if (VALID.contains(name)) {
return;
}
diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/WrongIdDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/WrongIdDetector.java
new file mode 100644
index 0000000..15b3e5e
--- /dev/null
+++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/WrongIdDetector.java
@@ -0,0 +1,335 @@
+/*
+ * 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.tools.lint.detector.api.LintConstants.ANDROID_URI;
+import static com.android.tools.lint.detector.api.LintConstants.ATTR_ID;
+import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_PREFIX;
+import static com.android.tools.lint.detector.api.LintConstants.ATTR_NAME;
+import static com.android.tools.lint.detector.api.LintConstants.ATTR_TYPE;
+import static com.android.tools.lint.detector.api.LintConstants.ID_RESOURCE_PREFIX;
+import static com.android.tools.lint.detector.api.LintConstants.NEW_ID_RESOURCE_PREFIX;
+import static com.android.tools.lint.detector.api.LintConstants.RELATIVE_LAYOUT;
+import static com.android.tools.lint.detector.api.LintConstants.TAG_ITEM;
+import static com.android.tools.lint.detector.api.LintConstants.VALUE_ID;
+import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix;
+
+import com.android.resources.ResourceFolderType;
+import com.android.tools.lint.client.api.IDomParser;
+import com.android.tools.lint.detector.api.Category;
+import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.Issue;
+import com.android.tools.lint.detector.api.LayoutDetector;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.tools.lint.detector.api.Location;
+import com.android.tools.lint.detector.api.Location.Handle;
+import com.android.tools.lint.detector.api.Scope;
+import com.android.tools.lint.detector.api.Severity;
+import com.android.tools.lint.detector.api.Speed;
+import com.android.tools.lint.detector.api.XmlContext;
+import com.android.util.Pair;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Checks for duplicate ids within a layout and within an included layout
+ */
+public class WrongIdDetector extends LayoutDetector {
+
+ /** Ids bound to widgets in any of the layout files */
+ private Set<String> mGlobalIds = new HashSet<String>(100);
+
+ /** Ids bound to widgets in the current layout file */
+ private Set<String> mFileIds;
+
+ /** Ids declared in a value's file, e.g. {@code <item type="id" name="foo"/>} */
+ private Set<String> mDeclaredIds;
+
+ /**
+ * Location handles for the various id references that were not found as
+ * defined in the same layout, to be checked after the whole project has
+ * been scanned
+ */
+ private List<Pair<String, Location.Handle>> mHandles;
+
+ /** List of RelativeLayout elements in the current layout */
+ private List<Element> mRelativeLayouts;
+
+ /** Reference to an unknown id */
+ public static final Issue UNKNOWN_ID = Issue.create(
+ "UnknownId", //$NON-NLS-1$
+ "Checks for id references in RelativeLayouts that are not defined elsewhere",
+ "The \"@+id/\" syntax refers to an existing id, or creates a new one if it has " +
+ "not already been defined elsewhere. However, this means that if you have a " +
+ "typo in your reference, or if the referred view no longer exists, you do not " +
+ "get a warning since the id will be created on demand. This check catches " +
+ "errors where you have renamed an id without updating all of the references to " +
+ "it.",
+ Category.CORRECTNESS,
+ 8,
+ Severity.ERROR,
+ WrongIdDetector.class,
+ Scope.ALL_RESOURCES_SCOPE);
+
+ /** Reference to an id that is not in the current layout */
+ public static final Issue UNKNOWN_ID_LAYOUT = Issue.create(
+ "UnknownIdInLayout", //$NON-NLS-1$
+ "Makes sure that @+id references refer to views in the same layout",
+
+ "The \"@+id/\" syntax refers to an existing id, or creates a new one if it has " +
+ "not already been defined elsewhere. However, this means that if you have a " +
+ "typo in your reference, or if the referred view no longer exists, you do not " +
+ "get a warning since the id will be created on demand.\n" +
+ "\n" +
+ "This is sometimes intentional, for example where you are referring to a view " +
+ "which is provided in a different layout via an include. However, it is usually " +
+ "an accident where you have a typo or you have renamed a view without updating " +
+ "all the references to it.",
+
+ Category.CORRECTNESS,
+ 5,
+ Severity.WARNING,
+ WrongIdDetector.class,
+ Scope.RESOURCE_FILE_SCOPE);
+
+ /** Constructs a duplicate id check */
+ public WrongIdDetector() {
+ };
+
+ @Override
+ public boolean appliesTo(ResourceFolderType folderType) {
+ return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
+ }
+
+ @Override
+ public Speed getSpeed() {
+ return Speed.FAST;
+ }
+
+ @Override
+ public Collection<String> getApplicableAttributes() {
+ return Collections.singletonList(ATTR_ID);
+ }
+
+ @Override
+ public Collection<String> getApplicableElements() {
+ return Arrays.asList(RELATIVE_LAYOUT, TAG_ITEM);
+ }
+
+ @Override
+ public void beforeCheckFile(Context context) {
+ mFileIds = new HashSet<String>();
+ mRelativeLayouts = null;
+ }
+
+ @Override
+ public void afterCheckFile(Context context) {
+ if (mRelativeLayouts != null) {
+ for (Element layout : mRelativeLayouts) {
+ NodeList children = layout.getChildNodes();
+ for (int j = 0, childCount = children.getLength(); j < childCount; j++) {
+ Node child = children.item(j);
+ if (child.getNodeType() != Node.ELEMENT_NODE) {
+ continue;
+ }
+ Element element = (Element) child;
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Attr attr = (Attr) attributes.item(i);
+ String value = attr.getValue();
+ if ((value.startsWith(NEW_ID_RESOURCE_PREFIX) ||
+ value.startsWith(ID_RESOURCE_PREFIX))
+ && ANDROID_URI.equals(attr.getNamespaceURI())
+ && attr.getLocalName().startsWith(ATTR_LAYOUT_PREFIX)) {
+ if (!idDefined(mFileIds, value)) {
+ // Stash a reference to this id and location such that
+ // we can check after the *whole* layout has been processed,
+ // since it's too early to conclude here that the id does
+ // not exist (you are allowed to have forward references)
+ XmlContext xmlContext = (XmlContext) context;
+ IDomParser parser = xmlContext.parser;
+ Handle handle = parser.createLocationHandle(xmlContext, attr);
+
+ if (mHandles == null) {
+ mHandles = new ArrayList<Pair<String,Handle>>();
+ }
+ mHandles.add(Pair.of(value, handle));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ mFileIds = null;
+ }
+
+ @Override
+ public void afterCheckProject(Context context) {
+ if (mHandles != null) {
+ boolean checkSameLayout = context.isEnabled(UNKNOWN_ID_LAYOUT);
+ boolean checkExists = context.isEnabled(UNKNOWN_ID);
+ boolean projectScope = context.getScope().contains(Scope.ALL_RESOURCE_FILES);
+ for (Pair<String, Handle> pair : mHandles) {
+ String id = pair.getFirst();
+ boolean isBound = idDefined(mGlobalIds, id);
+ if (!isBound && checkExists && projectScope) {
+ Handle handle = pair.getSecond();
+ Location location = handle.resolve();
+ boolean isDeclared = idDefined(mDeclaredIds, id);
+ id = stripIdPrefix(id);
+ String suggestionMessage;
+ List<String> suggestions = getSpellingSuggestions(id, mGlobalIds);
+ if (suggestions.size() > 1) {
+ suggestionMessage = String.format(" Did you mean one of {%2$s} ?",
+ id, Joiner.on(", ").join(suggestions));
+ } else if (suggestions.size() > 0) {
+ suggestionMessage = String.format(" Did you mean %2$s ?",
+ id, suggestions.get(0));
+ } else {
+ suggestionMessage = "";
+ }
+ String message;
+ if (isDeclared) {
+ message = String.format(
+ "The id \"%1$s\" is defined but not assigned to any views.%2$s",
+ id, suggestionMessage);
+ } else {
+ message = String.format(
+ "The id \"%1$s\" is not defined anywhere.%2$s",
+ id, suggestionMessage);
+ }
+ context.report(UNKNOWN_ID, location, message, null);
+ } else if (checkSameLayout && (!projectScope || isBound)) {
+ // The id was defined, but in a different layout. Usually not intentional
+ // (might be referring to a random other view that happens to have the same
+ // name.)
+ Handle handle = pair.getSecond();
+ Location location = handle.resolve();
+ context.report(UNKNOWN_ID_LAYOUT, location,
+ String.format(
+ "The id \"%1$s\" is not referring to any views in this layout",
+ stripIdPrefix(id)),
+ null);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void visitElement(XmlContext context, Element element) {
+ if (element.getTagName().equals(RELATIVE_LAYOUT)) {
+ if (mRelativeLayouts == null) {
+ mRelativeLayouts = new ArrayList<Element>();
+ }
+ mRelativeLayouts.add(element);
+ } else {
+ assert element.getTagName().equals(TAG_ITEM);
+ String type = element.getAttribute(ATTR_TYPE);
+ if (VALUE_ID.equals(type)) {
+ String name = element.getAttribute(ATTR_NAME);
+ if (name.length() > 0) {
+ if (mDeclaredIds == null) {
+ mDeclaredIds = Sets.newHashSet();
+ }
+ mDeclaredIds.add(ID_RESOURCE_PREFIX + name);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void visitAttribute(XmlContext context, Attr attribute) {
+ assert attribute.getLocalName().equals(ATTR_ID);
+ String id = attribute.getValue();
+ mFileIds.add(id);
+ mGlobalIds.add(id);
+ }
+
+ private static boolean idDefined(Set<String> ids, String id) {
+ if (ids == null) {
+ return false;
+ }
+ boolean definedLocally = ids.contains(id);
+ if (!definedLocally) {
+ if (id.startsWith(NEW_ID_RESOURCE_PREFIX)) {
+ definedLocally = ids.contains(ID_RESOURCE_PREFIX +
+ id.substring(NEW_ID_RESOURCE_PREFIX.length()));
+ } else if (id.startsWith(ID_RESOURCE_PREFIX)) {
+ definedLocally = ids.contains(NEW_ID_RESOURCE_PREFIX +
+ id.substring(ID_RESOURCE_PREFIX.length()));
+ }
+ }
+
+ return definedLocally;
+ }
+
+ private List<String> getSpellingSuggestions(String id, Collection<String> ids) {
+ int maxDistance = id.length() >= 4 ? 2 : 1;
+
+ // Look for typos and try to match with custom views and android views
+ Multimap<Integer, String> matches = ArrayListMultimap.create(2, 10);
+ int count = 0;
+ if (ids.size() > 0) {
+ for (String matchWith : ids) {
+ matchWith = stripIdPrefix(matchWith);
+ if (Math.abs(id.length() - matchWith.length()) > maxDistance) {
+ // The string lengths differ more than the allowed edit distance;
+ // no point in even attempting to compute the edit distance (requires
+ // O(n*m) storage and O(n*m) speed, where n and m are the string lengths)
+ continue;
+ }
+ int distance = LintUtils.editDistance(id, matchWith);
+ if (distance <= maxDistance) {
+ matches.put(distance, matchWith);
+ }
+
+ if (count++ > 100) {
+ // Make sure that for huge projects we don't completely grind to a halt
+ break;
+ }
+ }
+ }
+
+ for (int i = 0; i < maxDistance; i++) {
+ Collection<String> s = matches.get(i);
+ if (s != null && s.size() > 0) {
+ List<String> suggestions = new ArrayList<String>(s);
+ Collections.sort(suggestions);
+ return suggestions;
+ }
+ }
+
+ return Collections.emptyList();
+ }
+}
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WrongIdDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WrongIdDetectorTest.java
new file mode 100644
index 0000000..1b14c9b
--- /dev/null
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/WrongIdDetectorTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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 com.android.tools.lint.detector.api.Detector;
+
+@SuppressWarnings("javadoc")
+public class WrongIdDetectorTest extends AbstractCheckTest {
+ @Override
+ protected Detector getDetector() {
+ return new WrongIdDetector();
+ }
+
+ public void test() throws Exception {
+ assertEquals(
+ "layout1.xml:14: Error: The id \"button5\" is not defined anywhere. Did you mean one of {button1, button2, button3, button4} ?\n" +
+ "layout1.xml:15: Warning: The id \"my_id2\" is not referring to any views in this layout\n" +
+ "layout1.xml:17: Error: The id \"my_id3\" is not defined anywhere. Did you mean my_id2 ?\n" +
+ "layout1.xml:18: Error: The id \"my_id1\" is defined but not assigned to any views. Did you mean my_id2 ?",
+
+ lintProject(
+ "wrongid/layout1.xml=>res/layout/layout1.xml",
+ "wrongid/layout2.xml=>res/layout/layout2.xml",
+ "wrongid/ids.xml=>res/values/ids.xml"
+ ));
+ }
+
+ public void testSingleFile() throws Exception {
+ assertEquals(
+ "layout1.xml:14: Warning: The id \"button5\" is not referring to any views in this layout\n" +
+ "layout1.xml:15: Warning: The id \"my_id2\" is not referring to any views in this layout\n" +
+ "layout1.xml:17: Warning: The id \"my_id3\" is not referring to any views in this layout\n" +
+ "layout1.xml:18: Warning: The id \"my_id1\" is not referring to any views in this layout",
+
+ lintFiles("wrongid/layout1.xml=>res/layout/layout1.xml"));
+ }
+
+}
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/ids.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/ids.xml
new file mode 100644
index 0000000..07e8ae9
--- /dev/null
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/ids.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <item name="my_id1" type="id"/>
+
+</resources>
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout1.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout1.xml
new file mode 100644
index 0000000..073dddd
--- /dev/null
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout1.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/RelativeLayout1"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <!-- my_id1 is defined in ids.xml, my_id2 is defined in main2, my_id3 does not exist -->
+
+ <Button
+ android:id="@+id/button1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/button5"
+ android:layout_alignLeft="@+id/my_id2"
+ android:layout_alignParentTop="true"
+ android:layout_alignRight="@+id/my_id3"
+ android:layout_alignTop="@+id/my_id1"
+ android:text="Button" />
+
+ <Button
+ android:id="@+id/button2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@+id/button1"
+ android:text="Button" />
+
+ <Button
+ android:id="@+id/button3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@+id/button2"
+ android:text="Button" />
+
+ <Button
+ android:id="@+id/button4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@+id/button3"
+ android:text="Button" />
+
+</RelativeLayout>
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout2.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout2.xml
new file mode 100644
index 0000000..54dd91a
--- /dev/null
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/wrongid/layout2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <Button
+ android:id="@+id/my_id2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Button" />
+
+</LinearLayout>
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java
index e4c4372..ba3df3b 100644
--- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/LintUtilsTest.java
@@ -45,4 +45,18 @@ public class LintUtilsTest extends TestCase {
assertFalse(LintUtils.isXmlFile(new File("xml")));
assertFalse(LintUtils.isXmlFile(new File("xml.png")));
}
+
+ public void testEditDistance() {
+ assertEquals(0, LintUtils.editDistance("kitten", "kitten"));
+
+ // editing kitten to sitting has edit distance 3:
+ // replace k with s
+ // replace e with i
+ // append g
+ assertEquals(3, LintUtils.editDistance("kitten", "sitting"));
+
+ assertEquals(3, LintUtils.editDistance("saturday", "sunday"));
+ assertEquals(1, LintUtils.editDistance("button", "bitton"));
+ assertEquals(6, LintUtils.editDistance("radiobutton", "bitton"));
+ }
} \ No newline at end of file