| /* |
| * 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); |
| } |
| } |
| } |