diff options
author | Tor Norbye <tnorbye@google.com> | 2012-10-15 20:31:30 -0700 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2012-10-16 10:18:18 -0700 |
commit | 32b243dd4b800771897ccc2e1d894ece1587bd67 (patch) | |
tree | b67c07de74f1d1c9536c077d0f174eea62915ea5 /lint | |
parent | 18f09df5f4251f5c370ba9fb92f414379b1bc530 (diff) | |
download | sdk-32b243dd4b800771897ccc2e1d894ece1587bd67.zip sdk-32b243dd4b800771897ccc2e1d894ece1587bd67.tar.gz sdk-32b243dd4b800771897ccc2e1d894ece1587bd67.tar.bz2 |
Check for missing layout_width and layout_height attributes
Also looks at style declarations and attributes on include
tags to allow elements to either pick up sizes from styles
or from the including context.
Change-Id: I91a944805d8a906ff63b5a22f2faa876e7292c19
Diffstat (limited to 'lint')
17 files changed, 851 insertions, 56 deletions
diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java index 97df003..d1c8f67 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java @@ -55,7 +55,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { private static final List<Issue> sIssues; static { - final int initialCapacity = 118; + final int initialCapacity = 119; List<Issue> issues = new ArrayList<Issue>(initialCapacity); issues.add(AccessibilityDetector.ISSUE); @@ -176,6 +176,7 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(PrivateKeyDetector.ISSUE); issues.add(AnnotationDetector.ISSUE); issues.add(SystemPermissionsDetector.ISSUE); + issues.add(RequiredAttributeDetector.ISSUE); assert initialCapacity >= issues.size() : issues.size(); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/ControlFlowGraph.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/ControlFlowGraph.java index b7e5787..5f5b2fe 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/ControlFlowGraph.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/ControlFlowGraph.java @@ -1,11 +1,11 @@ /* * Copyright (C) 2012 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.tools.lint.checks; import com.android.annotations.NonNull; diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java index 5b2f632..2f416b5 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/IconDetector.java @@ -1616,7 +1616,7 @@ public class IconDetector extends ResourceXmlDetector implements Detector.JavaSc return super.visitConstructorInvocation(node); } - Node method = getParentMethod(node); + Node method = StringFormatDetector.getParentMethod(node); if (method != null) { // Must track local types String name = StringFormatDetector.getResourceForFirstArg(method, node); @@ -1639,7 +1639,7 @@ public class IconDetector extends ResourceXmlDetector implements Detector.JavaSc } } if (isBuilder) { - Node method = getParentMethod(node); + Node method = StringFormatDetector.getParentMethod(node); if (method != null) { SetIconFinder finder = new SetIconFinder(); method.accept(finder); @@ -1651,16 +1651,6 @@ public class IconDetector extends ResourceXmlDetector implements Detector.JavaSc } } - @Nullable - private Node getParentMethod(@NonNull Node node) { - Node method = node; - while (method != null && !(method.getParent() instanceof MethodDeclaration)) { - method = method.getParent(); - } - - return method; - } - private boolean handleSelect(Select select) { if (select.toString().startsWith(R_DRAWABLE_PREFIX)) { String name = select.astIdentifier().astValue(); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/MissingIdDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/MissingIdDetector.java index b02e44a..174261c 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/MissingIdDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/MissingIdDetector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2012 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. diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/RequiredAttributeDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/RequiredAttributeDetector.java new file mode 100644 index 0000000..f4b3830 --- /dev/null +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/RequiredAttributeDetector.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2012 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_NS_NAME_PREFIX; +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_LAYOUT; +import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; +import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_PARENT; +import static com.android.SdkConstants.ATTR_STYLE; +import static com.android.SdkConstants.FD_RES_LAYOUT; +import static com.android.SdkConstants.FN_RESOURCE_BASE; +import static com.android.SdkConstants.FQCN_GRID_LAYOUT_V7; +import static com.android.SdkConstants.GRID_LAYOUT; +import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.TABLE_LAYOUT; +import static com.android.SdkConstants.TABLE_ROW; +import static com.android.SdkConstants.TAG_ITEM; +import static com.android.SdkConstants.TAG_STYLE; +import static com.android.SdkConstants.VIEW_INCLUDE; +import static com.android.SdkConstants.VIEW_MERGE; +import static com.android.resources.ResourceFolderType.LAYOUT; +import static com.android.resources.ResourceFolderType.VALUES; +import static com.android.tools.lint.detector.api.LintUtils.getLayoutName; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceFolderType; +import com.android.tools.lint.detector.api.Category; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.Detector; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.JavaContext; +import com.android.tools.lint.detector.api.LayoutDetector; +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.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.ast.AstVisitor; +import lombok.ast.Expression; +import lombok.ast.MethodInvocation; +import lombok.ast.NullLiteral; +import lombok.ast.Select; +import lombok.ast.StrictListAccessor; +import lombok.ast.VariableReference; + +/** + * Ensures that layout width and height attributes are specified + */ +public class RequiredAttributeDetector extends LayoutDetector implements Detector.JavaScanner { + /** The main issue discovered by this detector */ + public static final Issue ISSUE = Issue.create( + "RequiredSize", //$NON-NLS-1$ + "Ensures that the layout_width and layout_height are specified for all views", + + "All views must specify an explicit layout_width and layout_height attribute. " + + "There is a runtime check for this, so if you fail to specify a size, an exception " + + "is thrown at runtime.\n" + + "\n" + + "It's possible to specify these widths via styles as well. GridLayout, as a special " + + "case, does not require you to specify a size.", + Category.CORRECTNESS, + 4, + Severity.ERROR, + RequiredAttributeDetector.class, + EnumSet.of(Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES)); + + /** Map from each style name to parent style */ + private @Nullable Map<String, String> mStyleParents; + + /** Set of style names where the style sets the layout width */ + private @Nullable Set<String> mWidthStyles; + + /** Set of style names where the style sets the layout height */ + private @Nullable Set<String> mHeightStyles; + + /** Set of layout names for layouts that are included by an {@code <include>} tag + * where the width is set on the include */ + private @Nullable Set<String> mIncludedWidths; + + /** Set of layout names for layouts that are included by an {@code <include>} tag + * where the height is set on the include */ + private @Nullable Set<String> mIncludedHeights; + + /** Set of layout names for layouts that are included by an {@code <include>} tag + * where the width is <b>not</b> set on the include */ + private @Nullable Set<String> mNotIncludedWidths; + + /** Set of layout names for layouts that are included by an {@code <include>} tag + * where the height is <b>not</b> set on the include */ + private @Nullable Set<String> mNotIncludedHeights; + + /** Constructs a new {@link RequiredAttributeDetector} */ + public RequiredAttributeDetector() { + } + + @Override + public @NonNull Speed getSpeed() { + return Speed.FAST; + } + + @Override + public boolean appliesTo(@NonNull ResourceFolderType folderType) { + return folderType == LAYOUT || folderType == VALUES; + } + + @Override + public void afterCheckProject(@NonNull Context context) { + // Process checks in two phases: + // Phase 1: Gather styles and includes (styles are encountered after the layouts + // so we can't do it in a single phase, and includes can be affected by includes from + // layouts we haven't seen yet) + // Phase 2: Process layouts, using gathered style and include data, and mark layouts + // not known. + // + if (context.getPhase() == 1) { + context.requestRepeat(this, Scope.RESOURCE_FILE_SCOPE); + } + } + + private boolean isWidthStyle(String style) { + return isSizeStyle(style, mWidthStyles); + } + + private boolean isHeightStyle(String style) { + return isSizeStyle(style, mHeightStyles); + } + + private boolean isSizeStyle(String style, Set<String> sizeStyles) { + if (sizeStyles == null) { + return false; + } + return isSizeStyle(stripStylePrefix(style), sizeStyles, 0); + } + + private boolean isSizeStyle( + @NonNull String style, + @NonNull Set<String> sizeStyles, int depth) { + if (depth == 30) { + // Cycle between local and framework attribute style missed + // by the fact that we're stripping the distinction between framework + // and local styles here + return false; + } + + assert !style.startsWith(STYLE_RESOURCE_PREFIX) + && !style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX); + + if (sizeStyles.contains(style)) { + return true; + } + + if (mStyleParents != null) { + String parentStyle = mStyleParents.get(style); + if (parentStyle != null) { + parentStyle = stripStylePrefix(parentStyle); + if (isSizeStyle(parentStyle, sizeStyles, depth + 1)) { + return true; + } + } + } + + int index = style.lastIndexOf('.'); + if (index > 0) { + return isSizeStyle(style.substring(0, index), sizeStyles, depth + 1); + } + + return false; + } + + private static boolean hasLayoutVariations(File file) { + File parent = file.getParentFile(); + if (parent == null) { + return false; + } + File res = file.getParentFile(); + if (res == null) { + return false; + } + String name = file.getName(); + File[] folders = res.listFiles(); + if (folders == null) { + return false; + } + for (File folder : folders) { + if (!folder.getName().startsWith(FD_RES_LAYOUT)) { + continue; + } + if (folder.equals(parent)) { + continue; + } + File other = new File(folder, name); + if (other.exists()) { + return true; + } + } + + return false; + } + + private static String stripStylePrefix(@NonNull String style) { + if (style.startsWith(STYLE_RESOURCE_PREFIX)) { + style = style.substring(STYLE_RESOURCE_PREFIX.length()); + } else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) { + style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length()); + } + + return style; + } + + private static boolean isRootElement(@NonNull Node node) { + return node == node.getOwnerDocument().getDocumentElement(); + } + + // ---- Implements XmlScanner ---- + + @Override + public Collection<String> getApplicableElements() { + return ALL; + } + + @Override + public void visitElement(@NonNull XmlContext context, @NonNull Element element) { + ResourceFolderType folderType = context.getResourceFolderType(); + int phase = context.getPhase(); + if (phase == 1 && folderType == VALUES) { + String tag = element.getTagName(); + if (TAG_STYLE.equals(tag)) { + String parent = element.getAttribute(ATTR_PARENT); + if (parent != null && !parent.isEmpty()) { + String name = element.getAttribute(ATTR_NAME); + if (name != null && !name.isEmpty()) { + if (mStyleParents == null) { + mStyleParents = Maps.newHashMap(); + } + mStyleParents.put(name, parent); + } + } + } else if (TAG_ITEM.equals(tag) + && TAG_STYLE.equals(element.getParentNode().getNodeName())) { + String name = element.getAttribute(ATTR_NAME); + if (name.endsWith(ATTR_LAYOUT_WIDTH) && + name.equals(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_WIDTH)) { + if (mWidthStyles == null) { + mWidthStyles = Sets.newHashSet(); + } + String styleName = ((Element) element.getParentNode()).getAttribute(ATTR_NAME); + mWidthStyles.add(styleName); + + } + if (name.endsWith(ATTR_LAYOUT_HEIGHT) && + name.equals(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_HEIGHT)) { + if (mHeightStyles == null) { + mHeightStyles = Sets.newHashSet(); + } + String styleName = ((Element) element.getParentNode()).getAttribute(ATTR_NAME); + mHeightStyles.add(styleName); + + } + } + } else if (folderType == LAYOUT) { + if (phase == 1) { + // Gather includes + if (element.getTagName().equals(VIEW_INCLUDE)) { + String layout = element.getAttribute(ATTR_LAYOUT); + if (layout != null && !layout.isEmpty()) { + recordIncludeWidth(layout, + element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)); + recordIncludeHeight(layout, + element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)); + } + } + } else { + assert phase == 2; // Check everything using style data and include data + boolean hasWidth = element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); + boolean hasHeight = element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + if (hasWidth && hasHeight) { + return; + } + + if (VIEW_MERGE.equals(element.getNodeName())) { + return; + } + + boolean certain = true; + + String parentTag = element.getParentNode() != null + ? element.getParentNode().getNodeName() : ""; + if (TABLE_LAYOUT.equals(parentTag) || TABLE_ROW.equals(parentTag)) { + return; + } + + boolean isRoot = isRootElement(element); + if (isRoot || isRootElement(element.getParentNode()) + && VIEW_MERGE.equals(parentTag)) { + String name = LAYOUT_RESOURCE_PREFIX + getLayoutName(context.file); + if (!hasWidth && mIncludedWidths != null) { + hasWidth = mIncludedWidths.contains(name); + // If the layout is *also* included in a context where the width + // was not set, we're not certain; it's possible that + if (mNotIncludedWidths != null && mNotIncludedWidths.contains(name)) { + hasWidth = false; + // If we only have a single layout we know that this layout isn't + // always included with layout_width or layout_height set, but + // if there are multiple layouts, it's possible that at runtime + // we only load the size-less layout by the tag which includes + // the size + certain = !hasLayoutVariations(context.file); + } + } + if (!hasHeight && mIncludedHeights != null) { + hasHeight = mIncludedHeights.contains(name); + if (mNotIncludedHeights != null && mNotIncludedHeights.contains(name)) { + hasHeight = false; + certain = !hasLayoutVariations(context.file); + } + } + if (hasWidth && hasHeight) { + return; + } + } + + if (!hasWidth || !hasHeight) { + String style = element.getAttribute(ATTR_STYLE); + if (style != null && !style.isEmpty()) { + if (!hasWidth) { + hasWidth = isWidthStyle(style); + } + if (!hasHeight) { + hasHeight = isHeightStyle(style); + } + } + if (hasWidth && hasHeight) { + return; + } + } + + String tag = element.getTagName(); + if (tag.equals(VIEW_INCLUDE) || tag.equals(GRID_LAYOUT) + || tag.equals(FQCN_GRID_LAYOUT_V7)) { + return; + } + String message; + if (!(hasWidth || hasHeight)) { + if (certain) { + message = "The required layout_width and layout_height attributes " + + "are missing"; + } else { + message = "The required layout_width and layout_height attributes " + + "*may* be missing"; + } + } else { + String attribute = hasWidth ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; + if (certain) { + message = String.format("The required %1$s attribute is missing", + attribute); + } else { + message = String.format("The required %1$s attribute *may* be missing", + attribute); + } + } + context.report(ISSUE, element, context.getLocation(element), + message, null); + } + } + } + + private void recordIncludeWidth(String layout, boolean providesWidth) { + if (providesWidth) { + if (mIncludedWidths == null) { + mIncludedWidths = Sets.newHashSet(); + } + mIncludedWidths.add(layout); + } else { + if (mNotIncludedWidths == null) { + mNotIncludedWidths = Sets.newHashSet(); + } + mNotIncludedWidths.add(layout); + } + } + + private void recordIncludeHeight(String layout, boolean providesHeight) { + if (providesHeight) { + if (mIncludedHeights == null) { + mIncludedHeights = Sets.newHashSet(); + } + mIncludedHeights.add(layout); + } else { + if (mNotIncludedHeights == null) { + mNotIncludedHeights = Sets.newHashSet(); + } + mNotIncludedHeights.add(layout); + } + } + + // ---- Implements JavaScanner ---- + + @Override + @Nullable + public List<String> getApplicableMethodNames() { + return Collections.singletonList("inflate"); //$NON-NLS-1$ + } + + @Override + public void visitMethod( + @NonNull JavaContext context, + @Nullable AstVisitor visitor, + @NonNull MethodInvocation call) { + // Handle + // View#inflate(Context context, int resource, ViewGroup root) + // LayoutInflater#inflate(int resource, ViewGroup root) + // LayoutInflater#inflate(int resource, ViewGroup root, boolean attachToRoot) + StrictListAccessor<Expression, MethodInvocation> args = call.astArguments(); + + String layout = null; + int index = 0; + for (Iterator<Expression> iterator = args.iterator(); iterator.hasNext(); index++) { + Expression expression = iterator.next(); + if (expression instanceof Select) { + Select outer = (Select) expression; + Expression operand = outer.astOperand(); + if (operand instanceof Select) { + Select inner = (Select) operand; + if (inner.astOperand() instanceof VariableReference) { + VariableReference reference = (VariableReference) inner.astOperand(); + if (FN_RESOURCE_BASE.equals(reference.astIdentifier().astValue()) + // TODO: constant + && "layout".equals(inner.astIdentifier().astValue())) { + layout = LAYOUT_RESOURCE_PREFIX + outer.astIdentifier().astValue(); + break; + } + } + } + } + } + + if (layout == null) { + lombok.ast.Node method = StringFormatDetector.getParentMethod(call); + if (method != null) { + // Must track local types + index = 0; + String name = StringFormatDetector.getResourceArg(method, call, index); + if (name == null) { + index = 1; + name = StringFormatDetector.getResourceArg(method, call, index); + } + if (name != null) { + layout = LAYOUT_RESOURCE_PREFIX + name; + } + } + if (layout == null) { + // Flow analysis didn't succeed + return; + } + } + + // In all the applicable signatures, the view root argument is immediately after + // the layout resource id. + int viewRootPos = index + 1; + if (viewRootPos < args.size()) { + int i = 0; + Iterator<Expression> iterator = args.iterator(); + while (iterator.hasNext() && i < viewRootPos) { + iterator.next(); + i++; + } + if (iterator.hasNext()) { + Expression viewRoot = iterator.next(); + if (viewRoot instanceof NullLiteral) { + // Yep, this one inflates the given view with a null parent: + // Tag it as such. For now just use the include data structure since + // it has the same net effect + recordIncludeWidth(layout, true); + recordIncludeHeight(layout, true); + return; + } + } + } + if (args.size() == 2) { + } else if (args.size() == 3) { + } else { +// Debugging only +context.report(ISSUE, context.getLocation(call), "Unexpected inflate signature here", null); + } + } +} diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java index bf16aeb..f0c499c 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StringFormatDetector.java @@ -863,12 +863,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto } private void checkFormatCall(JavaContext context, MethodInvocation node) { - lombok.ast.Node current = node.getParent(); - while (current != null - && !(current instanceof MethodDeclaration) - && !(current instanceof ConstructorDeclaration)) { - current = current.getParent(); - } + lombok.ast.Node current = getParentMethod(node); if (current != null) { checkStringFormatCall(context, current, node); } @@ -892,7 +887,7 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto return; } - StringTracker tracker = new StringTracker(method, call); + StringTracker tracker = new StringTracker(method, call, 0); method.accept(tracker); String name = tracker.getFormatStringName(); if (name == null) { @@ -1010,10 +1005,33 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto } } + /** Returns the parent method of the given AST node */ + @Nullable + static lombok.ast.Node getParentMethod(@NonNull lombok.ast.Node node) { + lombok.ast.Node current = node.getParent(); + while (current != null + && !(current instanceof MethodDeclaration) + && !(current instanceof ConstructorDeclaration)) { + current = current.getParent(); + } + + return current; + } + /** Returns the resource name corresponding to the first argument in the given call */ static String getResourceForFirstArg(lombok.ast.Node method, lombok.ast.Node call) { assert call instanceof MethodInvocation || call instanceof ConstructorInvocation; - StringTracker tracker = new StringTracker(method, call); + StringTracker tracker = new StringTracker(method, call, 0); + method.accept(tracker); + String name = tracker.getFormatStringName(); + + return name; + } + + /** Returns the resource name corresponding to the given argument in the given call */ + static String getResourceArg(lombok.ast.Node method, lombok.ast.Node call, int argIndex) { + assert call instanceof MethodInvocation || call instanceof ConstructorInvocation; + StringTracker tracker = new StringTracker(method, call, argIndex); method.accept(tracker); String name = tracker.getFormatStringName(); @@ -1044,6 +1062,8 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto private static class StringTracker extends ForwardingAstVisitor { /** Method we're searching within */ private final lombok.ast.Node mTop; + /** The argument index in the method we're targeting */ + private final int mArgIndex; /** Map from variable name to corresponding string resource name */ private final Map<String, String> mMap = new HashMap<String, String>(); /** Map from variable name to corresponding type */ @@ -1057,8 +1077,9 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto */ private String mName; - public StringTracker(lombok.ast.Node top, lombok.ast.Node targetNode) { + public StringTracker(lombok.ast.Node top, lombok.ast.Node targetNode, int argIndex) { mTop = top; + mArgIndex = argIndex; mTargetNode = targetNode; } @@ -1145,19 +1166,38 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto return false; } + @Nullable + private Expression getTargetArgument() { + Iterator<Expression> iterator; + if (mTargetNode instanceof MethodInvocation) { + iterator = ((MethodInvocation) mTargetNode).astArguments().iterator(); + } else if (mTargetNode instanceof ConstructorInvocation) { + iterator = ((ConstructorInvocation) mTargetNode).astArguments().iterator(); + } else { + return null; + } + int i = 0; + while (i < mArgIndex && iterator.hasNext()) { + iterator.next(); + i++; + } + if (iterator.hasNext()) { + return iterator.next(); + } + + return null; + } + @Override public boolean visitMethodInvocation(MethodInvocation node) { if (node == mTargetNode) { - StrictListAccessor<Expression, MethodInvocation> args = node.astArguments(); - if (args.size() > 0) { - Expression first = args.first(); - if (first instanceof VariableReference) { - VariableReference reference = (VariableReference) first; - String variable = reference.astIdentifier().astValue(); - mName = mMap.get(variable); - mDone = true; - return true; - } + Expression arg = getTargetArgument(); + if (arg instanceof VariableReference) { + VariableReference reference = (VariableReference) arg; + String variable = reference.astIdentifier().astValue(); + mName = mMap.get(variable); + mDone = true; + return true; } } @@ -1169,16 +1209,13 @@ public class StringFormatDetector extends ResourceXmlDetector implements Detecto @Override public boolean visitConstructorInvocation(ConstructorInvocation node) { if (node == mTargetNode) { - StrictListAccessor<Expression, ConstructorInvocation> args = node.astArguments(); - if (args.size() > 0) { - Expression first = args.first(); - if (first instanceof VariableReference) { - VariableReference reference = (VariableReference) first; - String variable = reference.astIdentifier().astValue(); - mName = mMap.get(variable); - mDone = true; - return true; - } + Expression arg = getTargetArgument(); + if (arg instanceof VariableReference) { + VariableReference reference = (VariableReference) arg; + String variable = reference.astIdentifier().astValue(); + mName = mMap.get(variable); + mDone = true; + return true; } } diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/XmlReporterTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/XmlReporterTest.java index 5792e1d..95147b4 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/XmlReporterTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/XmlReporterTest.java @@ -1,11 +1,11 @@ /* * Copyright (C) 2012 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.tools.lint; import com.android.tools.lint.checks.AbstractCheckTest; diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java index 7e139af..6d69e00 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/LabelForDetectorTest.java @@ -1,11 +1,11 @@ /* * Copyright (C) 2012 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.tools.lint.checks; import com.android.tools.lint.detector.api.Detector; diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/MissingIdDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/MissingIdDetectorTest.java index ac6b6c0..b72d8c8 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/MissingIdDetectorTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/MissingIdDetectorTest.java @@ -1,11 +1,11 @@ /* * Copyright (C) 2012 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.tools.lint.checks; import com.android.tools.lint.detector.api.Detector; diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/RequiredAttributeDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/RequiredAttributeDetectorTest.java new file mode 100644 index 0000000..abf3e04 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/RequiredAttributeDetectorTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2012 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 com.android.tools.lint.detector.api.Detector; + +@SuppressWarnings("javadoc") +public class RequiredAttributeDetectorTest extends AbstractCheckTest { + @Override + protected Detector getDetector() { + return new RequiredAttributeDetector(); + } + + public void test() throws Exception { + // Simple: Only consider missing attributes in the layout xml file + // (though skip warnings on <merge> tags and under <GridLayout> + assertEquals( + "res/layout/size.xml:13: Error: The required layout_height attribute is missing [RequiredSize]\n" + + " <RadioButton\n" + + " ^\n" + + "res/layout/size.xml:18: Error: The required layout_width attribute is missing [RequiredSize]\n" + + " <EditText\n" + + " ^\n" + + "res/layout/size.xml:23: Error: The required layout_width and layout_height attributes are missing [RequiredSize]\n" + + " <EditText\n" + + " ^\n" + + "3 errors, 0 warnings\n", + + lintProject("res/layout/size.xml")); + } + + public void test2() throws Exception { + // Consider styles (specifying sizes) and includes (providing sizes for the root tags) + assertEquals( + "res/layout/size2.xml:9: Error: The required layout_width and layout_height attributes are missing [RequiredSize]\n" + + " <Button\n" + + " ^\n" + + "res/layout/size2.xml:18: Error: The required layout_height attribute is missing [RequiredSize]\n" + + " <Button\n" + + " ^\n" + + "2 errors, 0 warnings\n", + + lintProject( + "res/layout/size2.xml", + "res/layout/sizeincluded.xml", + "res/values/sizestyles.xml" + //"res/layout/sizeincludedmerge" + )); + } + + public void testInflaters() throws Exception { + // Consider java inflation + assertEquals( + "res/layout/size5.xml:2: Error: The required layout_width and layout_height attributes are missing [RequiredSize]\n" + + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + "^\n" + + "1 errors, 0 warnings\n", + + lintProject( + "src/test/pkg/InflaterTest.java.txt=>src/test/pkg/InflaterTest.java", + "res/layout/sizeincluded.xml=>res/layout/size1.xml", + "res/layout/sizeincluded.xml=>res/layout/size2.xml", + "res/layout/sizeincluded.xml=>res/layout/size3.xml", + "res/layout/sizeincluded.xml=>res/layout/size4.xml", + "res/layout/sizeincluded.xml=>res/layout/size5.xml", + "res/layout/sizeincluded.xml=>res/layout/size6.xml", + "res/layout/sizeincluded.xml=>res/layout/size7.xml" + )); + } + +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/size.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/size.xml new file mode 100644 index 0000000..e4d4a9b --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/size.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <RadioButton + android:id="@+id/button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Button" /> + + <RadioButton + android:id="@+id/button2" + android:layout_width="wrap_content" + android:text="Button" /> + + <EditText + android:id="@+id/edittext" + android:layout_height="wrap_content" + android:text="EditText" /> + + <EditText + android:id="@+id/edittext2" + android:text="EditText" /> + +</LinearLayout> diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/size2.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/size2.xml new file mode 100644 index 0000000..d06d00f --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/size2.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:ignore="HardcodedText" > + + <Button + android:id="@+id/button1" + android:text="Button" /> + + <Button + android:id="@+id/button2" + style="@style/WidthAndHeight" + android:text="Button" /> + + <Button + android:id="@+id/button3" + style="@style/Width" + android:text="Button" /> + + <Button + android:id="@+id/button4" + style="@style/MyStyle" + android:text="Button" /> + + <Button + android:id="@+id/button5" + style="@style/MyStyle.Big" + android:text="Button" /> + + <Button + android:id="@+id/button6" + style="@style/MyOtherStyle" + android:text="Button" /> + + <include + android:layout_width="match_parent" + android:layout_height="match_parent" + layout="@layout/sizeincluded" /> + +</LinearLayout>
\ No newline at end of file diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/sizeincluded.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/sizeincluded.xml new file mode 100644 index 0000000..5fccff6 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/layout/sizeincluded.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" > + +</LinearLayout>
\ No newline at end of file diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/sizestyles.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/sizestyles.xml new file mode 100644 index 0000000..468974e --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/sizestyles.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="WidthAndHeight" parent="@android:attr/textAppearanceMedium"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">match_parent</item> + </style> + + <style name="Width" parent="@android:attr/textAppearanceMedium"> + <item name="android:layout_width">match_parent</item> + </style> + + <style name="MyStyle" parent="@style/WidthAndHeight"></style> + <style name="MyStyle.Big"></style> + <style name="MyOtherStyle" parent="@style/MyStyle.Big"></style> + +</resources>
\ No newline at end of file diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/InflaterTest.java.txt b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/InflaterTest.java.txt new file mode 100644 index 0000000..5a4e346 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/src/test/pkg/InflaterTest.java.txt @@ -0,0 +1,65 @@ +package test.pkg; + +import com.example.includetest.R; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +@SuppressWarnings("unused") +public class InflaterTest extends Activity { + private LayoutInflater mInflater; + private View mRootView; + + private LayoutInflater getInflater() { + if (mInflater == null) { + mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + return mInflater; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + public void testInflate1() { + View.inflate(this, R.layout.size1, null); + } + + public void testInflate2() { + mRootView = getInflater().inflate(R.layout.size2, null); + } + + public void testInflate4() { + getInflater().inflate(R.layout.size3, null, false); + } + + public void testInflate5() { + int mylayout = R.layout.size4; + getInflater().inflate(mylayout, null, false); + } + + public void testNotNull(ViewGroup root) { + getInflater().inflate(R.layout.size5, root, false); // Should be flagged + } + + public void testInflate6() { + int mylayout = R.layout.size7; + View.inflate(this, mylayout, null); + } + + public class MyButton extends Button { + public MyButton(Context context) { + super(context); + } + + public void test() { + inflate(getContext(), R.layout.size6, null); + } + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/client/api/LintClientTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/client/api/LintClientTest.java index 2a3a43a..a4376d8 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/client/api/LintClientTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/client/api/LintClientTest.java @@ -1,11 +1,11 @@ /* * Copyright (C) 2012 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.tools.lint.client.api; import com.android.tools.lint.Main; diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/IssueTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/IssueTest.java index eba5fd3..c8aaf12 100644 --- a/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/IssueTest.java +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/detector/api/IssueTest.java @@ -1,11 +1,11 @@ /* * Copyright (C) 2012 The Android Open Source Project * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * 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.eclipse.org/org/documents/epl-v10.php + * 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, @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.tools.lint.detector.api; -import static com.android.tools.lint.detector.api.Issue.convertMarkup; import static com.android.SdkConstants.AUTO_URI; +import static com.android.tools.lint.detector.api.Issue.convertMarkup; import junit.framework.TestCase; @SuppressWarnings("javadoc") |