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