Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* 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_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.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.tools.lint.checks.RequiredAttributeDetector.PERCENT_RELATIVE_LAYOUT;
import static com.android.tools.lint.detector.api.LintUtils.editDistance;
import static com.android.tools.lint.detector.api.LintUtils.getChildren;
import static com.android.tools.lint.detector.api.LintUtils.isSameResourceFile;
import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
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.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.utils.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 java.io.File;
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 {
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 mGlobalIds = new HashSet(100);
/** Ids bound to widgets in the current layout file */
private Set mFileIds;
/** Ids declared in a value's file, e.g. {@code } */
private Set 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> mHandles;
/**
* 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 mRelativeLayouts;
/** Reference to an unknown id */
@SuppressWarnings("unchecked")
public static final Issue UNKNOWN_ID = Issue.create(
"UnknownId", //$NON-NLS-1$
"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", //$NON-NLS-1$
"RelativeLayout Invalid Constraints",
"Layout constraints in a given `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", //$NON-NLS-1$
"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", //$NON-NLS-1$
"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;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection getApplicableAttributes() {
return Collections.singletonList(ATTR_ID);
}
@Override
public Collection getApplicableElements() {
return Arrays.asList(RELATIVE_LAYOUT, TAG_ITEM, PERCENT_RELATIVE_LAYOUT,
SdkConstants.CLASS_CONSTRAINT_LAYOUT);
}
@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) {
List children = getChildren(layout);
Set ids = Sets.newHashSetWithExpectedSize(children.size());
for (Element child : children) {
String id = child.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty()) {
ids.add(id);
}
}
boolean isConstraintLayout = layout.getTagName().equals(SdkConstants.CLASS_CONSTRAINT_LAYOUT);
for (Element element : children) {
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))
&& attr.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
&& (ANDROID_URI.equals(attr.getNamespaceURI()) ||
AUTO_URI.equals(attr.getNamespaceURI()))) {
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;
Handle handle = xmlContext.createLocationHandle(attr);
handle.setClientData(attr);
if (mHandles == null) {
mHandles = new ArrayList>();
}
mHandles.add(Pair.of(value, handle));
} else {
// Check siblings. TODO: Look for cycles!
if (ids.contains(value)) {
// Make sure it's not pointing to self
if (!ATTR_ID.equals(attr.getLocalName())
&& !selfId.isEmpty()
&& value.endsWith(selfId)
&& stripIdPrefix(value).equals(selfId)) {
XmlContext xmlContext = (XmlContext) context;
String message = String.format(
"Cannot be relative to self: id=%1$s, %2$s=%3$s",
selfId, attr.getLocalName(), selfId);
Location location = xmlContext.getLocation(attr);
xmlContext.report(NOT_SIBLING, attr, location, message);
}
continue;
}
if (value.startsWith(NEW_ID_PREFIX)) {
if (ids.contains(ID_PREFIX + stripIdPrefix(value))) {
continue;
}
} else {
assert value.startsWith(ID_PREFIX) : value;
if (ids.contains(NEW_ID_PREFIX + stripIdPrefix(value))) {
continue;
}
}
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(value))) {
continue;
}
}
if (context.isEnabled(NOT_SIBLING)) {
XmlContext xmlContext = (XmlContext) context;
String message = String.format(
"`%1$s` is not a sibling in the same `RelativeLayout`",
value);
Location location = xmlContext.getLocation(attr);
xmlContext.report(NOT_SIBLING, attr, location, message);
}
}
}
}
}
}
}
mFileIds = null;
if (!context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
checkHandles(context);
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
checkHandles(context);
}
}
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 (Pair pair : mHandles) {
String id = pair.getFirst();
boolean isBound = projectScope ? idDefined(mGlobalIds, id) :
idDefined(context, id, context.file);
LintClient client = context.getClient();
if (!isBound && checkExists
&& (projectScope || client.supportsProjectResources())) {
Handle handle = pair.getSecond();
boolean isDeclared = idDefined(mDeclaredIds, id);
id = stripIdPrefix(id);
String suggestionMessage;
Set spellingDictionary = mGlobalIds;
if (!projectScope && client.supportsProjectResources()) {
AbstractResourceRepository resources =
client.getResourceRepository(context.getProject(), true, false);
if (resources != null) {
spellingDictionary = Sets.newHashSet(
resources.getItemsOfType(ResourceType.ID));
spellingDictionary.remove(id);
}
}
List 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.)
Handle handle = pair.getSecond();
report(context, UNKNOWN_ID_LAYOUT, handle,
String.format(
"The id \"`%1$s`\" is not referring to any views in this layout",
stripIdPrefix(id)));
}
}
}
}
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(ID_PREFIX + name);
}
}
} else {
assert tagName.equals(RELATIVE_LAYOUT)
|| tagName.equals(PERCENT_RELATIVE_LAYOUT)
|| tagName.equals(SdkConstants.CLASS_CONSTRAINT_LAYOUT);
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) || "@+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) //$NON-NLS-1$
&& !id.startsWith("@+android:id/") //$NON-NLS-1$
|| 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 ids, String id) {
if (ids == null) {
return false;
}
boolean definedLocally = ids.contains(id);
if (!definedLocally) {
if (id.startsWith(NEW_ID_PREFIX)) {
definedLocally = ids.contains(ID_PREFIX +
id.substring(NEW_ID_PREFIX.length()));
} else if (id.startsWith(ID_PREFIX)) {
definedLocally = ids.contains(NEW_ID_PREFIX +
id.substring(ID_PREFIX.length()));
}
}
return definedLocally;
}
private boolean idDefined(@NonNull Context context, @NonNull String id,
@Nullable File notIn) {
AbstractResourceRepository resources =
context.getClient().getResourceRepository(context.getProject(), true, true);
if (resources != null) {
List items = resources.getResourceItem(ResourceType.ID,
stripIdPrefix(id));
if (items == null || items.isEmpty()) {
return false;
}
for (ResourceItem item : items) {
ResourceFile source = item.getSource();
if (source != null) {
File file = source.getFile();
if (file.getParentFile().getName().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(file, notIn)) {
return true;
}
}
}
}
return false;
}
private static List getSpellingSuggestions(String id, Collection ids) {
int maxDistance = id.length() >= 4 ? 2 : 1;
// Look for typos and try to match with custom views and android views
Multimap 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 strings = matches.get(i);
if (strings != null && !strings.isEmpty()) {
List suggestions = new ArrayList(strings);
Collections.sort(suggestions);
return suggestions;
}
}
return Collections.emptyList();
}
}