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