@Override public void visitElement(Context context, Element element) { context.client.report(context, ISSUE, context.getLocation(element), String.format("%1$s is deprecated", element.getTagName()), null); } }
@Override public void visitAttribute(Context context, Attr attribute) { String value = attribute.getValue(); if (value.length() > 0 && (value.charAt(0) != '@' && value.charAt(0) != '?')) { // Make sure this is really one of the android: attributes if (!ANDROID_URI.equals(attribute.getNamespaceURI())) { return; } context.client.report(context, ISSUE, context.getLocation(attribute), String.format("[I18N] Hardcoded string \"%1$s\", should use @string resource", value), null); } } }
@Override public void visitAttribute(Context context, Attr attribute) { String value = attribute.getValue(); if (value.endsWith("px") && value.matches("\\d+px")) { //$NON-NLS-1$ if (value.charAt(0) == '0') { // 0px is fine. 0px is 0dp regardless of density... return; } context.client.report(context, ISSUE, context.getLocation(attribute), "Avoid using \"px\" as units; use \"dp\" instead", null); } } }
@Override public void visitAttribute(Context context, Attr attribute) { assert attribute.getLocalName().equals(ATTR_ID); String id = attribute.getValue(); if (mIds.contains(id)) { context.client.report(context, WITHIN_LAYOUT, context.getLocation(attribute), String.format("Duplicate id %1$s, already defined earlier in this layout", id), null); } else if (id.startsWith("@+id/")) { //$NON-NLS-1$ mIds.add(id); } } }
@Override public void visitElement(Context context, Element element) { String tag = element.getTagName(); if (tag.equals(TAG_APPLICATION)) { mSeenApplication = true; } else if (mSeenApplication) { context.client.report(context, ISSUE, context.getLocation(element), String.format("<%1$s> tag appears after <application> tag", tag), null); // Don't complain for *every* element following the <application> tag mSeenApplication = false; } } }
@Override public void visitElement(Context context, Element element) { mViewCount++; mDepth++; if (mDepth == MAX_DEPTH && !mWarnedAboutDepth) { // Have to record whether or not we've warned since we could have many siblings // at the max level and we'd warn for each one. No need to do the same thing // for the view count error since we'll only have view count exactly equal the // max just once. mWarnedAboutDepth = true; String msg = String.format("%1$s has more than %2$d levels, bad for performance", context.file.getName(), MAX_DEPTH); context.client.report(context, TOO_DEEP, context.getLocation(element), msg, null); } if (mViewCount == MAX_VIEW_COUNT) { String msg = String.format("%1$s has more than %2$d views, bad for performance", context.file.getName(), MAX_VIEW_COUNT); context.client.report(context, TOO_MANY, context.getLocation(element), msg, null); } }
@Override public void visitElement(Context context, Element element) { Attr attribute = element.getAttributeNode(ATTR_NAME); if (attribute == null || attribute.getValue().length() == 0) { context.client.report(context, INCONSISTENT, context.getLocation(element), String.format("Missing name attribute in %1$s declaration", element.getTagName()), null); } else { String name = attribute.getValue(); int childCount = LintUtils.getChildCount(element); mFileToArrayCount.put(context.file, Pair.of(name, childCount)); } } }
@Override public void visitElement(Context context, Element element) { if (!element.hasAttributeNS(ANDROID_URI, ATTR_CONTENT_DESCRIPTION)) { context.client.report(context, ISSUE, context.getLocation(element), "[Accessibility] Missing contentDescription attribute on image", null); } else { Attr attributeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_CONTENT_DESCRIPTION); String attribute = attributeNode.getValue(); if (attribute.length() == 0 || attribute.equals("TODO")) { //$NON-NLS-1$ context.client.report(context, ISSUE, context.getLocation(attributeNode), "[Accessibility] Empty contentDescription attribute on image", null); } } } }
@Override public void visitElement(Context context, Element element) { List<Element> children = LintUtils.getChildren(element); boolean isHorizontal = HORIZONTAL_SCROLL_VIEW.equals(element.getTagName()); String attributeName = isHorizontal ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT; for (Element child : children) { Attr sizeNode = child.getAttributeNodeNS(ANDROID_URI, attributeName); String value = sizeNode != null ? sizeNode.getValue() : null; if (VALUE_FILL_PARENT.equals(value) || VALUE_MATCH_PARENT.equals(value)) { String msg = String.format("This %1$s should use android:%2$s=\"wrap_content\"", child.getTagName(), attributeName); context.client.report(context, ISSUE, context.getLocation(sizeNode), msg, null); } } } }
@Override public void run(Context context) { String contents = context.getContents(); if (contents != null) { int index = contents.indexOf( // Old pattern: "-keepclasseswithmembernames class * {\n" + //$NON-NLS-1$ " public <init>(android."); //$NON-NLS-1$ if (index != -1) { context.client.report(context, ISSUE, context.getLocation(context), "Obsolete proguard file; use -keepclasseswithmembers instead of -keepclasseswithmembernames", null); } } }
private void checkGrantPermission(Context context, Element element) { Attr path = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH); Attr prefix = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PREFIX); Attr pattern = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PATTERN); String msg = "Content provider shares everything; this is potentially dangerous."; if (path != null && path.getValue().equals("/")) { //$NON-NLS-1$ context.client.report(context, OPEN_PROVIDER, context.getLocation(path), msg, null); } if (prefix != null && prefix.getValue().equals("/")) { //$NON-NLS-1$ context.client.report(context, OPEN_PROVIDER, context.getLocation(prefix), msg, null); } if (pattern != null && (pattern.getValue().equals("/") //$NON-NLS-1$ /* || pattern.getValue().equals(".*")*/)) { context.client.report(context, OPEN_PROVIDER, context.getLocation(pattern), msg, null); } } }
@Override public void visitAttribute(Context context, Attr attribute) { String uri = attribute.getNamespaceURI(); if (uri == null || uri.length() == 0) { String name = attribute.getName(); if (name == null) { return; } if (NO_PREFIX_ATTRS.contains(name)) { return; } Element element = attribute.getOwnerElement(); if (isCustomView(element)) { return; } if (name.startsWith(XMLNS_PREFIX)) { return; } context.client.report(context, MISSING_NAMESPACE, context.getLocation(attribute), "Attribute is missing the Android namespace prefix", null); } }
@Override public void visitElement(Context context, Element element) { int childCount = LintUtils.getChildCount(element); if (childCount == 2) { List<Element> children = LintUtils.getChildren(element); Element first = children.get(0); Element second = children.get(1); if ((first.getTagName().equals(IMAGE_VIEW) && second.getTagName().equals(TEXT_VIEW) && !first.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) || ((second.getTagName().equals(IMAGE_VIEW) && first.getTagName().equals(TEXT_VIEW) && !second.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)))) { context.client.report(context, ISSUE, context.getLocation(element), "This tag and its children can be replaced by one <TextView/> and " + "a compound drawable", null); } } } }
@Override public void visitElement(Context context, Element element) { int childCount = LintUtils.getChildCount(element); String tagName = element.getTagName(); if (tagName.equals(SCROLL_VIEW) || tagName.equals(HORIZONTAL_SCROLL_VIEW)) { if (childCount > 1 && getAccurateChildCount(element) > 1) { context.client.report(context, SCROLLVIEW_ISSUE, context.getLocation(element), "A scroll view can have only one child", null); } } else { // Adapter view if (childCount > 0 && getAccurateChildCount(element) > 0) { context.client.report(context, ADAPTERVIEW_ISSUE, context.getLocation(element), "A list/grid should have no children declared in XML", null); } } }
@Override public void afterCheckProject(Context context) { if (mRootAttributes != null) { for (Pair<Location, String> pair : mRootAttributes) { Location location = pair.getFirst(); String layoutName = location.getFile().getName(); if (endsWith(layoutName, DOT_XML)) { layoutName = layoutName.substring(0, layoutName.length() - DOT_XML.length()); } String theme = getTheme(context, layoutName); if (theme == null || !isBlankTheme(theme)) { String drawable = pair.getSecond(); String message = String.format( "Possible overdraw: Root element paints background %1$s with " + "a theme that also paints a background (inferred theme is %2$s)", drawable, theme); context.client.report(context, ISSUE, location, message, null); } } } }
private void checkUselessLeaf(Context context, Element element) { assert LintUtils.getChildCount(element) == 0; // Conditions: // - The node is a container view (LinearLayout, etc.) // - The node has no id // - The node has no background // - The node has no children if (element.hasAttributeNS(ANDROID_URI, ATTR_ID)) { return; } if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) { return; } Location location = context.getLocation(element); String tag = element.getTagName(); String message = String.format( "This %1$s view is useless (no children, no background, no id)", tag); context.client.report(context, USELESS_LEAF, location, message, null); } }
@Override public void visitDocument(Context context, Document document) { Element root = document.getDocumentElement(); if (root.getTagName().equals(FRAME_LAYOUT) && ((isWidthFillParent(root) && isHeightFillParent(root)) || !root.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY)) && !root.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND) && !root.hasAttributeNS(ANDROID_URI, ATTR_FOREGROUND) && !hasPadding(root)) { context.client.report(context, ISSUE, context.getLocation(root), "This <FrameLayout> can be replaced with a <merge> tag", null); } } }
private void formatError(String message, Object... args) { if (args != null && args.length > 0) { message = String.format(message, args); } message = "Failed to parse `lint.xml` configuration file: " + message; LintDriver driver = new LintDriver(new IssueRegistry() { @Override @NonNull public List<Issue> getIssues() { return Collections.emptyList(); } }, client); client.report(new Context(driver, project, project, configFile), IssueRegistry.LINT_ERROR, project.getConfiguration(driver).getSeverity(IssueRegistry.LINT_ERROR), Location.create(configFile), message, TextFormat.RAW); }
@Override public void visitElement(Context context, Element element) { if (!element.hasAttributeNS(ANDROID_URI, ATTR_INPUT_TYPE) && !element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) { // Also make sure the EditText does not set an inputMethod in which case // an inputType might be provided from the input. if (element.hasAttributeNS(ANDROID_URI, ATTR_INPUT_METHOD)) { return; } context.client.report(context, ISSUE, context.getLocation(element), "This text field does not specify an inputType or a hint", null); } } }
@Override public void report(Context context, Issue issue, Location location, String message, Object data) { Configuration configuration = context.configuration; if (!configuration.isEnabled(issue)) { if (issue != IssueRegistry.PARSER_ERROR) { mDelegate.log(null, "Incorrect detector reported disabled issue %1$s", issue.toString()); } return; } if (configuration.isIgnored(context, issue, location, message, data)) { return; } Severity severity = configuration.getSeverity(issue); if (severity == Severity.IGNORE) { return; } mDelegate.report(context, issue, location, message, data); }