blob: 631a118beeb01bb39566ed75e06e97256fef02a8 [file] [log] [blame]
/*
* 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_CLASS;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.VIEW_TAG;
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.ide.common.resources.ResourceUrl;
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.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.ResourceEvaluator;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.utils.XmlUtils;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.PsiTypeElement;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Detector for finding inconsistent usage of views and casts
* <p>
* TODO: Check findFragmentById
* <pre>
* ((ItemListFragment) getSupportFragmentManager()
* .findFragmentById(R.id.item_list))
* .setActivateOnItemClick(true);
* </pre>
* Here we should check the {@code <fragment>} tag pointed to by the id, and
* check its name or class attributes to make sure the cast is compatible with
* the named fragment class!
*/
public class ViewTypeDetector extends ResourceXmlDetector implements JavaPsiScanner {
/** Mismatched view types */
@SuppressWarnings("unchecked")
public static final Issue ISSUE = Issue.create(
"WrongViewCast", //$NON-NLS-1$
"Mismatched view type",
"Keeps track of the view types associated with ids and if it finds a usage of " +
"the id in the Java code it ensures that it is treated as the same type.",
Category.CORRECTNESS,
9,
Severity.FATAL,
new Implementation(
ViewTypeDetector.class,
EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES),
Scope.JAVA_FILE_SCOPE));
/** Flag used to do no work if we're running in incremental mode in a .java file without
* a client supporting project resources */
private Boolean mIgnore = null;
private final Map<String, Object> mIdToViewTag = new HashMap<String, Object>(50);
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_ID);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String view = attribute.getOwnerElement().getTagName();
String value = attribute.getValue();
String id = null;
if (value.startsWith(ID_PREFIX)) {
id = value.substring(ID_PREFIX.length());
} else if (value.startsWith(NEW_ID_PREFIX)) {
id = value.substring(NEW_ID_PREFIX.length());
} // else: could be @android id
if (id != null) {
if (view.equals(VIEW_TAG)) {
view = attribute.getOwnerElement().getAttribute(ATTR_CLASS);
}
Object existing = mIdToViewTag.get(id);
if (existing == null) {
mIdToViewTag.put(id, view);
} else if (existing instanceof String) {
String existingString = (String) existing;
if (!existingString.equals(view)) {
// Convert to list
List<String> list = new ArrayList<String>(2);
list.add((String) existing);
list.add(view);
mIdToViewTag.put(id, list);
}
} else if (existing instanceof List<?>) {
@SuppressWarnings("unchecked")
List<String> list = (List<String>) existing;
if (!list.contains(view)) {
list.add(view);
}
}
}
}
// ---- Implements Detector.JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("findViewById"); //$NON-NLS-1$
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
@NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) {
LintClient client = context.getClient();
if (mIgnore == Boolean.TRUE) {
return;
} else if (mIgnore == null) {
mIgnore = !context.getScope().contains(Scope.ALL_RESOURCE_FILES) &&
!client.supportsProjectResources();
if (mIgnore) {
return;
}
}
assert method.getName().equals("findViewById");
PsiElement node = call;
while (node != null && node.getParent() instanceof PsiParenthesizedExpression) {
node = node.getParent();
}
if (node.getParent() instanceof PsiTypeCastExpression) {
PsiTypeCastExpression cast = (PsiTypeCastExpression) node.getParent();
PsiTypeElement castTypeElement = cast.getCastType();
if (castTypeElement == null) {
return;
}
PsiType type = castTypeElement.getType();
String castType = null;
if (type instanceof PsiClassType) {
castType = type.getCanonicalText();
}
if (castType == null) {
return;
}
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length == 1) {
PsiExpression first = args[0];
ResourceUrl resourceUrl = ResourceEvaluator.getResource(context.getEvaluator(),
first);
if (resourceUrl != null && resourceUrl.type == ResourceType.ID &&
!resourceUrl.framework) {
String id = resourceUrl.name;
if (client.supportsProjectResources()) {
AbstractResourceRepository resources = client
.getProjectResources(context.getMainProject(), true);
if (resources == null) {
return;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.ID,
id);
if (items != null && !items.isEmpty()) {
Set<String> compatible = Sets.newHashSet();
for (ResourceItem item : items) {
Collection<String> tags = getViewTags(context, item);
if (tags != null) {
compatible.addAll(tags);
}
}
if (!compatible.isEmpty()) {
ArrayList<String> layoutTypes = Lists.newArrayList(compatible);
checkCompatible(context, castType, null, layoutTypes, cast);
}
}
} else {
Object types = mIdToViewTag.get(id);
if (types instanceof String) {
String layoutType = (String) types;
checkCompatible(context, castType, layoutType, null, cast);
} else if (types instanceof List<?>) {
@SuppressWarnings("unchecked")
List<String> layoutTypes = (List<String>) types;
checkCompatible(context, castType, null, layoutTypes, cast);
}
}
}
}
}
}
@Nullable
protected Collection<String> getViewTags(
@NonNull Context context,
@NonNull ResourceItem item) {
// Check view tag in this file. Can I do it cheaply? Try with
// an XML pull parser. Or DOM if we have multiple resources looked
// up?
ResourceFile source = item.getSource();
if (source != null) {
File file = source.getFile();
Multimap<String,String> map = getIdToTagsIn(context, file);
if (map != null) {
return map.get(item.getName());
}
}
return null;
}
private Map<File, Multimap<String, String>> mFileIdMap;
@Nullable
private Multimap<String, String> getIdToTagsIn(@NonNull Context context, @NonNull File file) {
if (!file.getPath().endsWith(DOT_XML)) {
return null;
}
if (mFileIdMap == null) {
mFileIdMap = Maps.newHashMap();
}
Multimap<String, String> map = mFileIdMap.get(file);
if (map == null) {
map = ArrayListMultimap.create();
mFileIdMap.put(file, map);
String xml = context.getClient().readFile(file);
// TODO: Use pull parser instead for better performance!
Document document = XmlUtils.parseDocumentSilently(xml, true);
if (document != null && document.getDocumentElement() != null) {
addViewTags(map, document.getDocumentElement());
}
}
return map;
}
private static void addViewTags(Multimap<String, String> map, Element element) {
String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty()) {
id = LintUtils.stripIdPrefix(id);
if (!map.containsEntry(id, element.getTagName())) {
map.put(id, element.getTagName());
}
}
NodeList children = element.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
addViewTags(map, (Element) child);
}
}
}
/** Check if the view and cast type are compatible */
private static void checkCompatible(JavaContext context, String castType, String layoutType,
List<String> layoutTypes, PsiTypeCastExpression node) {
assert layoutType == null || layoutTypes == null; // Should only specify one or the other
boolean compatible = true;
if (layoutType != null) {
if (!layoutType.equals(castType)
&& !context.getSdkInfo().isSubViewOf(castType, layoutType)) {
compatible = false;
}
} else {
compatible = false;
assert layoutTypes != null;
for (String type : layoutTypes) {
if (type.equals(castType)
|| context.getSdkInfo().isSubViewOf(castType, type)) {
compatible = true;
break;
}
}
}
if (!compatible) {
if (layoutType == null) {
layoutType = Joiner.on("|").join(layoutTypes);
}
String message = String.format(
"Unexpected cast to `%1$s`: layout tag was `%2$s`",
castType.substring(castType.lastIndexOf('.') + 1), layoutType);
context.report(ISSUE, node, context.getLocation(node), message);
}
}
}