blob: 6b902a5dbf110e5a90ef6b996ecd8ff12141fdb0 [file] [log] [blame]
/*
* 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.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.FORMAT_METHOD;
import static com.android.SdkConstants.GET_STRING_METHOD;
import static com.android.SdkConstants.R_CLASS;
import static com.android.SdkConstants.R_PREFIX;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.tools.lint.checks.SharedPrefsDetector.ANDROID_CONTENT_SHARED_PREFERENCES;
import static com.android.tools.lint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.lint.client.api.JavaParser.TYPE_BYTE;
import static com.android.tools.lint.client.api.JavaParser.TYPE_CHAR;
import static com.android.tools.lint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.lint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.lint.client.api.JavaParser.TYPE_NULL;
import static com.android.tools.lint.client.api.JavaParser.TYPE_OBJECT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_SHORT;
import static com.android.tools.lint.client.api.JavaParser.TYPE_STRING;
import static com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.LintClient;
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.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Location.Handle;
import com.android.tools.lint.detector.api.Position;
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.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.ast.AstVisitor;
import lombok.ast.BooleanLiteral;
import lombok.ast.CharLiteral;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.Expression;
import lombok.ast.FloatingPointLiteral;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.IntegralLiteral;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.NullLiteral;
import lombok.ast.Select;
import lombok.ast.StrictListAccessor;
import lombok.ast.StringLiteral;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Check which looks for problems with formatting strings such as inconsistencies between
* translations or between string declaration and string usage in Java.
* <p>
* TODO: Verify booleans!
* TODO: Handle Resources.getQuantityString as well
*/
public class StringFormatDetector extends ResourceXmlDetector implements Detector.JavaScanner {
private static final Implementation IMPLEMENTATION_XML = new Implementation(
StringFormatDetector.class,
Scope.ALL_RESOURCES_SCOPE);
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION_XML_AND_JAVA = new Implementation(
StringFormatDetector.class,
EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE),
Scope.JAVA_FILE_SCOPE);
/** Whether formatting strings are invalid */
public static final Issue INVALID = Issue.create(
"StringFormatInvalid", //$NON-NLS-1$
"Invalid format string",
"If a string contains a '%' character, then the string may be a formatting string " +
"which will be passed to `String.format` from Java code to replace each '%' " +
"occurrence with specific values.\n" +
"\n" +
"This lint warning checks for two related problems:\n" +
"(1) Formatting strings that are invalid, meaning that `String.format` will throw " +
"exceptions at runtime when attempting to use the format string.\n" +
"(2) Strings containing '%' that are not formatting strings getting passed to " +
"a `String.format` call. In this case the '%' will need to be escaped as '%%'.\n" +
"\n" +
"NOTE: Not all Strings which look like formatting strings are intended for " +
"use by `String.format`; for example, they may contain date formats intended " +
"for `android.text.format.Time#format()`. Lint cannot always figure out that " +
"a String is a date format, so you may get false warnings in those scenarios. " +
"See the suppress help topic for information on how to suppress errors in " +
"that case.",
Category.MESSAGES,
9,
Severity.ERROR,
IMPLEMENTATION_XML);
/** Whether formatting argument types are consistent across translations */
public static final Issue ARG_COUNT = Issue.create(
"StringFormatCount", //$NON-NLS-1$
"Formatting argument types incomplete or inconsistent",
"When a formatted string takes arguments, it usually needs to reference the " +
"same arguments in all translations (or all arguments if there are no " +
"translations.\n" +
"\n" +
"There are cases where this is not the case, so this issue is a warning rather " +
"than an error by default. However, this usually happens when a language is not " +
"translated or updated correctly.",
Category.MESSAGES,
5,
Severity.WARNING,
IMPLEMENTATION_XML);
/** Whether the string format supplied in a call to String.format matches the format string */
public static final Issue ARG_TYPES = Issue.create(
"StringFormatMatches", //$NON-NLS-1$
"`String.format` string doesn't match the XML format string",
"This lint check ensures the following:\n" +
"(1) If there are multiple translations of the format string, then all translations " +
"use the same type for the same numbered arguments\n" +
"(2) The usage of the format string in Java is consistent with the format string, " +
"meaning that the parameter types passed to String.format matches those in the " +
"format string.",
Category.MESSAGES,
9,
Severity.ERROR,
IMPLEMENTATION_XML_AND_JAVA);
/** This plural does not use the quantity value */
public static final Issue POTENTIAL_PLURAL = Issue.create(
"PluralsCandidate", //$NON-NLS-1$
"Potential Plurals",
"This lint check looks for potential errors in internationalization where you have " +
"translated a message which involves a quantity and it looks like other parts of " +
"the string may need grammatical changes.\n" +
"\n" +
"For example, rather than something like this:\n" +
" <string name=\"try_again\">Try again in %d seconds.</string>\n" +
"you should be using a plural:\n" +
" <plurals name=\"try_again\">\n" +
" <item quantity=\"one\">Try again in %d second</item>\n" +
" <item quantity=\"other\">Try again in %d seconds</item>\n" +
" </plurals>\n" +
"This will ensure that in other languages the right set of translations are " +
"provided for the different quantity classes.\n" +
"\n" +
"(This check depends on some heuristics, so it may not accurately determine whether " +
"a string really should be a quantity. You can use tools:ignore to filter out false " +
"positives.",
Category.MESSAGES,
5,
Severity.WARNING,
IMPLEMENTATION_XML).addMoreInfo(
"http://developer.android.com/guide/topics/resources/string-resource.html#Plurals");
/**
* Map from a format string name to a list of declaration file and actual
* formatting string content. We're using a list since a format string can be
* defined multiple times, usually for different translations.
*/
private Map<String, List<Pair<Handle, String>>> mFormatStrings;
/**
* Map of strings that contain percents that aren't formatting strings; these
* should not be passed to String.format.
*/
private final Map<String, Handle> mNotFormatStrings = new HashMap<String, Handle>();
/**
* Set of strings that have an unknown format such as date formatting; we should not
* flag these as invalid when used from a String#format call
*/
private Set<String> mIgnoreStrings;
/** Constructs a new {@link StringFormatDetector} check */
public StringFormatDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.VALUES;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
if (LintUtils.endsWith(file.getName(), DOT_JAVA)) {
return mFormatStrings != null;
}
return super.appliesTo(context, file);
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_STRING);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
NodeList childNodes = element.getChildNodes();
if (childNodes.getLength() > 0) {
if (childNodes.getLength() == 1) {
Node child = childNodes.item(0);
if (child.getNodeType() == Node.TEXT_NODE) {
checkTextNode(context, element, strip(child.getNodeValue()));
}
} else {
// Concatenate children and build up a plain string.
// This is needed to handle xliff localization documents,
// but this needs more work so ignore compound XML documents as
// string values for now:
StringBuilder sb = new StringBuilder();
addText(sb, element);
if (sb.length() > 0) {
checkTextNode(context, element, sb.toString());
}
}
}
}
private static void addText(StringBuilder sb, Node node) {
if (node.getNodeType() == Node.TEXT_NODE) {
sb.append(strip(node.getNodeValue().trim()));
} else {
NodeList childNodes = node.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
addText(sb, childNodes.item(i));
}
}
}
private static String strip(String s) {
if (s.length() < 2) {
return s;
}
char first = s.charAt(0);
char last = s.charAt(s.length() - 1);
if (first == last && (first == '\'' || first == '"')) {
return s.substring(1, s.length() - 1);
}
return s;
}
private void checkTextNode(XmlContext context, Element element, String text) {
String name = null;
boolean found = false;
boolean foundPlural = false;
// Look at the String and see if it's a format string (contains
// positional %'s)
for (int j = 0, m = text.length(); j < m; j++) {
char c = text.charAt(j);
if (c == '\\') {
j++;
}
if (c == '%') {
if (name == null) {
name = element.getAttribute(ATTR_NAME);
}
// Also make sure this String isn't an unformatted String
String formatted = element.getAttribute("formatted"); //$NON-NLS-1$
if (!formatted.isEmpty() && !Boolean.parseBoolean(formatted)) {
if (!mNotFormatStrings.containsKey(name)) {
Handle handle = context.createLocationHandle(element);
handle.setClientData(element);
mNotFormatStrings.put(name, handle);
}
return;
}
// See if it's not a format string, e.g. "Battery charge is 100%!".
// If so we want to record this name in a special list such that we can
// make sure you don't attempt to reference this string from a String.format
// call.
Matcher matcher = FORMAT.matcher(text);
if (!matcher.find(j)) {
if (!mNotFormatStrings.containsKey(name)) {
Handle handle = context.createLocationHandle(element);
handle.setClientData(element);
mNotFormatStrings.put(name, handle);
}
return;
}
String conversion = matcher.group(6);
int conversionClass = getConversionClass(conversion.charAt(0));
if (conversionClass == CONVERSION_CLASS_UNKNOWN || matcher.group(5) != null) {
if (mIgnoreStrings == null) {
mIgnoreStrings = new HashSet<String>();
}
mIgnoreStrings.add(name);
// Don't process any other strings here; some of them could
// accidentally look like a string, e.g. "%H" is a hash code conversion
// in String.format (and hour in Time formatting).
return;
}
if (conversionClass == CONVERSION_CLASS_INTEGER && !foundPlural) {
// See if there appears to be further text content here.
// Look for whitespace followed by a letter, with no punctuation in between
for (int k = matcher.end(); k < m; k++) {
char nc = text.charAt(k);
if (!Character.isWhitespace(nc)) {
if (Character.isLetter(nc)) {
foundPlural = checkPotentialPlural(context, element, text, k);
}
break;
}
}
}
found = true;
j++; // Ensure that when we process a "%%" we don't separately check the second %
}
}
if (found && name != null) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
// Record it for analysis when seen in Java code
if (mFormatStrings == null) {
mFormatStrings = new HashMap<String, List<Pair<Handle,String>>>();
}
List<Pair<Handle, String>> list = mFormatStrings.get(name);
if (list == null) {
list = new ArrayList<Pair<Handle, String>>();
mFormatStrings.put(name, list);
}
Handle handle = context.createLocationHandle(element);
handle.setClientData(element);
list.add(Pair.of(handle, text));
}
}
/**
* Checks whether the text begins with a non-unit word, pointing to a string
* that should probably be a plural instead. This
*/
private static boolean checkPotentialPlural(XmlContext context, Element element, String text,
int wordBegin) {
// This method should only be called if the text is known to start with a word
assert Character.isLetter(text.charAt(wordBegin));
int wordEnd = wordBegin;
while (wordEnd < text.length()) {
if (!Character.isLetter(text.charAt(wordEnd))) {
break;
}
wordEnd++;
}
// Eliminate units, since those are not sentences you need to use plurals for, e.g.
// "Elevation gain: %1$d m (%2$d ft)"
// We'll determine whether something is a unit by looking for
// (1) Multiple uppercase characters (e.g. KB, or MiB), or better yet, uppercase characters
// anywhere but as the first letter
// (2) No vowels (e.g. ft)
// (3) Adjacent consonants (e.g. ft); this one can eliminate some legitimate
// English words as well (e.g. "the") so we should really limit this to
// letter pairs that are not common in English. This is probably overkill
// so not handled yet. Instead we use a simpler heuristic:
// (4) Very short "words" (1-2 letters)
if (wordEnd - wordBegin <= 2) {
// Very short word (1-2 chars): possible unit, e.g. "m", "ft", "kb", etc
return false;
}
boolean hasVowel = false;
for (int i = wordBegin; i < wordEnd; i++) {
// Uppercase character anywhere but first character: probably a unit (e.g. KB)
char c = text.charAt(i);
if (i > wordBegin && Character.isUpperCase(c)) {
return false;
}
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y') {
hasVowel = true;
}
}
if (!hasVowel) {
// No vowels: likely unit
return false;
}
String word = text.substring(wordBegin, wordEnd);
// Some other known abbreviations that we don't want to count:
if (word.equals("min")) {
return false;
}
// This heuristic only works in English!
if (LintUtils.isEnglishResource(context, true)) {
String message = String.format("Formatting %%d followed by words (\"%1$s\"): "
+ "This should probably be a plural rather than a string", word);
context.report(POTENTIAL_PLURAL, element,
context.getLocation(element),
message);
// Avoid reporting multiple errors on the same string
// (if it contains more than one %d)
return true;
}
return false;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mFormatStrings != null) {
boolean checkCount = context.isEnabled(ARG_COUNT);
boolean checkValid = context.isEnabled(INVALID);
boolean checkTypes = context.isEnabled(ARG_TYPES);
// Ensure that all the format strings are consistent with respect to each other;
// e.g. they all have the same number of arguments, they all use all the
// arguments, and they all use the same types for all the numbered arguments
for (Map.Entry<String, List<Pair<Handle, String>>> entry : mFormatStrings.entrySet()) {
String name = entry.getKey();
List<Pair<Handle, String>> list = entry.getValue();
// Check argument counts
if (checkCount) {
checkArity(context, name, list);
}
// Check argument types (and also make sure that the formatting strings are valid)
if (checkValid || checkTypes) {
checkTypes(context, checkValid, checkTypes, name, list);
}
}
}
}
private static void checkTypes(Context context, boolean checkValid,
boolean checkTypes, String name, List<Pair<Handle, String>> list) {
Map<Integer, String> types = new HashMap<Integer, String>();
Map<Integer, Handle> typeDefinition = new HashMap<Integer, Handle>();
for (Pair<Handle, String> pair : list) {
Handle handle = pair.getFirst();
String formatString = pair.getSecond();
//boolean warned = false;
Matcher matcher = FORMAT.matcher(formatString);
int index = 0;
int prevIndex = 0;
int nextNumber = 1;
while (true) {
if (matcher.find(index)) {
int matchStart = matcher.start();
// Make sure this is not an escaped '%'
for (; prevIndex < matchStart; prevIndex++) {
char c = formatString.charAt(prevIndex);
if (c == '\\') {
prevIndex++;
}
}
if (prevIndex > matchStart) {
// We're in an escape, ignore this result
index = prevIndex;
continue;
}
index = matcher.end(); // Ensure loop proceeds
String str = formatString.substring(matchStart, matcher.end());
if (str.equals("%%") || str.equals("%n")) { //$NON-NLS-1$ //$NON-NLS-2$
// Just an escaped %
continue;
}
if (checkValid) {
// Make sure it's a valid format string
if (str.length() > 2 && str.charAt(str.length() - 2) == ' ') {
char last = str.charAt(str.length() - 1);
// If you forget to include the conversion character, e.g.
// "Weight=%1$ g" instead of "Weight=%1$d g", then
// you're going to end up with a format string interpreted as
// "%1$ g". This means that the space character is interpreted
// as a flag character, but it can only be a flag character
// when used in conjunction with the numeric conversion
// formats (d, o, x, X). If that's not the case, make a
// dedicated error message
if (last != 'd' && last != 'o' && last != 'x' && last != 'X') {
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, INVALID,
(Node) clientData)) {
return;
}
}
Location location = handle.resolve();
String message = String.format(
"Incorrect formatting string `%1$s`; missing conversion " +
"character in '`%2$s`' ?", name, str);
context.report(INVALID, location, message);
//warned = true;
continue;
}
}
}
if (!checkTypes) {
continue;
}
// Shouldn't throw a number format exception since we've already
// matched the pattern in the regexp
int number;
String numberString = matcher.group(1);
if (numberString != null) {
// Strip off trailing $
numberString = numberString.substring(0, numberString.length() - 1);
number = Integer.parseInt(numberString);
nextNumber = number + 1;
} else {
number = nextNumber++;
}
String format = matcher.group(6);
String currentFormat = types.get(number);
if (currentFormat == null) {
types.put(number, format);
typeDefinition.put(number, handle);
} else if (!currentFormat.equals(format)
&& isIncompatible(currentFormat.charAt(0), format.charAt(0))) {
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ARG_TYPES,
(Node) clientData)) {
return;
}
}
Location location = handle.resolve();
// Attempt to limit the location range to just the formatting
// string in question
location = refineLocation(context, location, formatString,
matcher.start(), matcher.end());
Location otherLocation = typeDefinition.get(number).resolve();
otherLocation.setMessage("Conflicting argument type here");
location.setSecondary(otherLocation);
File f = otherLocation.getFile();
String message = String.format(
"Inconsistent formatting types for argument #%1$d in " +
"format string `%2$s` ('%3$s'): Found both '`%4$s`' and '`%5$s`' " +
"(in %6$s)",
number, name,
str,
currentFormat, format,
f.getParentFile().getName() + File.separator + f.getName());
//warned = true;
context.report(ARG_TYPES, location, message);
break;
}
} else {
break;
}
}
// Check that the format string is valid by actually attempting to instantiate
// it. We only do this if we haven't already complained about this string
// for other reasons.
/* Check disabled for now: it had many false reports due to conversion
* errors (which is expected since we just pass in strings), but once those
* are eliminated there aren't really any other valid error messages returned
* (for example, calling the formatter with bogus formatting flags always just
* returns a "conversion" error. It looks like we'd need to actually pass compatible
* arguments to trigger other types of formatting errors such as precision errors.
if (!warned && checkValid) {
try {
formatter.format(formatString, "", "", "", "", "", "", "",
"", "", "", "", "", "", "");
} catch (IllegalFormatException t) { // TODO: UnknownFormatConversionException
if (!t.getLocalizedMessage().contains(" != ")
&& !t.getLocalizedMessage().contains("Conversion")) {
Location location = handle.resolve();
context.report(INVALID, location,
String.format("Wrong format for %1$s: %2$s",
name, t.getLocalizedMessage()), null);
}
}
}
*/
}
}
/**
* Returns true if two String.format conversions are "incompatible" (meaning
* that using these two for the same argument across different translations
* is more likely an error than intentional. Some conversions are
* incompatible, e.g. "d" and "s" where one is a number and string, whereas
* others may work (e.g. float versus integer) but are probably not
* intentional.
*/
private static boolean isIncompatible(char conversion1, char conversion2) {
int class1 = getConversionClass(conversion1);
int class2 = getConversionClass(conversion2);
return class1 != class2
&& class1 != CONVERSION_CLASS_UNKNOWN
&& class2 != CONVERSION_CLASS_UNKNOWN;
}
private static final int CONVERSION_CLASS_UNKNOWN = 0;
private static final int CONVERSION_CLASS_STRING = 1;
private static final int CONVERSION_CLASS_CHARACTER = 2;
private static final int CONVERSION_CLASS_INTEGER = 3;
private static final int CONVERSION_CLASS_FLOAT = 4;
private static final int CONVERSION_CLASS_BOOLEAN = 5;
private static final int CONVERSION_CLASS_HASHCODE = 6;
private static final int CONVERSION_CLASS_PERCENT = 7;
private static final int CONVERSION_CLASS_NEWLINE = 8;
private static final int CONVERSION_CLASS_DATETIME = 9;
private static int getConversionClass(char conversion) {
// See http://developer.android.com/reference/java/util/Formatter.html
switch (conversion) {
case 't': // Time/date conversion
case 'T':
return CONVERSION_CLASS_DATETIME;
case 's': // string
case 'S': // Uppercase string
return CONVERSION_CLASS_STRING;
case 'c': // character
case 'C': // Uppercase character
return CONVERSION_CLASS_CHARACTER;
case 'd': // decimal
case 'o': // octal
case 'x': // hex
case 'X':
return CONVERSION_CLASS_INTEGER;
case 'f': // decimal float
case 'e': // exponential float
case 'E':
case 'g': // decimal or exponential depending on size
case 'G':
case 'a': // hex float
case 'A':
return CONVERSION_CLASS_FLOAT;
case 'b': // boolean
case 'B':
return CONVERSION_CLASS_BOOLEAN;
case 'h': // boolean
case 'H':
return CONVERSION_CLASS_HASHCODE;
case '%': // literal
return CONVERSION_CLASS_PERCENT;
case 'n': // literal
return CONVERSION_CLASS_NEWLINE;
}
return CONVERSION_CLASS_UNKNOWN;
}
private static Location refineLocation(Context context, Location location,
String formatString, int substringStart, int substringEnd) {
Position startLocation = location.getStart();
Position endLocation = location.getEnd();
if (startLocation != null && endLocation != null) {
int startOffset = startLocation.getOffset();
int endOffset = endLocation.getOffset();
if (startOffset >= 0) {
String contents = context.getClient().readFile(location.getFile());
if (endOffset <= contents.length() && startOffset < endOffset) {
int formatOffset = contents.indexOf(formatString, startOffset);
if (formatOffset != -1 && formatOffset <= endOffset) {
return Location.create(location.getFile(), contents,
formatOffset + substringStart, formatOffset + substringEnd);
}
}
}
}
return location;
}
/**
* Check that the number of arguments in the format string is consistent
* across translations, and that all arguments are used
*/
private static void checkArity(Context context, String name, List<Pair<Handle, String>> list) {
// Check to make sure that the argument counts and types are consistent
int prevCount = -1;
for (Pair<Handle, String> pair : list) {
Set<Integer> indices = new HashSet<Integer>();
int count = getFormatArgumentCount(pair.getSecond(), indices);
Handle handle = pair.getFirst();
if (prevCount != -1 && prevCount != count) {
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ARG_COUNT, (Node) clientData)) {
return;
}
}
Location location = handle.resolve();
Location secondary = list.get(0).getFirst().resolve();
secondary.setMessage("Conflicting number of arguments here");
location.setSecondary(secondary);
String message = String.format(
"Inconsistent number of arguments in formatting string `%1$s`; " +
"found both %2$d and %3$d", name, prevCount, count);
context.report(ARG_COUNT, location, message);
break;
}
for (int i = 1; i <= count; i++) {
if (!indices.contains(i)) {
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, ARG_COUNT,
(Node) clientData)) {
return;
}
}
Set<Integer> all = new HashSet<Integer>();
for (int j = 1; j < count; j++) {
all.add(j);
}
all.removeAll(indices);
List<Integer> sorted = new ArrayList<Integer>(all);
Collections.sort(sorted);
Location location = handle.resolve();
String message = String.format(
"Formatting string '`%1$s`' is not referencing numbered arguments %2$s",
name, sorted);
context.report(ARG_COUNT, location, message);
break;
}
}
prevCount = count;
}
}
// See java.util.Formatter docs
public static final Pattern FORMAT = Pattern.compile(
// Generic format:
// %[argument_index$][flags][width][.precision]conversion
//
"%" + //$NON-NLS-1$
// Argument Index
"(\\d+\\$)?" + //$NON-NLS-1$
// Flags
"([-+#, 0(\\<]*)?" + //$NON-NLS-1$
// Width
"(\\d+)?" + //$NON-NLS-1$
// Precision
"(\\.\\d+)?" + //$NON-NLS-1$
// Conversion. These are all a single character, except date/time conversions
// which take a prefix of t/T:
"([tT])?" + //$NON-NLS-1$
// The current set of conversion characters are
// b,h,s,c,d,o,x,e,f,g,a,t (as well as all those as upper-case characters), plus
// n for newlines and % as a literal %. And then there are all the time/date
// characters: HIKLm etc. Just match on all characters here since there should
// be at least one.
"([a-zA-Z%])"); //$NON-NLS-1$
/** Given a format string returns the format type of the given argument */
@VisibleForTesting
@Nullable
static String getFormatArgumentType(String s, int argument) {
Matcher matcher = FORMAT.matcher(s);
int index = 0;
int prevIndex = 0;
int nextNumber = 1;
while (true) {
if (matcher.find(index)) {
String value = matcher.group(6);
if ("%".equals(value) || "n".equals(value)) { //$NON-NLS-1$ //$NON-NLS-2$
index = matcher.end();
continue;
}
int matchStart = matcher.start();
// Make sure this is not an escaped '%'
for (; prevIndex < matchStart; prevIndex++) {
char c = s.charAt(prevIndex);
if (c == '\\') {
prevIndex++;
}
}
if (prevIndex > matchStart) {
// We're in an escape, ignore this result
index = prevIndex;
continue;
}
// Shouldn't throw a number format exception since we've already
// matched the pattern in the regexp
int number;
String numberString = matcher.group(1);
if (numberString != null) {
// Strip off trailing $
numberString = numberString.substring(0, numberString.length() - 1);
number = Integer.parseInt(numberString);
nextNumber = number + 1;
} else {
number = nextNumber++;
}
if (number == argument) {
return matcher.group(6);
}
index = matcher.end();
} else {
break;
}
}
return null;
}
/**
* Given a format string returns the number of required arguments. If the
* {@code seenArguments} parameter is not null, put the indices of any
* observed arguments into it.
*/
@VisibleForTesting
static int getFormatArgumentCount(@NonNull String s, @Nullable Set<Integer> seenArguments) {
Matcher matcher = FORMAT.matcher(s);
int index = 0;
int prevIndex = 0;
int nextNumber = 1;
int max = 0;
while (true) {
if (matcher.find(index)) {
String value = matcher.group(6);
if ("%".equals(value) || "n".equals(value)) { //$NON-NLS-1$ //$NON-NLS-2$
index = matcher.end();
continue;
}
int matchStart = matcher.start();
// Make sure this is not an escaped '%'
for (; prevIndex < matchStart; prevIndex++) {
char c = s.charAt(prevIndex);
if (c == '\\') {
prevIndex++;
}
}
if (prevIndex > matchStart) {
// We're in an escape, ignore this result
index = prevIndex;
continue;
}
// Shouldn't throw a number format exception since we've already
// matched the pattern in the regexp
int number;
String numberString = matcher.group(1);
if (numberString != null) {
// Strip off trailing $
numberString = numberString.substring(0, numberString.length() - 1);
number = Integer.parseInt(numberString);
nextNumber = number + 1;
} else {
number = nextNumber++;
}
if (number > max) {
max = number;
}
if (seenArguments != null) {
seenArguments.add(number);
}
index = matcher.end();
} else {
break;
}
}
return max;
}
/**
* Determines whether the given {@link String#format(String, Object...)}
* formatting string is "locale dependent", meaning that its output depends
* on the locale. This is the case if it for example references decimal
* numbers of dates and times.
*
* @param format the format string
* @return true if the format is locale sensitive, false otherwise
*/
public static boolean isLocaleSpecific(@NonNull String format) {
if (format.indexOf('%') == -1) {
return false;
}
Matcher matcher = FORMAT.matcher(format);
int index = 0;
int prevIndex = 0;
while (true) {
if (matcher.find(index)) {
int matchStart = matcher.start();
// Make sure this is not an escaped '%'
for (; prevIndex < matchStart; prevIndex++) {
char c = format.charAt(prevIndex);
if (c == '\\') {
prevIndex++;
}
}
if (prevIndex > matchStart) {
// We're in an escape, ignore this result
index = prevIndex;
continue;
}
String type = matcher.group(6);
if (!type.isEmpty()) {
char t = type.charAt(0);
// The following formatting characters are locale sensitive:
switch (t) {
case 'd': // decimal integer
case 'e': // scientific
case 'E':
case 'f': // decimal float
case 'g': // general
case 'G':
case 't': // date/time
case 'T':
return true;
}
}
index = matcher.end();
} else {
break;
}
}
return false;
}
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(FORMAT_METHOD, GET_STRING_METHOD);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
if (mFormatStrings == null && !context.getClient().supportsProjectResources()) {
return;
}
String methodName = node.astName().astValue();
if (methodName.equals(FORMAT_METHOD)) {
// String.format(getResources().getString(R.string.foo), arg1, arg2, ...)
// Check that the arguments in R.string.foo match arg1, arg2, ...
if (node.astOperand() instanceof VariableReference) {
VariableReference ref = (VariableReference) node.astOperand();
if ("String".equals(ref.astIdentifier().astValue())) { //$NON-NLS-1$
// Found a String.format call
// Look inside to see if we can find an R string
// Find surrounding method
checkFormatCall(context, node);
}
}
} else {
// getResources().getString(R.string.foo, arg1, arg2, ...)
// Check that the arguments in R.string.foo match arg1, arg2, ...
if (node.astArguments().size() > 1 && node.astOperand() != null ) {
checkFormatCall(context, node);
}
}
}
private void checkFormatCall(JavaContext context, MethodInvocation node) {
lombok.ast.Node current = getParentMethod(node);
if (current != null) {
checkStringFormatCall(context, current, node);
}
}
/**
* Check the given String.format call (with the given arguments) to see if
* the string format is being used correctly
*
* @param context the context to report errors to
* @param method the method containing the {@link String#format} call
* @param call the AST node for the {@link String#format}
*/
private void checkStringFormatCall(
JavaContext context,
lombok.ast.Node method,
MethodInvocation call) {
StrictListAccessor<Expression, MethodInvocation> args = call.astArguments();
if (args.isEmpty()) {
return;
}
StringTracker tracker = new StringTracker(context, method, call, 0);
method.accept(tracker);
String name = tracker.getFormatStringName();
if (name == null) {
return;
}
if (mIgnoreStrings != null && mIgnoreStrings.contains(name)) {
return;
}
if (mNotFormatStrings.containsKey(name)) {
Handle handle = mNotFormatStrings.get(name);
Object clientData = handle.getClientData();
if (clientData instanceof Node) {
if (context.getDriver().isSuppressed(null, INVALID, (Node) clientData)) {
return;
}
}
Location location = handle.resolve();
String message = String.format(
"Format string '`%1$s`' is not a valid format string so it should not be " +
"passed to `String.format`",
name);
context.report(INVALID, call, location, message);
return;
}
Iterator<Expression> argIterator = args.iterator();
Expression first = argIterator.next();
Expression second = argIterator.hasNext() ? argIterator.next() : null;
boolean specifiesLocale;
TypeDescriptor parameterType = context.getType(first);
if (parameterType != null) {
specifiesLocale = isLocaleReference(parameterType.getName());
} else if (!call.astName().astValue().equals(FORMAT_METHOD)) {
specifiesLocale = false;
} else {
// No type information with this AST; use string patterns instead to make
// an educated guess
String firstName = first.toString();
specifiesLocale = firstName.startsWith("Locale.") //$NON-NLS-1$
|| firstName.contains("locale") //$NON-NLS-1$
|| firstName.equals("null") //$NON-NLS-1$
|| (second != null && second.toString().contains("getString") //$NON-NLS-1$
&& !firstName.contains("getString") //$NON-NLS-1$
&& !firstName.contains(R_PREFIX)
&& !(first instanceof StringLiteral));
}
List<Pair<Handle, String>> list = mFormatStrings != null ? mFormatStrings.get(name) : null;
if (list == null) {
LintClient client = context.getClient();
if (client.supportsProjectResources() &&
!context.getScope().contains(Scope.RESOURCE_FILE)) {
AbstractResourceRepository resources = client
.getProjectResources(context.getMainProject(), true);
List<ResourceItem> items = resources
.getResourceItem(ResourceType.STRING, name);
if (items != null) {
for (final ResourceItem item : items) {
ResourceValue v = item.getResourceValue(false);
if (v != null) {
String value = v.getRawXmlValue();
if (value != null) {
// Make sure it's really a formatting string,
// not for example "Battery remaining: 90%"
boolean isFormattingString = value.indexOf('%') != -1;
for (int j = 0, m = value.length();
j < m && isFormattingString;
j++) {
char c = value.charAt(j);
if (c == '\\') {
j++;
} else if (c == '%') {
Matcher matcher = FORMAT.matcher(value);
if (!matcher.find(j)) {
isFormattingString = false;
} else {
String conversion = matcher.group(6);
int conversionClass = getConversionClass(
conversion.charAt(0));
if (conversionClass == CONVERSION_CLASS_UNKNOWN
|| matcher.group(5) != null) {
// Some date format etc - don't process
return;
}
}
j++; // Don't process second % in a %%
}
// If the user marked the string with
}
if (isFormattingString) {
if (list == null) {
list = Lists.newArrayList();
if (mFormatStrings == null) {
mFormatStrings = Maps.newHashMap();
}
mFormatStrings.put(name, list);
}
Handle handle = client.createResourceItemHandle(item);
list.add(Pair.of(handle, value));
}
}
}
}
}
} else {
return;
}
}
if (list != null) {
Set<String> reported = null;
for (Pair<Handle, String> pair : list) {
String s = pair.getSecond();
if (reported != null && reported.contains(s)) {
continue;
}
int count = getFormatArgumentCount(s, null);
Handle handle = pair.getFirst();
if (count != args.size() - 1 - (specifiesLocale ? 1 : 0)) {
if (isSharedPreferenceGetString(context, call)) {
continue;
}
Location location = context.getLocation(call);
Location secondary = handle.resolve();
secondary.setMessage(String.format("This definition requires %1$d arguments",
count));
location.setSecondary(secondary);
String message = String.format(
"Wrong argument count, format string `%1$s` requires `%2$d` but format " +
"call supplies `%3$d`",
name, count, args.size() - 1 - (specifiesLocale ? 1 : 0));
context.report(ARG_TYPES, method, location, message);
if (reported == null) {
reported = Sets.newHashSet();
}
reported.add(s);
} else {
for (int i = 1; i <= count; i++) {
int argumentIndex = i + (specifiesLocale ? 1 : 0);
Class<?> type = tracker.getArgumentType(argumentIndex);
if (type != null) {
boolean valid = true;
String formatType = getFormatArgumentType(s, i);
if (formatType == null) {
continue;
}
char last = formatType.charAt(formatType.length() - 1);
if (formatType.length() >= 2 &&
Character.toLowerCase(
formatType.charAt(formatType.length() - 2)) == 't') {
// Date time conversion.
// TODO
continue;
}
switch (last) {
// Booleans. It's okay to pass objects to these;
// it will print "true" if non-null, but it's
// unusual and probably not intended.
case 'b':
case 'B':
valid = type == Boolean.TYPE;
break;
// Numeric: integer and floats in various formats
case 'x':
case 'X':
case 'd':
case 'o':
case 'e':
case 'E':
case 'f':
case 'g':
case 'G':
case 'a':
case 'A':
valid = type == Integer.TYPE
|| type == Float.TYPE
|| type == Double.TYPE
|| type == Long.TYPE
|| type == Byte.TYPE
|| type == Short.TYPE;
break;
case 'c':
case 'C':
// Unicode character
valid = type == Character.TYPE;
break;
case 'h':
case 'H': // Hex print of hash code of objects
case 's':
case 'S':
// String. Can pass anything, but warn about
// numbers since you may have meant more
// specific formatting. Use special issue
// explanation for this?
valid = type != Boolean.TYPE &&
!Number.class.isAssignableFrom(type);
break;
}
if (!valid) {
if (isSharedPreferenceGetString(context, call)) {
continue;
}
Expression argument = tracker.getArgument(argumentIndex);
Location location = context.getLocation(argument);
Location secondary = handle.resolve();
secondary.setMessage("Conflicting argument declaration here");
location.setSecondary(secondary);
String message = String.format(
"Wrong argument type for formatting argument '#%1$d' " +
"in `%2$s`: conversion is '`%3$s`', received `%4$s` " +
"(argument #%5$d in method call)",
i, name, formatType, type.getSimpleName(),
argumentIndex + 1);
context.report(ARG_TYPES, method, location, message);
if (reported == null) {
reported = Sets.newHashSet();
}
reported.add(s);
}
}
}
}
}
}
}
private static boolean isSharedPreferenceGetString(@NonNull JavaContext context,
@NonNull MethodInvocation call) {
if (!GET_STRING_METHOD.equals(call.astName().astValue())) {
return false;
}
JavaParser.ResolvedNode resolved = context.resolve(call);
if (resolved instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod resolvedMethod = (JavaParser.ResolvedMethod) resolved;
JavaParser.ResolvedClass containingClass = resolvedMethod.getContainingClass();
return containingClass.isSubclassOf(ANDROID_CONTENT_SHARED_PREFERENCES, false);
}
return false; // not certain
}
private static boolean isLocaleReference(@Nullable TypeDescriptor reference) {
return reference != null && isLocaleReference(reference.getName());
}
private static boolean isLocaleReference(@Nullable String typeName) {
return typeName != null && (typeName.equals("Locale") //$NON-NLS-1$
|| typeName.equals("java.util.Locale")); //$NON-NLS-1$
}
/** Returns the parent method of the given AST node */
@Nullable
public 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 */
@Nullable
public static String getResourceForFirstArg(
@NonNull lombok.ast.Node method,
@NonNull lombok.ast.Node call) {
assert call instanceof MethodInvocation || call instanceof ConstructorInvocation;
StringTracker tracker = new StringTracker(null, method, call, 0);
method.accept(tracker);
return tracker.getFormatStringName();
}
/** Returns the resource name corresponding to the given argument in the given call */
@Nullable
public static String getResourceArg(
@NonNull lombok.ast.Node method,
@NonNull lombok.ast.Node call,
int argIndex) {
assert call instanceof MethodInvocation || call instanceof ConstructorInvocation;
StringTracker tracker = new StringTracker(null, method, call, argIndex);
method.accept(tracker);
return tracker.getFormatStringName();
}
/**
* Given a variable reference, finds the original R.string value corresponding to it.
* For example:
* <pre>
* {@code
* String target = "World";
* String hello = getResources().getString(R.string.hello);
* String output = String.format(hello, target);
* }
* </pre>
*
* Given the {@code String.format} call, we want to find out what R.string resource
* corresponds to the first argument, in this case {@code R.string.hello}.
* To do this, we look for R.string references, and track those through assignments
* until we reach the target node.
* <p>
* In addition, it also does some primitive type tracking such that it (in some cases)
* can answer questions about the types of variables. This allows it to check whether
* certain argument types are valid. Note however that it does not do full-blown
* type analysis by checking method call signatures and so on.
*/
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 */
private final Map<String, Class<?>> mTypes = new HashMap<String, Class<?>>();
/** The AST node for the String.format we're interested in */
private final lombok.ast.Node mTargetNode;
private boolean mDone;
@Nullable
private JavaContext mContext;
/**
* Result: the name of the string resource being passed to the
* String.format, if any
*/
private String mName;
public StringTracker(@Nullable JavaContext context, lombok.ast.Node top, lombok.ast.Node targetNode, int argIndex) {
mContext = context;
mTop = top;
mArgIndex = argIndex;
mTargetNode = targetNode;
}
public String getFormatStringName() {
return mName;
}
/** Returns the argument type of the given formatting argument of the
* target node. Note: This is in the formatting string, which is one higher
* than the String.format parameter number, since the first argument is the
* formatting string itself.
*
* @param argument the argument number
* @return the class (such as {@link Integer#TYPE} etc) or null if not known
*/
public Class<?> getArgumentType(int argument) {
Expression arg = getArgument(argument);
if (arg != null) {
// Look up type based on the source code literals
Class<?> type = getType(arg);
if (type != null) {
return type;
}
// If the AST supports type resolution, use that for other types
// of expressions
if (mContext != null) {
return getTypeClass(mContext.getType(arg));
}
}
return null;
}
private static Class<?> getTypeClass(@Nullable TypeDescriptor type) {
if (type != null) {
return getTypeClass(type.getName());
}
return null;
}
private static Class<?> getTypeClass(@Nullable String fqcn) {
if (fqcn == null) {
return null;
} else if (fqcn.equals(TYPE_STRING) || fqcn.equals("String")) { //$NON-NLS-1$
return String.class;
} else if (fqcn.equals(TYPE_INT)) {
return Integer.TYPE;
} else if (fqcn.equals(TYPE_BOOLEAN)) {
return Boolean.TYPE;
} else if (fqcn.equals(TYPE_NULL)) {
return Object.class;
} else if (fqcn.equals(TYPE_LONG)) {
return Long.TYPE;
} else if (fqcn.equals(TYPE_FLOAT)) {
return Float.TYPE;
} else if (fqcn.equals(TYPE_DOUBLE)) {
return Double.TYPE;
} else if (fqcn.equals(TYPE_CHAR)) {
return Character.TYPE;
} else if (fqcn.equals("BigDecimal") //$NON-NLS-1$
|| fqcn.equals("java.math.BigDecimal")) { //$NON-NLS-1$
return Float.TYPE;
} else if (fqcn.equals("BigInteger") //$NON-NLS-1$
|| fqcn.equals("java.math.BigInteger")) { //$NON-NLS-1$
return Integer.TYPE;
} else if (fqcn.equals(TYPE_OBJECT)) {
return null;
} else if (fqcn.startsWith("java.lang.")) {
if (fqcn.equals("java.lang.Integer")
|| fqcn.equals("java.lang.Short")
|| fqcn.equals("java.lang.Byte")
|| fqcn.equals("java.lang.Long")) {
return Integer.TYPE;
} else if (fqcn.equals("java.lang.Float")
|| fqcn.equals("java.lang.Double")) {
return Float.TYPE;
} else {
return null;
}
} else if (fqcn.equals(TYPE_BYTE)) {
return Byte.TYPE;
} else if (fqcn.equals(TYPE_SHORT)) {
return Short.TYPE;
} else {
return null;
}
}
public Expression getArgument(int argument) {
if (!(mTargetNode instanceof MethodInvocation)) {
return null;
}
MethodInvocation call = (MethodInvocation) mTargetNode;
StrictListAccessor<Expression, MethodInvocation> args = call.astArguments();
if (argument >= args.size()) {
return null;
}
Iterator<Expression> iterator = args.iterator();
int index = 0;
while (iterator.hasNext()) {
Expression arg = iterator.next();
if (index++ == argument) {
return arg;
}
}
return null;
}
@Override
public boolean visitNode(lombok.ast.Node node) {
return mDone || super.visitNode(node);
}
@Override
public boolean visitVariableReference(VariableReference node) {
if (node.astIdentifier().astValue().equals(R_CLASS) && //$NON-NLS-1$
node.getParent() instanceof Select &&
node.getParent().getParent() instanceof Select) {
// See if we're on the right hand side of an assignment
lombok.ast.Node current = node.getParent().getParent();
String reference = ((Select) current).astIdentifier().astValue();
while (current != mTop && !(current instanceof VariableDefinitionEntry)) {
if (current == mTargetNode) {
mName = reference;
mDone = true;
return false;
}
current = current.getParent();
}
if (current instanceof VariableDefinitionEntry) {
VariableDefinitionEntry entry = (VariableDefinitionEntry) current;
String variable = entry.astName().astValue();
mMap.put(variable, reference);
}
}
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()) {
Expression next = iterator.next();
if (next != null && mContext != null && iterator.hasNext()) {
TypeDescriptor type = mContext.getType(next);
if (isLocaleReference(type)) {
next = iterator.next();
} else if (type == null
&& next.toString().startsWith("Locale.")) { //$NON-NLS-1$
next = iterator.next();
}
}
return next;
}
return null;
}
@Override
public boolean visitMethodInvocation(MethodInvocation node) {
if (node == mTargetNode) {
Expression arg = getTargetArgument();
if (arg instanceof VariableReference) {
VariableReference reference = (VariableReference) arg;
String variable = reference.astIdentifier().astValue();
mName = mMap.get(variable);
mDone = true;
return true;
}
}
// Is this a getString() call? On a resource object? If so,
// promote the resource argument up to the left hand side
return super.visitMethodInvocation(node);
}
@Override
public boolean visitConstructorInvocation(ConstructorInvocation node) {
if (node == mTargetNode) {
Expression arg = getTargetArgument();
if (arg instanceof VariableReference) {
VariableReference reference = (VariableReference) arg;
String variable = reference.astIdentifier().astValue();
mName = mMap.get(variable);
mDone = true;
return true;
}
}
// Is this a getString() call? On a resource object? If so,
// promote the resource argument up to the left hand side
return super.visitConstructorInvocation(node);
}
@Override
public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) {
String name = node.astName().astValue();
Expression rhs = node.astInitializer();
Class<?> type = getType(rhs);
if (type != null) {
mTypes.put(name, type);
} else {
// Make sure we're not visiting the String.format node itself. If you have
// msg = String.format("%1$s", msg)
// then we'd be wiping out the type of "msg" before visiting the
// String.format call!
if (rhs != mTargetNode) {
mTypes.remove(name);
}
}
return super.visitVariableDefinitionEntry(node);
}
private Class<?> getType(Expression expression) {
if (expression == null) {
return null;
}
if (expression instanceof VariableReference) {
VariableReference reference = (VariableReference) expression;
String variable = reference.astIdentifier().astValue();
Class<?> type = mTypes.get(variable);
if (type != null) {
return type;
}
} else if (expression instanceof MethodInvocation) {
MethodInvocation method = (MethodInvocation) expression;
String methodName = method.astName().astValue();
if (methodName.equals(GET_STRING_METHOD)) {
return String.class;
}
} else if (expression instanceof StringLiteral) {
return String.class;
} else if (expression instanceof IntegralLiteral) {
return Integer.TYPE;
} else if (expression instanceof FloatingPointLiteral) {
return Float.TYPE;
} else if (expression instanceof CharLiteral) {
return Character.TYPE;
} else if (expression instanceof BooleanLiteral) {
return Boolean.TYPE;
} else if (expression instanceof NullLiteral) {
return Object.class;
}
if (mContext != null) {
TypeDescriptor type = mContext.getType(expression);
if (type != null) {
Class<?> typeClass = getTypeClass(type);
if (typeClass != null) {
return typeClass;
} else {
return Object.class;
}
}
}
return null;
}
}
}