blob: 52f0ed05f4202b01e58837acb730ac7512e86c44 [file] [log] [blame]
/*
* Copyright (C) 2014 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.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_NAME;
import static com.android.SdkConstants.DIMEN_PREFIX;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.TAG_DIMEN;
import static com.android.SdkConstants.TAG_ITEM;
import static com.android.SdkConstants.TAG_STYLE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
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.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
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.Project;
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 org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
/**
* Checks for negative margins in the following scenarios:
* <ul>
* <li>In direct layout attribute usages, e.g. {@code <Button android:layoutMargin="-5dp"}</li>
* <li>In theme styles, e.g. {@code <item name="android:layoutMargin">-5dp</item>}</li>
* <li>In dimension usages, e.g. {@code <Button android:layoutMargin="@dimen/foo"} along
* with {@code <dimen name="foo">-5dp</dimen>}</li>
* </ul>
*/
public class NegativeMarginDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION = new Implementation(
NegativeMarginDetector.class,
Scope.RESOURCE_FILE_SCOPE);
/** Negative margins */
public static final Issue ISSUE = Issue.create(
"NegativeMargin", //$NON-NLS-1$
"Negative Margins",
"Margin values should be positive. Negative values are generally a sign that " +
"you are making assumptions about views surrounding the current one, or may be "+
"tempted to turn off child clipping to allow a view to escape its parent. " +
"Turning off child clipping to do this not only leads to poor graphical " +
"performance, it also results in wrong touch event handling since touch events " +
"are based strictly on a chain of parent-rect hit tests. Finally, making " +
"assumptions about the size of strings can lead to localization problems.",
Category.USABILITY,
4,
Severity.WARNING,
IMPLEMENTATION).setEnabledByDefault(false);
private HashMap<String, Location.Handle> mDimenUsage;
/** Constructs a new {@link NegativeMarginDetector} */
public NegativeMarginDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// Look in both layouts (at attribute values) and in value files (style and dimension
// definitions)
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@Override
public Collection<String> getApplicableAttributes() {
return Arrays.asList(
ATTR_LAYOUT_MARGIN,
ATTR_LAYOUT_MARGIN_LEFT,
ATTR_LAYOUT_MARGIN_TOP,
ATTR_LAYOUT_MARGIN_RIGHT,
ATTR_LAYOUT_MARGIN_BOTTOM,
ATTR_LAYOUT_MARGIN_START,
ATTR_LAYOUT_MARGIN_END
);
}
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Arrays.asList(TAG_DIMEN, TAG_STYLE);
}
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
String value = attribute.getValue();
checkMarginValue(context, value, attribute, null);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (context.getResourceFolderType() != ResourceFolderType.VALUES) {
return;
}
String tag = element.getTagName();
if (TAG_DIMEN.equals(tag)) {
NodeList itemNodes = element.getChildNodes();
String name = element.getAttribute(ATTR_NAME);
Location.Handle handle = mDimenUsage != null ? mDimenUsage.get(name) : null;
if (handle != null) {
for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
Node item = itemNodes.item(j);
if (item.getNodeType() == Node.TEXT_NODE) {
String text = item.getNodeValue().trim();
checkMarginValue(context, text, null, handle);
}
}
}
} else {
assert TAG_STYLE.equals(tag) : tag;
NodeList itemNodes = element.getChildNodes();
for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) {
Node item = itemNodes.item(j);
if (item.getNodeType() == Node.ELEMENT_NODE &&
TAG_ITEM.equals(item.getNodeName())) {
Element itemElement = (Element) item;
String name = itemElement.getAttribute(ATTR_NAME);
if (name.startsWith(PREFIX_ANDROID) &&
name.startsWith(ATTR_LAYOUT_MARGIN, PREFIX_ANDROID.length())) {
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() != Node.TEXT_NODE) {
return;
}
checkMarginValue(context, child.getNodeValue(), child, null);
}
}
}
}
}
}
private static boolean isNegativeDimension(@NonNull String value) {
return value.trim().startsWith("-");
}
private void checkMarginValue(
@NonNull XmlContext context,
@NonNull String value,
@Nullable Node scope,
@Nullable Location.Handle handle) {
if (isNegativeDimension(value)) {
String message = "Margin values should not be negative";
if (scope != null) {
context.report(ISSUE, scope, context.getLocation(scope), message);
} else {
assert handle != null;
context.report(ISSUE, handle.resolve(), message);
}
} else if (value.startsWith(DIMEN_PREFIX) && scope != null) {
ResourceUrl url = ResourceUrl.parse(value);
if (url == null) {
return;
}
if (context.getClient().supportsProjectResources()) {
// Typically interactive IDE usage, where we are only analyzing a single file,
// but we can use the IDE to resolve resource URLs
LintClient client = context.getClient();
Project project = context.getProject();
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources != null) {
List<ResourceItem> items = resources.getResourceItem(url.type, url.name);
if (items != null) {
for (ResourceItem item : items) {
ResourceValue resourceValue = item.getResourceValue(false);
if (resourceValue != null) {
String dimenValue = resourceValue.getValue();
if (dimenValue != null && isNegativeDimension(dimenValue)) {
ResourceFile sourceFile = item.getSource();
assert sourceFile != null;
String message = String.format(
"Margin values should not be negative "
+ "(`%1$s` is defined as `%2$s` in `%3$s`",
value, dimenValue, sourceFile.getFile());
context.report(ISSUE, scope,
context.getLocation(scope),
message);
break;
}
}
}
}
}
} else if (!context.getDriver().isSuppressed(context, ISSUE, scope)) {
// Batch mode where we process layouts then values in order
if (mDimenUsage == null) {
mDimenUsage = new HashMap<String, Location.Handle>();
}
mDimenUsage.put(url.name, context.createLocationHandle(scope));
}
}
}
}