| /* |
| * 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 static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ATTR_LAYOUT; |
| import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; |
| import static com.android.SdkConstants.ATTR_NAME; |
| import static com.android.SdkConstants.ATTR_TYPE; |
| import static com.android.SdkConstants.AUTO_URI; |
| import static com.android.SdkConstants.CLASS_CONSTRAINT_LAYOUT; |
| import static com.android.SdkConstants.CONSTRAINT_REFERENCED_IDS; |
| import static com.android.SdkConstants.FD_RES_VALUES; |
| import static com.android.SdkConstants.ID_PREFIX; |
| import static com.android.SdkConstants.NEW_ID_PREFIX; |
| import static com.android.SdkConstants.RELATIVE_LAYOUT; |
| import static com.android.SdkConstants.TAG_ITEM; |
| import static com.android.SdkConstants.VALUE_ID; |
| import static com.android.SdkConstants.VIEW_INCLUDE; |
| import static com.android.SdkConstants.VIEW_MERGE; |
| import static com.android.tools.lint.checks.RequiredAttributeDetector.PERCENT_RELATIVE_LAYOUT; |
| import static com.android.tools.lint.detector.api.Lint.editDistance; |
| import static com.android.tools.lint.detector.api.Lint.isSameResourceFile; |
| import static com.android.tools.lint.detector.api.Lint.stripIdPrefix; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.rendering.api.ResourceNamespace; |
| import com.android.ide.common.resources.ResourceItem; |
| import com.android.ide.common.resources.ResourceRepository; |
| import com.android.ide.common.util.PathString; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.tools.lint.client.api.LintClient; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.LayoutDetector; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.Location.Handle; |
| import com.android.tools.lint.detector.api.Project; |
| 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.utils.XmlUtils; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.Sets; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| /** Checks for duplicate ids within a layout and within an included layout */ |
| public class WrongIdDetector extends LayoutDetector { |
| private static final Implementation IMPLEMENTATION = |
| new Implementation(WrongIdDetector.class, Scope.RESOURCE_FILE_SCOPE); |
| |
| /** Ids bound to widgets in any of the layout files */ |
| private final Set<String> mGlobalIds = new HashSet<>(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 Map<String, Handle> mHandles; |
| |
| /** |
| * Issues to be reported for id references unless we determine that the id was undefined after |
| * checking the whole project |
| */ |
| private Map<String, String> mPendingNotSibling; |
| |
| /** |
| * List of RelativeLayout elements in the current layout (and percent layouts, and constraint |
| * layouts, etc -- any elements that have constraints among their children with id references |
| */ |
| private List<Element> mRelativeLayouts; |
| |
| /** Reference to an unknown id */ |
| @SuppressWarnings("unchecked") |
| public static final Issue UNKNOWN_ID = |
| Issue.create( |
| "UnknownId", |
| "Reference to an unknown id", |
| "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.FATAL, |
| new Implementation( |
| WrongIdDetector.class, |
| Scope.ALL_RESOURCES_SCOPE, |
| Scope.RESOURCE_FILE_SCOPE)); |
| |
| /** Reference to an id that is not a sibling */ |
| public static final Issue NOT_SIBLING = |
| Issue.create( |
| "NotSibling", |
| "Invalid Constraints", |
| "Layout constraints in a given `ConstraintLayout` or `RelativeLayout` should " |
| + "reference other views within the same relative layout (but not itself!)", |
| Category.CORRECTNESS, |
| 6, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| /** An ID declaration which is not valid */ |
| public static final Issue INVALID = |
| Issue.create( |
| "InvalidId", |
| "Invalid ID declaration", |
| "An id definition **must** be of the form `@+id/yourname`. The tools have not " |
| + "rejected strings of the form `@+foo/bar` in the past, but that was an error, " |
| + "and could lead to tricky errors because of the way the id integers are assigned.\n" |
| + "\n" |
| + "If you really want to have different \"scopes\" for your id's, use prefixes " |
| + "instead, such as `login_button1` and `login_button2`.", |
| Category.CORRECTNESS, |
| 6, |
| Severity.FATAL, |
| IMPLEMENTATION); |
| |
| /** Reference to an id that is not in the current layout */ |
| public static final Issue UNKNOWN_ID_LAYOUT = |
| Issue.create( |
| "UnknownIdInLayout", |
| "Reference to an id that is not in the current 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, |
| new Implementation(WrongIdDetector.class, Scope.RESOURCE_FILE_SCOPE)); |
| |
| /** Constructs a duplicate id check */ |
| public WrongIdDetector() {} |
| |
| @Override |
| public boolean appliesTo(@NonNull ResourceFolderType folderType) { |
| return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES; |
| } |
| |
| @Override |
| public Collection<String> getApplicableAttributes() { |
| return Collections.singletonList(ATTR_ID); |
| } |
| |
| @Override |
| public Collection<String> getApplicableElements() { |
| return ImmutableSet.of( |
| RELATIVE_LAYOUT, |
| TAG_ITEM, |
| PERCENT_RELATIVE_LAYOUT, |
| SdkConstants.CLASS_CONSTRAINT_LAYOUT.oldName(), |
| SdkConstants.CLASS_CONSTRAINT_LAYOUT.newName()); |
| } |
| |
| @Override |
| public void beforeCheckFile(@NonNull Context context) { |
| mFileIds = new HashSet<>(); |
| mRelativeLayouts = null; |
| } |
| |
| @Override |
| public void afterCheckFile(@NonNull Context context) { |
| if (mRelativeLayouts != null) { |
| if (!context.getProject().getReportIssues()) { |
| // If this is a library project not being analyzed, ignore it |
| return; |
| } |
| |
| for (Element layout : mRelativeLayouts) { |
| checkLayout(context, layout); |
| } |
| } |
| |
| mFileIds = null; |
| |
| if (!context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { |
| checkHandles(context); |
| } |
| } |
| |
| private void checkLayout(@NonNull Context context, Element layout) { |
| Set<String> ids = Sets.newHashSetWithExpectedSize(20); |
| for (Element child : XmlUtils.getSubTags(layout)) { |
| String id = child.getAttributeNS(ANDROID_URI, ATTR_ID); |
| if (id != null && !id.isEmpty()) { |
| ids.add(id); |
| } else if (VIEW_INCLUDE.equals(child.getTagName())) { |
| String included = child.getAttribute(ATTR_LAYOUT); |
| if (!addIncludedIds(context, ids, included)) { |
| // There's an include tag and something went wrong looking |
| // up the corresponding layout: don't attempt to enforce |
| // sibling lookup here. |
| mHandles = null; |
| return; |
| } |
| } |
| } |
| |
| boolean isConstraintLayout = |
| SdkConstants.CLASS_CONSTRAINT_LAYOUT.isEquals(layout.getTagName()); |
| |
| for (Element element : XmlUtils.getSubTags(layout)) { |
| String selfId = stripIdPrefix(element.getAttributeNS(ANDROID_URI, ATTR_ID)); |
| |
| 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_PREFIX) || value.startsWith(ID_PREFIX)) { |
| String localName = attr.getLocalName(); |
| if (localName != null |
| && localName.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) |
| && (ANDROID_URI.equals(attr.getNamespaceURI()) |
| || AUTO_URI.equals(attr.getNamespaceURI()))) { |
| checkIdReference( |
| context, layout, ids, isConstraintLayout, selfId, attr, value); |
| } |
| } else if (isConstraintLayout |
| && CONSTRAINT_REFERENCED_IDS.equals(attr.getLocalName())) { |
| Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings(); |
| for (String id : splitter.split(value)) { |
| checkIdReference(context, layout, ids, true, selfId, attr, id); |
| } |
| } |
| } |
| } |
| } |
| |
| private static boolean addIncludedIds( |
| @NonNull Context context, @NonNull Set<String> ids, @NonNull String included) { |
| if (included.isEmpty()) { |
| return true; |
| } |
| |
| LintClient client = context.getClient(); |
| if (!client.supportsProjectResources()) { |
| return false; |
| } |
| |
| Project project = context.getMainProject(); |
| ResourceRepository resources = client.getResourceRepository(project, true, false); |
| if (resources == null) { |
| return false; |
| } |
| |
| List<ResourceItem> layouts = |
| resources.getResources(ResourceNamespace.TODO(), ResourceType.LAYOUT, included); |
| if (layouts.isEmpty()) { |
| return false; |
| } |
| ResourceItem layout = layouts.get(0); |
| PathString source = layout.getSource(); |
| if (source == null) { |
| return false; |
| } |
| try { |
| XmlPullParser parser = client.createXmlPullParser(source); |
| if (parser != null) { |
| addIncludedIds(parser, ids); |
| } |
| } catch (XmlPullParserException | IOException e) { |
| // Users might be editing these files in the IDE; don't flag |
| } |
| return true; |
| } |
| |
| private static void addIncludedIds(@NonNull XmlPullParser parser, @NonNull Set<String> ids) |
| throws XmlPullParserException, IOException { |
| int depth = -1; |
| while (true) { |
| int event = parser.next(); |
| if (event == XmlPullParser.START_TAG) { |
| depth++; |
| if (depth == 0 && !VIEW_MERGE.equals(parser.getName())) { |
| // Not a <merge>: just add the id on the root attribute; that will |
| // be the element inserted at the <include> side |
| String id = parser.getAttributeValue(ANDROID_URI, ATTR_ID); |
| if (!id.isEmpty()) { |
| ids.add(id); |
| } |
| return; |
| } // else: <merge>: add all children at depth 1 |
| if (depth == 1) { |
| String id = parser.getAttributeValue(ANDROID_URI, ATTR_ID); |
| if (!id.isEmpty()) { |
| ids.add(id); |
| } |
| } |
| } else if (event == XmlPullParser.END_TAG) { |
| depth--; |
| } else if (event == XmlPullParser.END_DOCUMENT) { |
| return; |
| } |
| } |
| } |
| |
| private void checkIdReference( |
| @NonNull Context context, |
| Element layout, |
| Set<String> siblingIds, |
| boolean isConstraintLayout, |
| String selfId, |
| Attr attr, |
| String id) { |
| if (!idDefined(mFileIds, id)) { |
| // 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; |
| Handle handle = xmlContext.createLocationHandle(attr); |
| handle.setClientData(attr); |
| |
| if (mHandles == null) { |
| mHandles = new LinkedHashMap<>(); |
| mPendingNotSibling = new HashMap<>(); |
| } |
| mHandles.put(id, handle); |
| } |
| // Check siblings. TODO: Look for cycles! |
| if (siblingIds.contains(id)) { |
| // Make sure it's not pointing to self |
| if (!ATTR_ID.equals(attr.getLocalName()) |
| && !selfId.isEmpty() |
| && id.endsWith(selfId) |
| && stripIdPrefix(id).equals(selfId)) { |
| String message = |
| String.format( |
| "Cannot be relative to self: id=%1$s, %2$s=%3$s", |
| selfId, attr.getLocalName(), selfId); |
| reportNotSiblingIfKnownId(context, id, attr, message); |
| } |
| return; |
| } |
| if (id.startsWith(NEW_ID_PREFIX)) { |
| if (siblingIds.contains(ID_PREFIX + stripIdPrefix(id))) { |
| return; |
| } |
| } else if (id.startsWith(ID_PREFIX)) { |
| if (siblingIds.contains(NEW_ID_PREFIX + stripIdPrefix(id))) { |
| return; |
| } |
| } else if (siblingIds.contains(NEW_ID_PREFIX + id) || siblingIds.contains(ID_PREFIX + id)) { |
| return; |
| } |
| if (isConstraintLayout) { |
| // A reference to the ConstraintLayout from a child is valid |
| String parentId = stripIdPrefix(layout.getAttributeNS(ANDROID_URI, ATTR_ID)); |
| if (parentId.equals(stripIdPrefix(id))) { |
| return; |
| } |
| } |
| if (context.isEnabled(NOT_SIBLING)) { |
| String message = |
| String.format( |
| "`%1$s` is not a sibling in the same `%2$s`", |
| id, isConstraintLayout ? "ConstraintLayout" : "RelativeLayout"); |
| reportNotSiblingIfKnownId(context, id, attr, message); |
| } |
| } |
| |
| @Override |
| public void afterCheckRootProject(@NonNull Context context) { |
| if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { |
| checkHandles(context); |
| } |
| } |
| |
| private void reportNotSiblingIfKnownId( |
| @NonNull Context context, |
| @NonNull String id, |
| @NonNull Node attr, |
| @NonNull String message) { |
| if (mHandles != null && mHandles.containsKey(id)) { |
| // Potentially unknown ID, store the issue for later |
| mPendingNotSibling.put(id, message); |
| } else { |
| // ID is known, just report the problem |
| XmlContext xmlContext = (XmlContext) context; |
| Location location = xmlContext.getLocation(attr); |
| xmlContext.report(NOT_SIBLING, attr, location, message); |
| } |
| } |
| |
| private void checkHandles(@NonNull 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 (Map.Entry<String, Handle> pair : mHandles.entrySet()) { |
| String id = pair.getKey(); |
| Handle handle = pair.getValue(); |
| boolean isBound = |
| projectScope |
| ? idDefined(mGlobalIds, id) |
| : idDefined(context, id, context.file); |
| LintClient client = context.getClient(); |
| if (!isBound |
| && checkExists |
| && (projectScope || client.supportsProjectResources())) { |
| boolean isDeclared = idDefined(mDeclaredIds, id); |
| id = stripIdPrefix(id); |
| String suggestionMessage; |
| Set<String> spellingDictionary = createSpellingDictionary(); |
| if (!projectScope && client.supportsProjectResources()) { |
| ResourceRepository resources = |
| client.getResourceRepository(context.getProject(), true, false); |
| if (resources != null) { |
| spellingDictionary = |
| Sets.newHashSet( |
| resources |
| .getResources( |
| ResourceNamespace.TODO(), |
| ResourceType.ID) |
| .keySet()); |
| spellingDictionary.remove(id); |
| } |
| } |
| List<String> suggestions = getSpellingSuggestions(id, spellingDictionary); |
| if (suggestions.size() > 1) { |
| suggestionMessage = |
| String.format( |
| " Did you mean one of {%2$s} ?", |
| id, Joiner.on(", ").join(suggestions)); |
| } else if (!suggestions.isEmpty()) { |
| 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); |
| } |
| report(context, UNKNOWN_ID, handle, message); |
| } else if (checkSameLayout |
| && (!projectScope || isBound) |
| && id.startsWith(NEW_ID_PREFIX)) { |
| // 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.) |
| report( |
| context, |
| UNKNOWN_ID_LAYOUT, |
| handle, |
| String.format( |
| "The id \"`%1$s`\" is not referring to any views in this layout", |
| stripIdPrefix(id))); |
| } else if (mPendingNotSibling.containsKey(id)) { |
| String message = mPendingNotSibling.get(id); |
| context.report(NOT_SIBLING, handle.resolve(), message); |
| } |
| } |
| } |
| } |
| |
| private Set<String> createSpellingDictionary() { |
| Set<String> dictionary = new HashSet<>(); |
| mGlobalIds |
| .stream() |
| .filter(id -> id.startsWith(NEW_ID_PREFIX)) |
| .forEach(id -> dictionary.add(stripIdPrefix(id))); |
| if (mDeclaredIds != null) { |
| mDeclaredIds.forEach(id -> dictionary.add(stripIdPrefix(id))); |
| } |
| return dictionary; |
| } |
| |
| private static void report(Context context, Issue issue, Handle handle, String message) { |
| Location location = handle.resolve(); |
| Object clientData = handle.getClientData(); |
| if (clientData instanceof Node) { |
| if (context.getDriver().isSuppressed(null, issue, (Node) clientData)) { |
| return; |
| } |
| } |
| |
| context.report(issue, location, message); |
| } |
| |
| @Override |
| public void visitElement(@NonNull XmlContext context, @NonNull Element element) { |
| String tagName = element.getTagName(); |
| if (tagName.equals(TAG_ITEM)) { |
| String type = element.getAttribute(ATTR_TYPE); |
| if (VALUE_ID.equals(type)) { |
| String name = element.getAttribute(ATTR_NAME); |
| if (!name.isEmpty()) { |
| if (mDeclaredIds == null) { |
| mDeclaredIds = Sets.newHashSet(); |
| } |
| mDeclaredIds.add(NEW_ID_PREFIX + name); |
| mGlobalIds.add(NEW_ID_PREFIX + name); |
| } |
| } |
| } else { |
| assert tagName.equals(RELATIVE_LAYOUT) |
| || tagName.equals(PERCENT_RELATIVE_LAYOUT) |
| || CLASS_CONSTRAINT_LAYOUT.isEquals(tagName); |
| if (mRelativeLayouts == null) { |
| mRelativeLayouts = new ArrayList<>(); |
| } |
| mRelativeLayouts.add(element); |
| } |
| } |
| |
| @Override |
| public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { |
| assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID); |
| String id = attribute.getValue(); |
| mFileIds.add(id); |
| mGlobalIds.add(id); |
| |
| if (id.equals(NEW_ID_PREFIX) || id.equals(ID_PREFIX)) { |
| String message = "Invalid id: missing value"; |
| context.report(INVALID, attribute, context.getLocation(attribute), message); |
| } else if (id.startsWith("@+") |
| && !id.startsWith(NEW_ID_PREFIX) |
| && !id.startsWith("@+android:id/") |
| || id.startsWith(NEW_ID_PREFIX) && id.indexOf('/', NEW_ID_PREFIX.length()) != -1) { |
| int nameStart = id.startsWith(NEW_ID_PREFIX) ? NEW_ID_PREFIX.length() : 2; |
| String suggested = NEW_ID_PREFIX + id.substring(nameStart).replace('/', '_'); |
| String message = |
| String.format( |
| "ID definitions **must** be of the form `@+id/name`; try using `%1$s`", |
| suggested); |
| context.report(INVALID, attribute, context.getLocation(attribute), message); |
| } |
| } |
| |
| 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_PREFIX)) { |
| return ids.contains(ID_PREFIX + id.substring(NEW_ID_PREFIX.length())); |
| } else if (id.startsWith(ID_PREFIX)) { |
| return ids.contains(NEW_ID_PREFIX + id.substring(ID_PREFIX.length())); |
| } else { |
| return ids.contains(NEW_ID_PREFIX + id); |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean idDefined(@NonNull Context context, @NonNull String id, @Nullable File notIn) { |
| ResourceRepository resources = |
| context.getClient().getResourceRepository(context.getProject(), true, true); |
| if (resources != null) { |
| List<ResourceItem> items = |
| resources.getResources( |
| ResourceNamespace.TODO(), ResourceType.ID, stripIdPrefix(id)); |
| for (ResourceItem item : items) { |
| PathString source = item.getSource(); |
| if (source != null) { |
| String parentName = source.getParentFileName(); |
| if (parentName != null && parentName.startsWith(FD_RES_VALUES)) { |
| if (mDeclaredIds == null) { |
| mDeclaredIds = Sets.newHashSet(); |
| } |
| mDeclaredIds.add(id); |
| continue; |
| } |
| |
| // Ignore definitions in the given file. This is used to ignore |
| // matches in the same file as the reference, since the reference |
| // is often expressed as a definition. |
| if (!isSameResourceFile(source.toFile(), notIn)) { |
| return true; |
| } |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| private static 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.isEmpty()) { |
| for (String matchWith : ids) { |
| matchWith = stripIdPrefix(matchWith); |
| int distance = editDistance(id, matchWith, maxDistance); |
| 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> strings = matches.get(i); |
| if (strings != null && !strings.isEmpty()) { |
| List<String> suggestions = new ArrayList<>(strings); |
| Collections.sort(suggestions); |
| return suggestions; |
| } |
| } |
| |
| return Collections.emptyList(); |
| } |
| } |