blob: 913fb3de5720f95682ee870c525c37c6e38aef4b [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.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_END;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING;
import static com.android.SdkConstants.ATTR_LAYOUT_BELOW;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT;
import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN;
import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_END;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_START;
import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW;
import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_SPAN;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_END_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_START_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_LAYOUT_X;
import static com.android.SdkConstants.ATTR_LAYOUT_Y;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.GRID_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.VIEW_INCLUDE;
import static com.android.SdkConstants.VIEW_MERGE;
import static com.android.SdkConstants.VIEW_TAG;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.SdkInfo;
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 org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Looks for layout params on views that are "obsolete" - may have made sense
* when the view was added but there is a different layout parent now which does
* not use the given layout params.
*/
public class ObsoleteLayoutParamsDetector extends LayoutDetector {
/** Usage of deprecated views or attributes */
public static final Issue ISSUE = Issue.create(
"ObsoleteLayoutParam", //$NON-NLS-1$
"Obsolete layout params",
"The given layout_param is not defined for the given layout, meaning it has no " +
"effect. This usually happens when you change the parent layout or move view " +
"code around without updating the layout params. This will cause useless " +
"attribute processing at runtime, and is misleading for others reading the " +
"layout so the parameter should be removed.",
Category.PERFORMANCE,
6,
Severity.WARNING,
new Implementation(
ObsoleteLayoutParamsDetector.class,
Scope.RESOURCE_FILE_SCOPE));
/**
* Set of layout parameter names that are considered valid no matter what so
* no other checking is necessary - such as layout_width and layout_height.
*/
private static final Set<String> VALID = new HashSet<String>(10);
/**
* Mapping from a layout parameter name (local name only) to the defining
* ViewGroup. Note that it's possible for the same name to be defined by
* multiple ViewGroups - but it turns out this is extremely rare (the only
* examples are layout_column defined by both TableRow and GridLayout, and
* layout_gravity defined by many layouts) so rather than handle this with
* every single layout attribute pointing to a list, this is just special
* cased instead.
*/
private static final Map<String, String> PARAM_TO_VIEW = new HashMap<String, String>(28);
static {
// Available (mostly) everywhere: No check
VALID.add(ATTR_LAYOUT_WIDTH);
VALID.add(ATTR_LAYOUT_HEIGHT);
// The layout_gravity isn't "global" but it's defined on many of the most
// common layouts (FrameLayout, LinearLayout and GridLayout) so we don't
// currently check for it. In order to do this we'd need to make the map point
// to lists rather than individual layouts or we'd need a bunch of special cases
// like the one done for layout_column below.
VALID.add(ATTR_LAYOUT_GRAVITY);
// From ViewGroup.MarginLayoutParams
VALID.add(ATTR_LAYOUT_MARGIN_LEFT);
VALID.add(ATTR_LAYOUT_MARGIN_START);
VALID.add(ATTR_LAYOUT_MARGIN_RIGHT);
VALID.add(ATTR_LAYOUT_MARGIN_END);
VALID.add(ATTR_LAYOUT_MARGIN_TOP);
VALID.add(ATTR_LAYOUT_MARGIN_BOTTOM);
VALID.add(ATTR_LAYOUT_MARGIN);
// Absolute Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_X, ABSOLUTE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_Y, ABSOLUTE_LAYOUT);
// Linear Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_WEIGHT, LINEAR_LAYOUT);
// Grid Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN_SPAN, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW_SPAN, GRID_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW_SPAN, GRID_LAYOUT);
// Table Layout
// ATTR_LAYOUT_COLUMN is defined for both GridLayout and TableLayout,
// so we don't want to do
// PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN, TABLE_ROW);
// here since it would wipe out the above GridLayout registration.
// Since this is the only case where there is a conflict (in addition to layout_gravity
// which is defined in many places), rather than making the map point to lists
// this specific case is just special cased below, look for ATTR_LAYOUT_COLUMN.
PARAM_TO_VIEW.put(ATTR_LAYOUT_SPAN, TABLE_ROW);
// Relative Layout
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_LEFT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_START, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_RIGHT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_END, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_TOP, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_BOTTOM, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_TOP, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_LEFT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_START, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_END, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_BASELINE, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_IN_PARENT, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_VERTICAL, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_HORIZONTAL, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_RIGHT_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_END_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_LEFT_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_START_OF, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_BELOW, RELATIVE_LAYOUT);
PARAM_TO_VIEW.put(ATTR_LAYOUT_ABOVE, RELATIVE_LAYOUT);
}
/**
* Map from an included layout to all the including contexts (each including
* context is a pair of a file containing the include to the parent tag at
* the included location)
*/
private Map<String, List<Pair<File, String>>> mIncludes;
/**
* List of pending include checks. When a layout parameter attribute is
* found on a root element, or on a child of a {@code merge} root tag, then
* we want to check across layouts whether the including context (the parent
* of the include tag) is valid for this attribute. We cannot check this
* immediately because we are processing the layouts in an arbitrary order
* so the included layout may be seen before the including layout and so on.
* Therefore, we stash these attributes to be checked after we're done. Each
* pair is a pair of an attribute name to be checked, and the file that
* attribute is referenced in.
*/
private final List<Pair<String, Location.Handle>> mPending =
new ArrayList<Pair<String,Location.Handle>>();
/** Constructs a new {@link ObsoleteLayoutParamsDetector} */
public ObsoleteLayoutParamsDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(VIEW_INCLUDE);
}
@Override
public Collection<String> getApplicableAttributes() {
return ALL;
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String name = attribute.getLocalName();
if (name != null && name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)
&& ANDROID_URI.equals(attribute.getNamespaceURI())) {
if (VALID.contains(name)) {
return;
}
String parent = PARAM_TO_VIEW.get(name);
if (parent != null) {
Element viewElement = attribute.getOwnerElement();
Node layoutNode = viewElement.getParentNode();
if (layoutNode == null || layoutNode.getNodeType() != Node.ELEMENT_NODE) {
// This is a layout attribute on a root element; this presumably means
// that this layout is included so check the included layouts to make
// sure at least one included context is valid for this layout_param.
// We can't do that yet since we may be processing the include tag to
// this layout after the layout itself. Instead, stash a work order...
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
Location.Handle handle = context.createLocationHandle(attribute);
handle.setClientData(attribute);
mPending.add(Pair.of(name, handle));
}
return;
}
String parentTag = ((Element) layoutNode).getTagName();
if (parentTag.equals(VIEW_MERGE)) {
// This is a merge which means we need to check the including contexts,
// wherever they are. This has to be done after all the files have been
// scanned since we are not processing the files in any particular order.
if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) {
Location.Handle handle = context.createLocationHandle(attribute);
handle.setClientData(attribute);
mPending.add(Pair.of(name, handle));
}
return;
}
if (!isValidParamForParent(context, name, parent, parentTag)) {
if (name.equals(ATTR_LAYOUT_COLUMN)
&& isValidParamForParent(context, name, TABLE_ROW, parentTag)) {
return;
}
context.report(ISSUE, attribute, context.getLocation(attribute),
String.format("Invalid layout param in a `%1$s`: `%2$s`", parentTag, name));
}
} else {
// We could warn about unknown layout params but this might be brittle if
// new params are added or if people write custom ones; this is just a log
// for us to track these and update the check as necessary:
//context.client.log(null,
// String.format("Unrecognized layout param '%1$s'", name));
}
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String layout = element.getAttribute(ATTR_LAYOUT);
if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts
layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());
Node parent = element.getParentNode();
if (parent.getNodeType() == Node.ELEMENT_NODE) {
String tag = parent.getNodeName();
if (tag.indexOf('.') == -1 && !tag.equals(VIEW_MERGE)) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mIncludes == null) {
mIncludes = new HashMap<String, List<Pair<File, String>>>();
}
List<Pair<File, String>> includes = mIncludes.get(layout);
if (includes == null) {
includes = new ArrayList<Pair<File, String>>();
mIncludes.put(layout, includes);
}
includes.add(Pair.of(context.file, tag));
}
}
}
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mIncludes == null) {
return;
}
for (Pair<String, Location.Handle> pending : mPending) {
Handle handle = pending.getSecond();
Location location = handle.resolve();
File file = location.getFile();
String layout = file.getName();
if (layout.endsWith(DOT_XML)) {
layout = layout.substring(0, layout.length() - DOT_XML.length());
}
List<Pair<File, String>> includes = mIncludes.get(layout);
if (includes == null) {
// Nobody included this file
continue;
}
String name = pending.getFirst();
String parent = PARAM_TO_VIEW.get(name);
if (parent == null) {
continue;
}
boolean isValid = false;
for (Pair<File, String> include : includes) {
String parentTag = include.getSecond();
if (isValidParamForParent(context, name, parent, parentTag)) {
isValid = true;
break;
} else if (!isValid && name.equals(ATTR_LAYOUT_COLUMN)
&& isValidParamForParent(context, name, TABLE_ROW, parentTag)) {
isValid = true;
break;
}
}
if (!isValid) {
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ISSUE, (Node) clientData)) {
return;
}
}
StringBuilder sb = new StringBuilder(40);
for (Pair<File, String> include : includes) {
if (sb.length() > 0) {
sb.append(", "); //$NON-NLS-1$
}
File from = include.getFirst();
String parentTag = include.getSecond();
sb.append(String.format("included from within a `%1$s` in `%2$s`",
parentTag,
from.getParentFile().getName() + File.separator + from.getName()));
}
String message = String.format("Invalid layout param '`%1$s`' (%2$s)",
name, sb.toString());
// TODO: Compute applicable scope node
context.report(ISSUE, location, message);
}
}
}
/**
* Checks whether the given layout parameter name is valid for the given
* parent tag assuming it has the given current parent tag
*/
private static boolean isValidParamForParent(Context context, String name, String parent,
String parentTag) {
if (parentTag.indexOf('.') != -1 || parentTag.equals(VIEW_TAG)) {
// Custom tag: We don't know whether it extends one of the builtin
// types where the layout param is valid, so don't complain
return true;
}
SdkInfo sdk = context.getSdkInfo();
if (!parentTag.equals(parent)) {
String tag = sdk.getParentViewName(parentTag);
while (tag != null) {
if (tag.equals(parent)) {
return true;
}
tag = sdk.getParentViewName(tag);
}
return false;
}
return true;
}
}