Java Code Examples for com.android.tools.lint.detector.api.XmlContext#report()

The following examples show how to use com.android.tools.lint.detector.api.XmlContext#report() . You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example 1
Source File: FullBackupContentDetector.java    From javaide with GNU General Public License v3.0 6 votes vote down vote up
@Nullable
private static String validateDomain(@NonNull XmlContext context, @NonNull Element element) {
    Attr domainNode = element.getAttributeNode(ATTR_DOMAIN);
    if (domainNode == null) {
        context.report(ISSUE, element, context.getLocation(element),
            String.format("Missing domain attribute, expected one of %1$s",
                    Joiner.on(", ").join(VALID_DOMAINS)));
        return null;
    }
    String domain = domainNode.getValue();
    for (String availableDomain : VALID_DOMAINS) {
        if (availableDomain.equals(domain)) {
            return domain;
        }
    }
    context.report(ISSUE, element, context.getValueLocation(domainNode),
            String.format("Unexpected domain `%1$s`, expected one of %2$s", domain,
                    Joiner.on(", ").join(VALID_DOMAINS)));

    return domain;
}
 
Example 2
Source File: WebViewDetector.java    From javaide with GNU General Public License v3.0 6 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    Node parentNode = element.getParentNode();
    if (parentNode != null && parentNode.getNodeType() == Node.ELEMENT_NODE) {
        Element parent = (Element)parentNode;
        Attr width = parent.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
        Attr height = parent.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
        Attr attr = null;
        if (width != null && VALUE_WRAP_CONTENT.equals(width.getValue())) {
            attr = width;
        }
        if (height != null && VALUE_WRAP_CONTENT.equals(height.getValue())) {
            attr = height;
        }
        if (attr != null) {
            String message = String.format("Placing a `<WebView>` in a parent element that "
                    + "uses a `wrap_content %1$s` can lead to subtle bugs; use `match_parent` "
                    + "instead", attr.getLocalName());
            Location location = context.getLocation(element);
            Location secondary = context.getLocation(attr);
            secondary.setMessage("`wrap_content` here may not work well with WebView below");
            location.setSecondary(secondary);
            context.report(ISSUE, element, location, message);
        }
    }
}
 
Example 3
Source File: NfcTechListDetector.java    From javaide with GNU General Public License v3.0 6 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    Node parentNode = element.getParentNode();
    if (parentNode == null || parentNode.getNodeType() != Node.ELEMENT_NODE ||
            !"tech-list".equals(parentNode.getNodeName())) {
        return;
    }

    NodeList children = element.getChildNodes();
    if (children.getLength() != 1) {
        return;
    }
    Node child = children.item(0);
    if (child.getNodeType() != Node.TEXT_NODE) {
        // TODO: Warn if you have comment nodes etc too? Will probably also break inflater.
        return;
    }

    String text = child.getNodeValue();
    if (!text.equals(text.trim())) {
        String message = "There should not be any whitespace inside `<tech>` elements";
        context.report(ISSUE, element, context.getLocation(child), message);
    }
}
 
Example 4
Source File: ResourcePrefixDetector.java    From javaide with GNU General Public License v3.0 6 votes vote down vote up
@Override
public void beforeCheckFile(@NonNull Context context) {
    if (mPrefix != null && context instanceof XmlContext) {
        XmlContext xmlContext = (XmlContext) context;
        ResourceFolderType folderType = xmlContext.getResourceFolderType();
        if (folderType != null && folderType != ResourceFolderType.VALUES) {
            String name = LintUtils.getBaseName(context.file.getName());
            if (!name.startsWith(mPrefix)) {
                // Attempt to report the error on the root tag of the associated
                // document to make suppressing the error with a tools:suppress
                // attribute etc possible
                if (xmlContext.document != null) {
                    Element root = xmlContext.document.getDocumentElement();
                    if (root != null) {
                        xmlContext.report(ISSUE, root, xmlContext.getLocation(root),
                                getErrorMessage(name));
                        return;
                    }
                }
                context.report(ISSUE, Location.create(context.file),
                        getErrorMessage(name));
            }
        }
    }
}
 
Example 5
Source File: TitleDetector.java    From javaide with GNU General Public License v3.0 6 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    if (element.hasAttributeNS(ANDROID_URI, ATTR_TITLE)) {
        return;
    }

    // TODO: Find out if this is necessary on older versions too.
    // I swear I saw it mentioned.
    if (context.getMainProject().getTargetSdk() < 11) {
        return;
    }

    if (VALUE_FALSE.equals(element.getAttributeNS(ANDROID_URI, ATTR_VISIBLE))) {
        return;
    }

    String message = "Menu items should specify a `title`";
    context.report(ISSUE, element, context.getLocation(element), message);
}
 
Example 6
Source File: HardcodedTextDetectorModified.java    From lewis with Apache License 2.0 5 votes vote down vote up
@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));
    }
}
 
Example 7
Source File: PreferStubViewDetector.java    From Folivora with Apache License 2.0 5 votes vote down vote up
@Override
public void visitAttribute(@NotNull XmlContext context, @NotNull Attr attribute) {
  Element tag = attribute.getOwnerElement();
  String tagName = tag.getTagName();
  if (sSystemViewNames.contains(tagName)) {
    LintFix fix = LintFix.create().replace().range(context.getLocation(tag))
      .text(tagName).with("cn.cricin.folivora.view." + tagName).build();
    context.report(ISSUE, context.getLocation(tag),
      "Using cn.cricin.folivora.view." + tagName + " instead to support design time preview", fix);
  }
}
 
Example 8
Source File: MissingIdDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID) &&
            !element.hasAttributeNS(ANDROID_URI, ATTR_TAG)) {
        context.report(ISSUE, element, context.getLocation(element),
            "This `<fragment>` tag should specify an id or a tag to preserve state " +
            "across activity restarts");
    }
}
 
Example 9
Source File: WrongCaseDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    String tag = element.getTagName();
    String correct = Character.toLowerCase(tag.charAt(0)) + tag.substring(1);
    context.report(WRONG_CASE, element, context.getLocation(element),
            String.format("Invalid tag `<%1$s>`; should be `<%2$s>`", tag, correct));
}
 
Example 10
Source File: PreferenceActivityDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    if (SecurityDetector.getExported(element)) {
        String fqcn = getFqcn(element);
        if (fqcn != null) {
            if (fqcn.equals(PREFERENCE_ACTIVITY) &&
                    !context.getDriver().isSuppressed(context, ISSUE, element)) {
                String message = "`PreferenceActivity` should not be exported";
                context.report(ISSUE, element, context.getLocation(element), message);
            }
            mExportedActivities.put(fqcn, context.createLocationHandle(element));
        }
    }
}
 
Example 11
Source File: SignatureOrSystemDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
@Override
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
    String protectionLevel = attribute.getValue();
    if (protectionLevel != null
        && protectionLevel.equals(SIGNATURE_OR_SYSTEM)) {
        String message = "`protectionLevel` should probably not be set to `signatureOrSystem`";
        context.report(ISSUE, attribute, context.getLocation(attribute), message);
    }
}
 
Example 12
Source File: TextFieldDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
private static void reportMismatch(XmlContext context, Attr idNode, Node inputTypeNode,
        String message) {
    Location location;
    if (inputTypeNode != null) {
        location = context.getLocation(inputTypeNode);
        Location secondary = context.getLocation(idNode);
        secondary.setMessage("id defined here");
        location.setSecondary(secondary);
    } else {
        location = context.getLocation(idNode);
    }
    context.report(ISSUE, idNode.getOwnerElement(), location, message);
}
 
Example 13
Source File: UseCompoundDrawableDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull 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)))) {
            // If the layout has a background, ignore since it would disappear from
            // the TextView
            if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) {
                return;
            }

            // Certain scale types cannot be done with compound drawables
            String scaleType = first.getTagName().equals(IMAGE_VIEW)
                    ? first.getAttributeNS(ANDROID_URI, ATTR_SCALE_TYPE)
                    : second.getAttributeNS(ANDROID_URI, ATTR_SCALE_TYPE);
            if (scaleType != null && !scaleType.isEmpty()) {
                // For now, ignore if any scale type is set
                return;
            }

            context.report(ISSUE, element, context.getLocation(element),
                    "This tag and its children can be replaced by one `<TextView/>` and " +
                            "a compound drawable");
        }
    }
}
 
Example 14
Source File: Utf8Detector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
    String xml = context.getContents();
    if (xml == null) {
        return;
    }

    // AAPT: The prologue must be in the first line
    int lineEnd = 0;
    int max = xml.length();
    for (; lineEnd < max; lineEnd++) {
        char c = xml.charAt(lineEnd);
        if (c == '\n' || c == '\r') {
            break;
        }
    }

    for (int i = 16; i < lineEnd - 5; i++) { // +4: Skip at least <?xml encoding="
        if ((xml.charAt(i) == 'u' || xml.charAt(i) == 'U')
                && (xml.charAt(i + 1) == 't' || xml.charAt(i + 1) == 'T')
                && (xml.charAt(i + 2) == 'f' || xml.charAt(i + 2) == 'F')
                && (xml.charAt(i + 3) == '-' || xml.charAt(i + 3) == '_')
                && (xml.charAt(i + 4) == '8')) {
            return;
        }
    }

    int encodingIndex = xml.lastIndexOf("encoding", lineEnd); //$NON-NLS-1$
    if (encodingIndex != -1) {
        Matcher matcher = ENCODING_PATTERN.matcher(xml);
        if (matcher.find(encodingIndex)) {
            String encoding = matcher.group(1);
            Location location = Location.create(context.file, xml,
                    matcher.start(1), matcher.end(1));
            context.report(ISSUE, null, location, String.format(
                    "%1$s: Not using UTF-8 as the file encoding. This can lead to subtle " +
                            "bugs with non-ascii characters", encoding));
        }
    }
}
 
Example 15
Source File: TypoDetector.java    From javaide with GNU General Public License v3.0 5 votes vote down vote up
/** Reports a repeated word */
private static void reportRepeatedWord(XmlContext context, Node node, String text,
        int lastWordBegin,
        int begin, int end) {
    String message = String.format(
            "Repeated word \"%1$s\" in message: possible typo",
            text.substring(begin, end));
    Location location = context.getLocation(node, lastWordBegin, end);
    context.report(ISSUE, node, location, message);
}
 
Example 16
Source File: AppIndexingApiDetector.java    From javaide with GNU General Public License v3.0 4 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element intent) {
    boolean actionView = hasActionView(intent);
    boolean browsable = isBrowsable(intent);
    boolean isHttp = false;
    boolean hasScheme = false;
    boolean hasHost = false;
    boolean hasPort = false;
    boolean hasPath = false;
    boolean hasMimeType = false;
    Element firstData = null;
    NodeList children = intent.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(NODE_DATA)) {
            Element data = (Element) child;
            if (firstData == null) {
                firstData = data;
            }
            if (isHttpSchema(data)) {
                isHttp = true;
            }
            checkSingleData(context, data);

            for (String name : PATH_ATTR_LIST) {
                if (data.hasAttributeNS(ANDROID_URI, name)) {
                    hasPath = true;
                }
            }

            if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
                hasScheme = true;
            }

            if (data.hasAttributeNS(ANDROID_URI, ATTR_HOST)) {
                hasHost = true;
            }

            if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
                hasPort = true;
            }

            if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_MIME_TYPE)) {
                hasMimeType = true;
            }
        }
    }

    // In data field, a URL is consisted by
    // <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
    // Each part of the URL should not have illegal character.
    if ((hasPath || hasHost || hasPort) && !hasScheme) {
        context.report(ISSUE_ERROR, firstData, context.getLocation(firstData),
                "android:scheme missing");
    }

    if ((hasPath || hasPort) && !hasHost) {
        context.report(ISSUE_ERROR, firstData, context.getLocation(firstData),
                "android:host missing");
    }

    if (actionView && browsable) {
        if (firstData == null) {
            // If this activity is an ACTION_VIEW action with category BROWSABLE, but doesn't
            // have data node, it may be a mistake and we will report error.
            context.report(ISSUE_ERROR, intent, context.getLocation(intent),
                    "Missing data node?");
        } else if (!hasScheme && !hasMimeType) {
            // If this activity is an action view, is browsable, but has neither a
            // URL nor mimeType, it may be a mistake and we will report error.
            context.report(ISSUE_ERROR, firstData, context.getLocation(firstData),
                    "Missing URL for the intent filter?");
        }
    }

    // If this activity is an ACTION_VIEW action, has a http URL but doesn't have
    // BROWSABLE, it may be a mistake and and we will report warning.
    if (actionView && isHttp && !browsable) {
        context.report(ISSUE_WARNING, intent, context.getLocation(intent),
                "Activity supporting ACTION_VIEW is not set as BROWSABLE");
    }
}
 
Example 17
Source File: LauncherActivityDetector.java    From lewis with Apache License 2.0 4 votes vote down vote up
/**
 * Returns true if the XML node is an activity with a launcher intent.
 *
 * @param node is the node to check.
 * @return true if the node is an activity with a launcher intent, false if not.
 */
private boolean isMainActivity(XmlContext context, Node node) {

    if (TAG_APPLICATION.equals(node.getNodeName())) {
        mApplicationTagLocation = context.getLocation(node);
    }

    if (TAG_ACTIVITY.equals(node.getNodeName())) {

        mHasActivity = true;

        for (Element activityChild : LintUtils.getChildren(node)) {
            if (TAG_INTENT_FILTER.equals(activityChild.getNodeName())) {

                boolean hasLauncherCategory = false;
                boolean hasMainAction = false;

                for (Element intentFilterChild : LintUtils.getChildren(activityChild)) {
                    // Check for category tag)
                    if (NODE_CATEGORY.equals(intentFilterChild.getNodeName())
                            && Constants.CATEGORY_NAME_LAUNCHER.equals(
                            intentFilterChild.getAttributeNS(ANDROID_URI, ATTR_NAME))) {
                        hasLauncherCategory = true;
                    }
                    // Check for action tag
                    if (NODE_ACTION.equals(intentFilterChild.getNodeName())
                            && Constants.ACTION_NAME_MAIN.equals(
                            intentFilterChild.getAttributeNS(ANDROID_URI, ATTR_NAME))) {
                        hasMainAction = true;
                    }
                }

                if (hasLauncherCategory && hasMainAction) {
                    if (mHasLauncherActivity) {
                        context.report(ISSUE_MORE_THAN_ONE_LAUNCHER, context.getLocation(node),
                                "Expecting " + ANDROID_MANIFEST_XML + " to have only one activity with a launcher intent.");
                    }

                    // if it is a library
                    if (context.getProject() == context.getMainProject() && context.getMainProject().isLibrary()) {
                        context.report(ISSUE_LAUNCHER_ACTIVITY_IN_LIBRARY, context.getLocation(node),
                                "Expecting " + ANDROID_MANIFEST_XML + " not to have an activity with a launcher intent.");
                    }

                    return true;
                }
            }
        }
    }
    return false;
}
 
Example 18
Source File: InefficientWeightDetector.java    From javaide with GNU General Public License v3.0 4 votes vote down vote up
private static void checkWrong0Dp(XmlContext context, Element element,
                                  List<Element> children) {
    boolean isVertical = false;
    String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
    if (VALUE_VERTICAL.equals(orientation)) {
        isVertical = true;
    }

    for (Element child : children) {
        String tagName = child.getTagName();
        if (tagName.equals(VIEW)) {
            // Might just used for spacing
            return;
        }
        if (tagName.indexOf('.') != -1 || tagName.equals(VIEW_TAG)) {
            // Custom views might perform their own dynamic sizing or ignore the layout
            // attributes all together
            return;
        }

        boolean hasWeight = child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT);

        Attr widthNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
        Attr heightNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);

        boolean noWidth = false;
        boolean noHeight = false;
        if (widthNode != null && widthNode.getValue().startsWith("0")) { //$NON-NLS-1$
            noWidth = true;
        }
        if (heightNode != null && heightNode.getValue().startsWith("0")) { //$NON-NLS-1$
            noHeight = true;
        } else if (!noWidth) {
            return;
        }

        // If you're specifying 0dp for both the width and height you are probably
        // trying to hide it deliberately
        if (noWidth && noHeight) {
            return;
        }

        if (noWidth) {
            if (!hasWeight) {
                context.report(WRONG_0DP, widthNode, context.getLocation(widthNode),
                    "Suspicious size: this will make the view invisible, should be " +
                    "used with `layout_weight`");
            } else if (isVertical) {
                context.report(WRONG_0DP, widthNode, context.getLocation(widthNode),
                    "Suspicious size: this will make the view invisible, probably " +
                    "intended for `layout_height`");
            }
        } else {
            if (!hasWeight) {
                context.report(WRONG_0DP, widthNode, context.getLocation(heightNode),
                    "Suspicious size: this will make the view invisible, should be " +
                    "used with `layout_weight`");
            } else if (!isVertical) {
                context.report(WRONG_0DP, widthNode, context.getLocation(heightNode),
                    "Suspicious size: this will make the view invisible, probably " +
                    "intended for `layout_width`");
            }
        }
    }
}
 
Example 19
Source File: TextViewDetector.java    From javaide with GNU General Public License v3.0 4 votes vote down vote up
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    if (element.getTagName().equals(TEXT_VIEW)) {
        if (!element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)
                && element.hasAttributeNS(ANDROID_URI, ATTR_ID)
                && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT_IS_SELECTABLE)
                && !element.hasAttributeNS(ANDROID_URI, ATTR_VISIBILITY)
                && !element.hasAttributeNS(ANDROID_URI, ATTR_ON_CLICK)
                && context.getMainProject().getTargetSdk() >= 11
                && context.isEnabled(SELECTABLE)) {
            context.report(SELECTABLE, element, context.getLocation(element),
                    "Consider making the text value selectable by specifying " +
                    "`android:textIsSelectable=\"true\"`");
        }
    }

    NamedNodeMap attributes = element.getAttributes();
    for (int i = 0, n = attributes.getLength(); i < n; i++) {
        Attr attribute = (Attr) attributes.item(i);
        String name = attribute.getLocalName();
        if (name == null || name.isEmpty()) {
            // Attribute not in a namespace; we only care about the android: ones
            continue;
        }

        boolean isEditAttribute = false;
        switch (name.charAt(0)) {
            case 'a': {
                isEditAttribute = name.equals(ATTR_AUTO_TEXT);
                break;
            }
            case 'b': {
                isEditAttribute = name.equals(ATTR_BUFFER_TYPE) &&
                        attribute.getValue().equals(VALUE_EDITABLE);
                break;
            }
            case 'p': {
                isEditAttribute = name.equals(ATTR_PASSWORD)
                        || name.equals(ATTR_PHONE_NUMBER)
                        || name.equals(ATTR_PRIVATE_IME_OPTIONS);
                break;
            }
            case 'c': {
                isEditAttribute = name.equals(ATTR_CAPITALIZE)
                        || name.equals(ATTR_CURSOR_VISIBLE);
                break;
            }
            case 'd': {
                isEditAttribute = name.equals(ATTR_DIGITS);
                break;
            }
            case 'e': {
                if (name.equals(ATTR_EDITABLE)) {
                    isEditAttribute = attribute.getValue().equals(VALUE_TRUE);
                } else {
                    isEditAttribute = name.equals(ATTR_EDITOR_EXTRAS);
                }
                break;
            }
            case 'i': {
                if (name.equals(ATTR_INPUT_TYPE)) {
                    String value = attribute.getValue();
                    isEditAttribute = !value.isEmpty() && !value.equals(VALUE_NONE);
                } else {
                    isEditAttribute = name.equals(ATTR_INPUT_TYPE)
                            || name.equals(ATTR_IME_OPTIONS)
                            || name.equals(ATTR_IME_ACTION_LABEL)
                            || name.equals(ATTR_IME_ACTION_ID)
                            || name.equals(ATTR_INPUT_METHOD);
                }
                break;
            }
            case 'n': {
                isEditAttribute = name.equals(ATTR_NUMERIC);
                break;
            }
        }

        if (isEditAttribute && ANDROID_URI.equals(attribute.getNamespaceURI()) && context.isEnabled(ISSUE)) {
            Location location = context.getLocation(attribute);
            String message;
            String view = element.getTagName();
            if (view.equals(TEXT_VIEW)) {
                message = String.format(
                        "Attribute `%1$s` should not be used with `<TextView>`: " +
                        "Change element type to `<EditText>` ?", attribute.getName());
            } else {
                message = String.format(
                        "Attribute `%1$s` should not be used with `<%2$s>`: " +
                        "intended for editable text widgets",
                        attribute.getName(), view);
            }
            context.report(ISSUE, attribute, location, message);
        }
    }
}
 
Example 20
Source File: TypoDetector.java    From javaide with GNU General Public License v3.0 4 votes vote down vote up
/** Report the typo found at the given offset and suggest the given replacements */
private static void reportTypo(XmlContext context, Node node, String text, int begin,
        List<String> replacements) {
    if (replacements.size() < 2) {
        return;
    }

    String typo = replacements.get(0);
    String word = text.substring(begin, begin + typo.length());

    String first = null;
    String message;

    boolean isCapitalized = Character.isUpperCase(word.charAt(0));
    StringBuilder sb = new StringBuilder(40);
    for (int i = 1, n = replacements.size(); i < n; i++) {
        String replacement = replacements.get(i);
        if (first == null) {
            first = replacement;
        }
        if (sb.length() > 0) {
            sb.append(" or ");
        }
        sb.append('"');
        if (isCapitalized) {
            sb.append(Character.toUpperCase(replacement.charAt(0)));
            sb.append(replacement.substring(1));
        } else {
            sb.append(replacement);
        }
        sb.append('"');
    }

    if (first != null && first.equalsIgnoreCase(word)) {
        if (first.equals(word)) {
            return;
        }
        message = String.format(
                "\"%1$s\" is usually capitalized as \"%2$s\"",
                word, first);
    } else {
        message = String.format(
                "\"%1$s\" is a common misspelling; did you mean %2$s ?",
                word, sb.toString());
    }

    int end = begin + word.length();
    context.report(ISSUE, node, context.getLocation(node, begin, end), message);
}