@Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { int declaredRowCount = getInt(element, ATTR_ROW_COUNT, -1); int declaredColumnCount = getInt(element, ATTR_COLUMN_COUNT, -1); if (declaredColumnCount != -1 || declaredRowCount != -1) { for (Element child : LintUtils.getChildren(element)) { if (declaredColumnCount != -1) { int column = getInt(child, ATTR_LAYOUT_COLUMN, -1); if (column >= declaredColumnCount) { Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_COLUMN); context.report( ISSUE, node, context.getLocation(node), String.format( "Column attribute (%1$d) exceeds declared grid column count (%2$d)", column, declaredColumnCount)); } } if (declaredRowCount != -1) { int row = getInt(child, ATTR_LAYOUT_ROW, -1); if (row > declaredRowCount) { Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_ROW); context.report( ISSUE, node, context.getLocation(node), String.format( "Row attribute (%1$d) exceeds declared grid row count (%2$d)", row, declaredRowCount)); } } } } if (element.getTagName().equals(FQCN_GRID_LAYOUT_V7)) { // Make sure that we're not using android: namespace attributes where we should // be using app namespace attributes! ensureAppNamespace(context, element, ATTR_COLUMN_COUNT); ensureAppNamespace(context, element, ATTR_ORIENTATION); ensureAppNamespace(context, element, ATTR_ROW_COUNT); ensureAppNamespace(context, element, ATTR_USE_DEFAULT_MARGINS); ensureAppNamespace(context, element, "alignmentMode"); ensureAppNamespace(context, element, "columnOrderPreserved"); ensureAppNamespace(context, element, "rowOrderPreserved"); for (Element child : LintUtils.getChildren(element)) { ensureAppNamespace(context, child, ATTR_LAYOUT_COLUMN); ensureAppNamespace(context, child, ATTR_LAYOUT_COLUMN_SPAN); ensureAppNamespace(context, child, ATTR_LAYOUT_GRAVITY); ensureAppNamespace(context, child, ATTR_LAYOUT_ROW); ensureAppNamespace(context, child, ATTR_LAYOUT_ROW_SPAN); } } }
@Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { // Traverse all child elements NodeList childNodes = element.getChildNodes(); int count = childNodes.getLength(); Map<String, LayoutNode> nodes = Maps.newHashMap(); for (int i = 0; i < count; i++) { Node node = childNodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { LayoutNode ln = new LayoutNode((Element) node, i); nodes.put(ln.getNodeId(), ln); } } // Node map is populated, recalculate nodes sizes for (LayoutNode ln : nodes.values()) { ln.processNode(nodes); } for (LayoutNode right : nodes.values()) { if (!right.mLastLeft || right.skip()) { continue; } Set<LayoutNode> canGrowLeft = right.canGrowLeft(); for (LayoutNode left : nodes.values()) { if (left == right || !left.mLastRight || left.skip() || !left.sameBucket(right)) { continue; } Set<LayoutNode> canGrowRight = left.canGrowRight(); if (canGrowLeft.size() > 0 || canGrowRight.size() > 0) { canGrowRight.addAll(canGrowLeft); LayoutNode nodeToBlame = right; LayoutNode otherNode = left; if (!canGrowRight.contains(right) && canGrowRight.contains(left)) { nodeToBlame = left; otherNode = right; } context.report( ISSUE, nodeToBlame.getNode(), context.getLocation(nodeToBlame.getNode()), String.format( "`%1$s` can overlap `%2$s` if %3$s %4$s due to localized text expansion", nodeToBlame.getNodeId(), otherNode.getNodeId(), Joiner.on(", ").join(canGrowRight), canGrowRight.size() > 1 ? "grow" : "grows")); } } } }
@Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { String value = attribute.getValue(); if (!value.isEmpty() && (value.charAt(0) != '@' && value.charAt(0) != '?')) { // Make sure this is really one of the android: attributes if (!ANDROID_URI.equals(attribute.getNamespaceURI())) { return; } context.report( ISSUE, attribute, context.getLocation(attribute), String.format("[I18N] Hardcoded string \"%1$s\", should use `@string` resource", value)); } }
@Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { String value = attribute.getValue(); if (value.isEmpty() || value.trim().isEmpty()) { context.report( ISSUE, attribute, context.getLocation(attribute), "onClick attribute value cannot be empty", null); } else if (!value.equals(value.trim())) { context.report( ISSUE, attribute, context.getLocation(attribute), "There should be no whitespace around attribute values", null); } else { if (mNames == null) { mNames = new HashMap<String, Location.Handle>(); } Handle handle = context.parser.createLocationHandle(context, attribute); handle.setClientData(attribute); // Replace unicode characters with the actual value since that's how they // appear in the ASM signatures if (value.contains("\\u")) { // $NON-NLS-1$ Pattern pattern = Pattern.compile("\\\\u(\\d\\d\\d\\d)"); // $NON-NLS-1$ Matcher matcher = pattern.matcher(value); StringBuilder sb = new StringBuilder(value.length()); int remainder = 0; while (matcher.find()) { sb.append(value.substring(0, matcher.start())); String unicode = matcher.group(1); int hex = Integer.parseInt(unicode, 16); sb.append((char) hex); remainder = matcher.end(); } sb.append(value.substring(remainder)); value = sb.toString(); } mNames.put(value, handle); } }
@Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { // Record include graph such that we can look for inter-layout duplicates after the // project has been fully checked String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); if (context.getPhase() == 1) { if (!context.getProject().getReportIssues()) { // If this is a library project not being analyzed, ignore it return; } List<String> to = mIncludes.get(context.file); if (to == null) { to = new ArrayList<String>(); mIncludes.put(context.file, to); } to.add(layout); } else { assert context.getPhase() == 2; Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file); if (maps != null && !maps.isEmpty()) { for (Multimap<String, Occurrence> map : maps) { if (!maps.isEmpty()) { Collection<Occurrence> occurrences = map.get(layout); if (occurrences != null && !occurrences.isEmpty()) { for (Occurrence occurrence : occurrences) { Location location = context.getLocation(element); location.setClientData(element); location.setMessage(occurrence.message); location.setSecondary(occurrence.location); occurrence.location = location; } } } } } } } }
private static void ensureAppNamespace(XmlContext context, Element element, String name) { Attr attribute = element.getAttributeNodeNS(ANDROID_URI, name); if (attribute != null) { String prefix = getNamespacePrefix(element.getOwnerDocument(), AUTO_URI); boolean haveNamespace = prefix != null; if (!haveNamespace) { prefix = "app"; } StringBuilder sb = new StringBuilder(); sb.append("Wrong namespace; with v7 `GridLayout` you should use ") .append(prefix) .append(":") .append(name); if (!haveNamespace) { sb.append(" (and add `xmlns:app=\"").append(AUTO_URI).append("\"` to your root element.)"); } String message = sb.toString(); context.report(ISSUE, attribute, context.getLocation(attribute), message); } }
@Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID); String id = attribute.getValue(); if (context.getPhase() == 1) { if (mIds.contains(id)) { Location location = context.getLocation(attribute); Attr first = findIdAttribute(attribute.getOwnerDocument(), id); if (first != null && first != attribute) { Location secondLocation = context.getLocation(first); secondLocation.setMessage(String.format("%1$s originally defined here", id)); location.setSecondary(secondLocation); } context.report( WITHIN_LAYOUT, attribute, location, String.format("Duplicate id %1$s, already defined earlier in this layout", id), null); } else if (id.startsWith(NEW_ID_PREFIX)) { // Skip id's on include tags if (attribute.getOwnerElement().getTagName().equals(VIEW_INCLUDE)) { return; } mIds.add(id); } } else { Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file); if (maps != null && !maps.isEmpty()) { for (Multimap<String, Occurrence> map : maps) { if (!maps.isEmpty()) { Collection<Occurrence> occurrences = map.get(id); if (occurrences != null && !occurrences.isEmpty()) { for (Occurrence occurrence : occurrences) { if (context.getDriver().isSuppressed(CROSS_LAYOUT, attribute)) { return; } Location location = context.getLocation(attribute); location.setClientData(attribute); location.setMessage(occurrence.message); location.setSecondary(occurrence.location); occurrence.location = location; } } } } } } }