diff options
author | Tor Norbye <tnorbye@google.com> | 2011-12-13 17:00:24 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-12-13 17:00:24 -0800 |
commit | f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8 (patch) | |
tree | e6acf249a12b2d07b6ce8b767d570f5c79bcd8b5 | |
parent | facc66faa372cdf8b8890faeb66a4f0f3996de94 (diff) | |
parent | 88678fba5568a66c259ac51c6fa124455f16016a (diff) | |
download | sdk-f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8.zip sdk-f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8.tar.gz sdk-f82407d2a96b5cb86d1ca4ea52bc9a432c0a8fe8.tar.bz2 |
Merge "New lint rule for invalid @+id references"
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 |