aboutsummaryrefslogtreecommitdiffstats
path: root/lint
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2011-11-21 12:18:04 -0800
committerTor Norbye <tnorbye@google.com>2011-11-23 13:16:32 -0800
commit1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f (patch)
tree0a7da811f5d438f05ff0be6faecde90c87b44f5c /lint
parent4f14da7e65c56041ea328e0e018316ec822307bc (diff)
downloadsdk-1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f.zip
sdk-1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f.tar.gz
sdk-1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f.tar.bz2
Typography lint checker
This changeset adds a new typography detector. This looks at the strings defined by the application and makes various suggestions to make the text look better typographically, such as - suggesting directional single and double quotes instead of straight single or or double quotes - suggesting typographical apostrophes instead of straight ones - suggesting n-dashes instead of hyphens in number ranges - suggesting m-dashes instead of --'s - suggesting fractional characters instead of strings like 1/2 - suggesting the ellipsis character instead of "..." - suggesting the copyright symbol instead of (c) There is also an Eclipse quick fix for these issues. Change-Id: If3912f9d34841faf069174f291911f73e6b78c28
Diffstat (limited to 'lint')
-rw-r--r--lint/cli/src/com/android/tools/lint/HtmlReporter.java9
-rw-r--r--lint/cli/src/com/android/tools/lint/Main.java4
-rw-r--r--lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java2
-rw-r--r--lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java5
-rw-r--r--lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java2
-rw-r--r--lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java485
-rw-r--r--lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs3
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java106
-rw-r--r--lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml29
9 files changed, 641 insertions, 4 deletions
diff --git a/lint/cli/src/com/android/tools/lint/HtmlReporter.java b/lint/cli/src/com/android/tools/lint/HtmlReporter.java
index 39e2f80..d00c406 100644
--- a/lint/cli/src/com/android/tools/lint/HtmlReporter.java
+++ b/lint/cli/src/com/android/tools/lint/HtmlReporter.java
@@ -352,7 +352,7 @@ class HtmlReporter extends Reporter {
String explanation = issue.getExplanation();
explanation = explanation.replace("\n", "<br/>"); //$NON-NLS-1$ //$NON-NLS-2$
explanation = Main.wrap(explanation);
- mWriter.write(explanation);
+ appendEscapedText(explanation);
mWriter.write("\n</div>\n"); //$NON-NLS-1$;
if (issue.getMoreInfo() != null) {
mWriter.write("<div class=\"moreinfo\">"); //$NON-NLS-1$
@@ -505,7 +505,12 @@ class HtmlReporter extends Reporter {
} else if (c == '&') {
mWriter.write("&amp;"); //$NON-NLS-1$
} else {
- mWriter.write(c);
+ if (c > 255) {
+ mWriter.write("&#"); //$NON-NLS-1$
+ mWriter.write(Integer.toString(c));
+ } else {
+ mWriter.write(c);
+ }
}
}
}
diff --git a/lint/cli/src/com/android/tools/lint/Main.java b/lint/cli/src/com/android/tools/lint/Main.java
index 5438712..2453ad5 100644
--- a/lint/cli/src/com/android/tools/lint/Main.java
+++ b/lint/cli/src/com/android/tools/lint/Main.java
@@ -168,10 +168,12 @@ public class Main extends LintClient {
if (issue.getCategory().getName().startsWith(category) ||
issue.getCategory().getFullName().startsWith(category)) {
describeIssue(issue);
+ System.out.println();
}
}
} else if (registry.isIssueId(id)) {
describeIssue(registry.getIssue(id));
+ System.out.println();
} else {
System.err.println("Invalid id or category \"" + id + "\".\n");
displayValidIds(registry, System.err);
@@ -479,7 +481,7 @@ public class Main extends LintClient {
System.out.println(wrap(issue.getExplanation()));
}
if (issue.getMoreInfo() != null) {
- System.out.println("\nMore information: " + issue.getMoreInfo());
+ System.out.println("More information: " + issue.getMoreInfo());
}
}
diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java
index 41eac96..88df665 100644
--- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java
+++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java
@@ -136,4 +136,6 @@ public final class Category implements Comparable<Category> {
// Sub categories
/** Issues related to icons */
public static final Category ICONS = Category.create(USABILITY, "Icons", null, 7);
+ /** Issues related to typography */
+ public static final Category TYPOGRAPHY = Category.create(USABILITY, "Typography", null, 8);
}
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 946c1b6..d6b1d44 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
@@ -93,6 +93,11 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(IconDetector.DUPLICATES_NAMES);
issues.add(IconDetector.DUPLICATES_CONFIGURATIONS);
issues.add(IconDetector.ICON_NODPI);
+ issues.add(TypographyDetector.DASHES);
+ issues.add(TypographyDetector.QUOTES);
+ issues.add(TypographyDetector.FRACTIONS);
+ issues.add(TypographyDetector.ELLIPSIS);
+ issues.add(TypographyDetector.OTHER);
issues.add(DetectMissingPrefix.MISSING_NAMESPACE);
addCustomIssues(issues);
diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java
index 7fa2ae4..e1f923e 100644
--- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java
+++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java
@@ -52,7 +52,7 @@ public class StateListDetector extends ResourceXmlDetector {
/** Constructs a new {@link StateListDetector} */
public StateListDetector() {
- };
+ }
@Override
public boolean appliesTo(ResourceFolderType folderType) {
diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java
new file mode 100644
index 0000000..01cb6bd
--- /dev/null
+++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2011 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.tools.lint.detector.api.LintConstants.TAG_STRING;
+import static com.android.tools.lint.detector.api.LintConstants.TAG_STRING_ARRAY;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.resources.ResourceFolderType;
+import com.android.tools.lint.client.api.Configuration;
+import com.android.tools.lint.detector.api.Category;
+import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.Issue;
+import com.android.tools.lint.detector.api.ResourceXmlDetector;
+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 org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Checks for various typographical issues in string definitions.
+ */
+public class TypographyDetector extends ResourceXmlDetector {
+ /** Replace hyphens with dashes? */
+ public static final Issue DASHES = Issue.create(
+ "TypographyDashes", //$NON-NLS-1$
+ "Looks for usages of hyphens which can be replaced by n dash and m dash characters",
+ "The \"n dash\" (\u2013, \\u2013) and the \"m dash\" (\u2014, \\u2014) " +
+ "characters are used for ranges (n dash) and breaks (m dash). Using these " +
+ "instead of plain hyphens can make text easier to read and your application " +
+ "will look more polished.",
+ Category.TYPOGRAPHY,
+ 5,
+ Severity.WARNING,
+ TypographyDetector.class,
+ Scope.RESOURCE_FILE_SCOPE).
+ setMoreInfo("http://en.wikipedia.org/wiki/Dash"); //$NON-NLS-1$
+
+ /** Replace dumb quotes with smart quotes? */
+ public static final Issue QUOTES = Issue.create(
+ "TypographyQuotes", //$NON-NLS-1$
+ "Looks for straight quotes which can be replaced by curvy quotes",
+ "Straight single quotes and double quotes, when used as a pair, can be replaced " +
+ "by \"curvy quotes\" (or directional quotes). This can make the text more " +
+ "readable.\n" +
+ "\n" +
+ "Note that you should never use grave accents and apostrophes to quote, " +
+ "`like this'.\n" +
+ "\n" +
+ "(Also note that you should not use curvy quotes for code fragments.)",
+ Category.TYPOGRAPHY,
+ 5,
+ Severity.WARNING,
+ TypographyDetector.class,
+ Scope.RESOURCE_FILE_SCOPE).
+ setMoreInfo("http://en.wikipedia.org/wiki/Quotation_mark"). //$NON-NLS-1$
+ // This feature is apparently controversial: recent apps have started using
+ // straight quotes to avoid inconsistencies. Disabled by default for now.
+ setEnabledByDefault(false);
+
+ /** Replace fraction strings with fraction characters? */
+ public static final Issue FRACTIONS = Issue.create(
+ "TypographyFractions", //$NON-NLS-1$
+ "Looks for fraction strings which can be replaced with a fraction character",
+ "You can replace certain strings, such as 1/2, and 1/4, with dedicated " +
+ "characters for these, such as \u00BD (\\u00Bd) and \00BC (\\u00Bc). " +
+ "This can help make the text more readable.",
+ Category.TYPOGRAPHY,
+ 5,
+ Severity.WARNING,
+ TypographyDetector.class,
+ Scope.RESOURCE_FILE_SCOPE).
+ setMoreInfo("http://en.wikipedia.org/wiki/Number_Forms"); //$NON-NLS-1$
+
+ /** Replace ... with the ellipsis character? */
+ public static final Issue ELLIPSIS = Issue.create(
+ "TypographyEllipsis", //$NON-NLS-1$
+ "Looks for ellipsis strings (...) which can be replaced with an ellipsis character",
+ "You can replace the string \"...\" with a dedicated ellipsis character, " +
+ "ellipsis character (\u2026, \\u2026). This can help make the text more readable.",
+ Category.TYPOGRAPHY,
+ 5,
+ Severity.WARNING,
+ TypographyDetector.class,
+ Scope.RESOURCE_FILE_SCOPE).
+ setMoreInfo("http://en.wikipedia.org/wiki/Ellipsis"); //$NON-NLS-1$
+
+ /** The main issue discovered by this detector */
+ public static final Issue OTHER = Issue.create(
+ "TypographyOther", //$NON-NLS-1$
+ "Looks for miscellaneous typographical problems like replacing (c) with \u00A9",
+ "This check looks for miscellaneous typographical problems and offers replacement " +
+ "sequences that will make the text easier to read and your application more " +
+ "polished.",
+ Category.TYPOGRAPHY,
+ 3,
+ Severity.WARNING,
+ TypographyDetector.class,
+ Scope.RESOURCE_FILE_SCOPE);
+
+ private static final String GRAVE_QUOTE_MESSAGE =
+ "Avoid quoting with grave accents; use apostrophes or better yet directional quotes instead";
+ private static final String ELLIPSIS_MESSAGE =
+ "Replace \"...\" with ellipsis character (\u2026) ?";
+ private static final String EN_DASH_MESSAGE =
+ "Replace \"-\" with an \"en dash\" character (\u2013, \\u2013) ?";
+ private static final String EM_DASH_MESSAGE =
+ "Replace \"--\" with an \"em dash\" character (\u2014, \\u2014) ?";
+ private static final String TYPOGRAPHIC_APOSTROPHE_MESSAGE =
+ "Replace apostrophe (') with typographic apostrophe (\u2019, \\u2019) ?";
+ private static final String SINGLE_QUOTE_MESSAGE =
+ "Replace straight quotes ('') with directional quotes (\u2018\u2019, \\u2018 and \\u2019) ?";
+ private static final String DBL_QUOTES_MESSAGE =
+ "Replace straight quotes (\") with directional quotes (\u201C\u201D, \\u201C and \\u201D) ?";
+ private static final String COPYRIGHT_MESSAGE =
+ "Replace (c) with copyright symbol \u00A9 (\\u00A9) ?";
+
+ /**
+ * Pattern used to detect scenarios which can be replaced with n dashes: a
+ * numeric range with a hyphen in the middle (and possibly spaces)
+ */
+ @VisibleForTesting
+ static final Pattern HYPHEN_RANGE_PATTERN =
+ Pattern.compile(".*(\\d+\\s*)-(\\s*\\d+).*"); //$NON-NLS-1$
+
+ /**
+ * Pattern used to detect scenarios where a grave accent mark is used
+ * to do ASCII quotations of the form `this'' or ``this'', which is frowned upon.
+ * This pattern tries to avoid falsely complaining about strings like
+ * "Type Option-` then 'Escape'."
+ */
+ @VisibleForTesting
+ static final Pattern GRAVE_QUOTATION =
+ Pattern.compile("(^[^`]*`[^'`]+'[^']*$)|(^[^`]*``[^'`]+''[^']*$)"); //$NON-NLS-1$
+
+ /**
+ * Pattern used to detect common fractions, e.g. 1/2, 1/3, 2/3, 1/4, 3/4 and
+ * variations like 2 / 3, but not 11/22 and so on.
+ */
+ @VisibleForTesting
+ static final Pattern FRACTION_PATTERN =
+ Pattern.compile(".*\\b([13])\\s*/\\s*([234])\\b.*"); //$NON-NLS-1$
+
+ /**
+ * Pattern used to detect single quote strings, such as 'hello', but
+ * not just quoted strings like 'Double quote: "', and not sentences
+ * where there are multiple apostrophes but not in a quoting context such
+ * as "Mind Your P's and Q's".
+ */
+ @VisibleForTesting
+ static final Pattern SINGLE_QUOTE =
+ Pattern.compile(".*\\W*'[^']+'(\\W.*)?"); //$NON-NLS-1$
+
+ private static final String FRACTION_MESSAGE =
+ "Use fraction character %1$c (%2$s) instead of %3$s ?";
+
+ private boolean mCheckDashes;
+ private boolean mCheckQuotes;
+ private boolean mCheckFractions;
+ private boolean mCheckEllipsis;
+ private boolean mCheckMisc;
+
+ /** Constructs a new {@link TypographyDetector} */
+ public TypographyDetector() {
+ }
+
+ @Override
+ public boolean appliesTo(ResourceFolderType folderType) {
+ return folderType == ResourceFolderType.VALUES;
+ }
+
+ @Override
+ public Speed getSpeed() {
+ return Speed.FAST;
+ }
+
+ @Override
+ public Collection<String> getApplicableElements() {
+ return Arrays.asList(new String[] {
+ TAG_STRING,
+ TAG_STRING_ARRAY
+ });
+ }
+
+ @Override
+ public void beforeCheckProject(Context context) {
+ Configuration configuration = context.configuration;
+ mCheckDashes = configuration.isEnabled(DASHES);
+ mCheckQuotes = configuration.isEnabled(QUOTES);
+ mCheckFractions = configuration.isEnabled(FRACTIONS);
+ mCheckEllipsis = configuration.isEnabled(ELLIPSIS);
+ mCheckMisc = configuration.isEnabled(OTHER);
+ }
+
+ @Override
+ public void visitElement(Context context, Element element) {
+ NodeList childNodes = element.getChildNodes();
+ for (int i = 0, n = childNodes.getLength(); i < n; i++) {
+ Node child = childNodes.item(i);
+ if (child.getNodeType() == Node.TEXT_NODE) {
+ String text = child.getNodeValue();
+ checkText(context, element, text);
+ } else if (child.getNodeType() == Node.ELEMENT_NODE &&
+ child.getParentNode().getNodeName().equals(TAG_STRING_ARRAY)) {
+ // String array item children
+ NodeList items = child.getChildNodes();
+ for (int j = 0, m = items.getLength(); j < m; j++) {
+ Node item = items.item(j);
+ if (item.getNodeType() == Node.TEXT_NODE) {
+ String text = item.getNodeValue();
+ checkText(context, child, text);
+ }
+ }
+ }
+ }
+ }
+
+ private void checkText(Context context, Node element, String text) {
+ if (mCheckEllipsis) {
+ // Replace ... with ellipsis character?
+ int ellipsis = text.indexOf("..."); //$NON-NLS-1$
+ if (ellipsis != -1 && !text.startsWith(".", ellipsis + 3)) { //$NON-NLS-1$
+ context.client.report(context, ELLIPSIS, context.getLocation(element),
+ ELLIPSIS_MESSAGE, null);
+ }
+ }
+
+ // Dashes
+ if (mCheckDashes) {
+ int hyphen = text.indexOf('-');
+ if (hyphen != -1) {
+ // n dash
+ Matcher matcher = HYPHEN_RANGE_PATTERN.matcher(text);
+ if (matcher.matches()) {
+ // Make sure that if there is no space before digit there isn't
+ // one on the left either -- since we don't want to consider
+ // "1 2 -3" as a range from 2 to 3
+ boolean isNegativeNumber =
+ !Character.isWhitespace(matcher.group(2).charAt(0)) &&
+ Character.isWhitespace(matcher.group(1).charAt(
+ matcher.group(1).length() - 1));
+ if (!isNegativeNumber) {
+ context.client.report(context, DASHES, context.getLocation(element),
+ EN_DASH_MESSAGE,
+ null);
+ }
+ }
+
+ // m dash
+ int emdash = text.indexOf("--"); //$NON-NLS-1$
+ // Don't suggest replacing -- or "--" with an m dash since these are sometimes
+ // used as digit marker strings
+ if (emdash > 1 && !text.startsWith("-", emdash + 2)) { //$NON-NLS-1$
+ context.client.report(context, DASHES, context.getLocation(element),
+ EM_DASH_MESSAGE, null);
+ }
+ }
+ }
+
+ if (mCheckQuotes) {
+ // Check for single quotes that can be replaced with directional quotes
+ int quoteStart = text.indexOf('\'');
+ if (quoteStart != -1) {
+ int quoteEnd = text.indexOf('\'', quoteStart + 1);
+ if (quoteEnd != -1 && quoteEnd > quoteStart + 1
+ && (quoteEnd < text.length() -1 || quoteStart > 0)
+ && SINGLE_QUOTE.matcher(text).matches()) {
+ context.client.report(context, QUOTES, context.getLocation(element),
+ SINGLE_QUOTE_MESSAGE, null);
+ return;
+ }
+
+ // Check for apostrophes that can be replaced by typographic apostrophes
+ if (quoteEnd == -1 && quoteStart > 0
+ && Character.isLetterOrDigit(text.charAt(quoteStart - 1))) {
+ context.client.report(context, QUOTES, context.getLocation(element),
+ TYPOGRAPHIC_APOSTROPHE_MESSAGE, null);
+ return;
+ }
+ }
+
+ // Check for double quotes that can be replaced by directional double quotes
+ quoteStart = text.indexOf('"');
+ if (quoteStart != -1) {
+ int quoteEnd = text.indexOf('"', quoteStart + 1);
+ if (quoteEnd != -1 && quoteEnd > quoteStart + 1) {
+ if (quoteEnd < text.length() -1 || quoteStart > 0) {
+ context.client.report(context, QUOTES, context.getLocation(element),
+ DBL_QUOTES_MESSAGE, null);
+ return;
+ }
+ }
+ }
+
+ // Check for grave accent quotations
+ if (text.indexOf('`') != -1 && GRAVE_QUOTATION.matcher(text).matches()) {
+ // Are we indenting ``like this'' or `this' ? If so, complain
+ context.client.report(context, QUOTES, context.getLocation(element),
+ GRAVE_QUOTE_MESSAGE, null);
+ return;
+ }
+
+ // Consider suggesting other types of directional quotes, such as guillemets, in
+ // other languages?
+ // There are a lot of exceptions and special cases to be considered so
+ // this will need careful implementation and testing.
+ // See http://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks
+ }
+
+ // Fraction symbols?
+ if (mCheckFractions && text.indexOf('/') != -1) {
+ Matcher matcher = FRACTION_PATTERN.matcher(text);
+ if (matcher.matches()) {
+ String top = matcher.group(1); // Numerator
+ String bottom = matcher.group(2); // Denominator
+ if (top.equals("1") && bottom.equals("2")) { //$NON-NLS-1$ //$NON-NLS-2$
+ context.client.report(context, FRACTIONS, context.getLocation(element),
+ String.format(FRACTION_MESSAGE, '\u00BD', "\\u00BD", "1/2"), null);
+ } else if (top.equals("1") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$
+ context.client.report(context, FRACTIONS, context.getLocation(element),
+ String.format(FRACTION_MESSAGE, '\u00BC', "\\u00BC", "1/4"), null);
+ } else if (top.equals("3") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$
+ context.client.report(context, FRACTIONS, context.getLocation(element),
+ String.format(FRACTION_MESSAGE, '\u00BE', "\\u00BE", "3/4"), null);
+ } else if (top.equals("1") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$
+ context.client.report(context, FRACTIONS, context.getLocation(element),
+ String.format(FRACTION_MESSAGE, '\u2153', "\\u2153", "1/3"), null);
+ } else if (top.equals("2") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$
+ context.client.report(context, FRACTIONS, context.getLocation(element),
+ String.format(FRACTION_MESSAGE, '\u2154', "\\u2154", "2/3"), null);
+ }
+ }
+ }
+
+ if (mCheckMisc) {
+ // Fix copyright symbol?
+ if (text.indexOf('(') != -1
+ && (text.contains("(c)") || text.contains("(C)"))) { //$NON-NLS-1$ //$NON-NLS-2$
+ // Suggest replacing with copyright symbol?
+ context.client.report(context, OTHER, context.getLocation(element),
+ COPYRIGHT_MESSAGE, null);
+ // Replace (R) and TM as well? There are unicode characters for these but they
+ // are probably not very common within Android app strings.
+ }
+ }
+ }
+
+ /**
+ * An object describing a single edit to be made. The offset points to a
+ * location to start editing; the length is the number of characters to
+ * delete, and the replaceWith string points to a string to insert at the
+ * offset. Note that this can model not just replacement edits but deletions
+ * (empty replaceWith) and insertions (replace length = 0) too.
+ */
+ public static class ReplaceEdit {
+ /** The offset of the edit */
+ public final int offset;
+ /** The number of characters to delete at the offset */
+ public final int length;
+ /** The characters to insert at the offset */
+ public final String replaceWith;
+
+ /**
+ * Creates a new replace edit
+ *
+ * @param offset the offset of the edit
+ * @param length the number of characters to delete at the offset
+ * @param replaceWith the characters to insert at the offset
+ */
+ public ReplaceEdit(int offset, int length, String replaceWith) {
+ super();
+ this.offset = offset;
+ this.length = length;
+ this.replaceWith = replaceWith;
+ }
+ }
+
+ /**
+ * Returns a list of edits to be applied to fix the suggestion made by the
+ * given warning. The specific issue id and message should be the message
+ * provided by this detector in an earlier run.
+ * <p>
+ * This is intended to help tools implement automatic fixes of these
+ * warnings. The reason only the message and issue id can be provided
+ * instead of actual state passed in the data field to a reporter is that
+ * fix operation can be run much later than the lint is processed (for
+ * example, in a subsequent run of the IDE when only the warnings have been
+ * persisted),
+ *
+ * @param issueId the issue id, which should be the id for one of the
+ * typography issues
+ * @param message the actual error message, which should be a message
+ * provided by this detector
+ * @param textNode a text node which corresponds to the text node the
+ * warning operated on
+ * @return a list of edits, which is never null but could be empty. The
+ * offsets in the edit objects are relative to the text node.
+ */
+ public static List<ReplaceEdit> getEdits(String issueId, String message, Node textNode) {
+ List<ReplaceEdit> edits = new ArrayList<ReplaceEdit>();
+ String text = textNode.getNodeValue();
+ if (message.equals(ELLIPSIS_MESSAGE)) {
+ int offset = text.indexOf("..."); //$NON-NLS-1$
+ if (offset != -1) {
+ edits.add(new ReplaceEdit(offset, 3, "\u2026")); //$NON-NLS-1$
+ }
+ } else if (message.equals(EN_DASH_MESSAGE)) {
+ int offset = text.indexOf('-');
+ if (offset != -1) {
+ edits.add(new ReplaceEdit(offset, 1, "\u2013")); //$NON-NLS-1$
+ }
+ } else if (message.equals(EM_DASH_MESSAGE)) {
+ int offset = text.indexOf("--"); //$NON-NLS-1$
+ if (offset != -1) {
+ edits.add(new ReplaceEdit(offset, 2, "\u2014")); //$NON-NLS-1$
+ }
+ } else if (message.equals(TYPOGRAPHIC_APOSTROPHE_MESSAGE)) {
+ int offset = text.indexOf('\'');
+ if (offset != -1) {
+ edits.add(new ReplaceEdit(offset, 1, "\u2019")); //$NON-NLS-1$
+ }
+ } else if (message.equals(COPYRIGHT_MESSAGE)) {
+ int offset = text.indexOf("(c)"); //$NON-NLS-1$
+ if (offset == -1) {
+ offset = text.indexOf("(C)"); //$NON-NLS-1$
+ }
+ if (offset != -1) {
+ edits.add(new ReplaceEdit(offset, 3, "\u00A9")); //$NON-NLS-1$
+ }
+ } else if (message.equals(SINGLE_QUOTE_MESSAGE)) {
+ int offset = text.indexOf('\'');
+ if (offset != -1) {
+ int endOffset = text.indexOf("'", offset + 1); //$NON-NLS-1$
+ if (endOffset != -1) {
+ edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$
+ edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$
+ }
+ }
+ } else if (message.equals(DBL_QUOTES_MESSAGE)) {
+ int offset = text.indexOf('"');
+ if (offset != -1) {
+ int endOffset = text.indexOf('"', offset + 1);
+ if (endOffset != -1) {
+ edits.add(new ReplaceEdit(offset, 1, "\u201C")); //$NON-NLS-1$
+ edits.add(new ReplaceEdit(endOffset, 1, "\u201D")); //$NON-NLS-1$
+ }
+ }
+ } else if (message.equals(GRAVE_QUOTE_MESSAGE)) {
+ int offset = text.indexOf('`');
+ if (offset != -1) {
+ int endOffset = text.indexOf('\'', offset + 1);
+ if (endOffset != -1) {
+ edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$
+ edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$
+ }
+ }
+ }
+
+ return edits;
+ }
+}
diff --git a/lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs b/lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..204b512
--- /dev/null
+++ b/lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,3 @@
+#Mon Nov 21 10:44:11 PST 2011
+eclipse.preferences.version=1
+encoding//src/com/android/tools/lint/checks/TypographyDetectorTest.java=UTF-8
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java
new file mode 100644
index 0000000..f33f144
--- /dev/null
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 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.tools.lint.checks.TypographyDetector.FRACTION_PATTERN;
+import static com.android.tools.lint.checks.TypographyDetector.GRAVE_QUOTATION;
+import static com.android.tools.lint.checks.TypographyDetector.HYPHEN_RANGE_PATTERN;
+import static com.android.tools.lint.checks.TypographyDetector.SINGLE_QUOTE;
+
+import com.android.tools.lint.detector.api.Detector;
+
+@SuppressWarnings("javadoc")
+public class TypographyDetectorTest extends AbstractCheckTest {
+ @Override
+ protected Detector getDetector() {
+ return new TypographyDetector();
+ }
+
+ public void test() throws Exception {
+ assertEquals(
+ "typography.xml:10: Warning: Avoid quoting with grave accents; use apostrophes or better yet directional quotes instead\n" +
+ "typography.xml:11: Warning: Replace straight quotes ('') with directional quotes (‘’, \\u2018 and \\u2019) ?\n" +
+ "typography.xml:12: Warning: Use fraction character ½ (\\u00BD) instead of 1/2 ?\n" +
+ "typography.xml:13: Warning: Use fraction character ¼ (\\u00BC) instead of 1/4 ?\n" +
+ "typography.xml:15: Warning: Replace \"...\" with ellipsis character (…) ?\n" +
+ "typography.xml:17: Warning: Replace \"-\" with an \"en dash\" character (–, \\u2013) ?\n" +
+ "typography.xml:18: Warning: Replace \"-\" with an \"en dash\" character (–, \\u2013) ?\n" +
+ "typography.xml:20: Warning: Replace \"--\" with an \"em dash\" character (—, \\u2014) ?\n" +
+ "typography.xml:24: Warning: Replace \"-\" with an \"en dash\" character (–, \\u2013) ?\n" +
+ "typography.xml:25: Warning: Use fraction character ½ (\\u00BD) instead of 1/2 ?\n" +
+ "typography.xml:3: Warning: Replace straight quotes ('') with directional quotes (‘’, \\u2018 and \\u2019) ?\n" +
+ "typography.xml:5: Warning: Replace straight quotes (\") with directional quotes (“”, \\u201C and \\u201D) ?\n" +
+ "typography.xml:6: Warning: Replace straight quotes (\") with directional quotes (“”, \\u201C and \\u201D) ?\n" +
+ "typography.xml:7: Warning: Replace apostrophe (') with typographic apostrophe (’, \\u2019) ?\n" +
+ "typography.xml:8: Warning: Replace (c) with copyright symbol © (\\u00A9) ?\n" +
+ "typography.xml:9: Warning: Replace apostrophe (') with typographic apostrophe (’, \\u2019) ?",
+
+ lintProject("res/values/typography.xml"));
+ }
+
+ public void testSingleQuotesRange() {
+ assertTrue(SINGLE_QUOTE.matcher("Foo: 'bar'").matches());
+ assertTrue(SINGLE_QUOTE.matcher("'Foo': bar").matches());
+ assertTrue(SINGLE_QUOTE.matcher("\"'foo'\"").matches());
+ assertTrue(SINGLE_QUOTE.matcher("\"'foo bar'\"").matches());
+
+ assertFalse(SINGLE_QUOTE.matcher("foo bar'").matches());
+ assertFalse(SINGLE_QUOTE.matcher("Mind your P's and Q's").matches());
+
+ // This isn't asserted by the regexp: checked independently in
+ // the detector. The goal here is to assert that we need to
+ // have some text on either side of the quotes.
+ //assertFalse(SINGLE_QUOTE.matcher("'foo bar'").matches());
+ }
+
+ public void testGraveRegexp() {
+ assertTrue(GRAVE_QUOTATION.matcher("`a'").matches());
+ assertTrue(GRAVE_QUOTATION.matcher(" `a' ").matches());
+ assertTrue(GRAVE_QUOTATION.matcher(" ``a'' ").matches());
+ assertFalse(GRAVE_QUOTATION.matcher("`a''").matches());
+ }
+
+ public void testFractionRegexp() {
+ assertTrue(FRACTION_PATTERN.matcher("fraction 1/2.").matches());
+ assertTrue(FRACTION_PATTERN.matcher("1/2").matches());
+ assertTrue(FRACTION_PATTERN.matcher("1/3").matches());
+ assertTrue(FRACTION_PATTERN.matcher("1/4").matches());
+ assertTrue(FRACTION_PATTERN.matcher("3/4").matches());
+ assertTrue(FRACTION_PATTERN.matcher("1 / 2").matches());
+ assertTrue(FRACTION_PATTERN.matcher("1 / 3").matches());
+ assertTrue(FRACTION_PATTERN.matcher("1 / 4").matches());
+ assertTrue(FRACTION_PATTERN.matcher("3 / 4").matches());
+
+ assertFalse(FRACTION_PATTERN.matcher("3 // 4").matches());
+ assertFalse(FRACTION_PATTERN.matcher("11 / 2").matches());
+ assertFalse(FRACTION_PATTERN.matcher("1 / 22").matches());
+ }
+
+ public void testNDashRegexp() {
+ assertTrue(HYPHEN_RANGE_PATTERN.matcher("3-4").matches());
+ assertTrue(HYPHEN_RANGE_PATTERN.matcher("13- 14").matches());
+ assertTrue(HYPHEN_RANGE_PATTERN.matcher("13 - 14").matches());
+ assertTrue(HYPHEN_RANGE_PATTERN.matcher("The range is 13 - 14").matches());
+ assertTrue(HYPHEN_RANGE_PATTERN.matcher("13 - 14.").matches());
+
+ assertFalse(HYPHEN_RANGE_PATTERN.matcher("13 - x").matches());
+ assertFalse(HYPHEN_RANGE_PATTERN.matcher("x - 14").matches());
+ assertFalse(HYPHEN_RANGE_PATTERN.matcher("x-y").matches());
+ assertFalse(HYPHEN_RANGE_PATTERN.matcher("-y").matches());
+ assertFalse(HYPHEN_RANGE_PATTERN.matcher("x-").matches());
+ }
+}
diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml
new file mode 100644
index 0000000..1dd4845
--- /dev/null
+++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="home_title">Home 'Sample'</string>
+ <string name="show_all_apps">"All"</string>
+ <string name="show_all_apps2">Show "All"</string>
+ <string name="escaped">Skip \"All\"</string>
+ <string name="single">Android's</string>
+ <string name="copyright">(c) 2011</string>
+ <string name="badquotes1">`First'</string>
+ <string name="badquotes2">``second''</string>
+ <string name="notbadquotes">Type Option-` then 'Escape'</string>
+ <string name="fraction1">5 1/2 times</string>
+ <string name="fraction4">1/4 times</string>
+ <string name="notfraction">51/2 times, 1/20</string>
+ <string name="ellipsis">40 times...</string>
+ <string name="notellipsis">40 times.......</string>
+ <string name="ndash">For ages 3-5</string>
+ <string name="ndash2">Copyright 2007 - 2011</string>
+ <string name="nontndash">x-y</string>
+ <string name="mdash">Not found -- please try again</string>
+ <string name="nontndash">----</string>
+ <string name="notdirectional">A's and B's</string>
+ <string-array name="typography">
+ <item>Ages 3-5</item>
+ <item>Age 5 1/2</item>
+ </string-array>
+ <string name="ndash">X Y Z: 10 10 -1</string>
+</resources>
+