blob: b216b055031b34695aa0e4d3ebbf8c7de177b9f1 [file] [log] [blame]
/*
* Copyright (C) 2012 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.FORMAT_METHOD;
import static com.android.tools.lint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
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.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.util.PsiTreeUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Checks for errors related to locale handling
*/
public class LocaleDetector extends Detector implements JavaPsiScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LocaleDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Calling risky convenience methods */
public static final Issue STRING_LOCALE = Issue.create(
"DefaultLocale", //$NON-NLS-1$
"Implied default locale in case conversion",
"Calling `String#toLowerCase()` or `#toUpperCase()` *without specifying an " +
"explicit locale* is a common source of bugs. The reason for that is that those " +
"methods will use the current locale on the user's device, and even though the " +
"code appears to work correctly when you are developing the app, it will fail " +
"in some locales. For example, in the Turkish locale, the uppercase replacement " +
"for `i` is *not* `I`.\n" +
"\n" +
"If you want the methods to just perform ASCII replacement, for example to convert " +
"an enum name, call `String#toUpperCase(Locale.US)` instead. If you really want to " +
"use the current locale, call `String#toUpperCase(Locale.getDefault())` instead.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/reference/java/util/Locale.html#default_locale"); //$NON-NLS-1$
/** Constructs a new {@link LocaleDetector} */
public LocaleDetector() {
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
if (LintClient.isStudio()) {
// In the IDE, don't flag toUpperCase/toLowerCase; these
// are already flagged by built-in IDE inspections, so we don't
// want duplicate warnings.
return Collections.singletonList(FORMAT_METHOD);
} else {
return Arrays.asList(
// Only when not running in the IDE
"toLowerCase", //$NON-NLS-1$
"toUpperCase", //$NON-NLS-1$
FORMAT_METHOD
);
}
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
@NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) {
if (context.getEvaluator().isMemberInClass(method, TYPE_STRING)) {
String name = method.getName();
if (name.equals(FORMAT_METHOD)) {
checkFormat(context, method, call);
} else if (method.getParameterList().getParametersCount() == 0) {
Location location = context.getNameLocation(call);
String message = String.format(
"Implicitly using the default locale is a common source of bugs: " +
"Use `%1$s(Locale)` instead", name);
context.report(STRING_LOCALE, call, location, message);
}
}
}
/** Returns true if the given node is a parameter to a Logging call */
private static boolean isLoggingParameter(
@NonNull JavaContext context,
@NonNull PsiMethodCallExpression node) {
PsiMethodCallExpression parentCall =
PsiTreeUtil.getParentOfType(node, PsiMethodCallExpression.class, true);
if (parentCall != null) {
String name = parentCall.getMethodExpression().getReferenceName();
if (name != null && name.length() == 1) { // "d", "i", "e" etc in Log
PsiMethod method = parentCall.resolveMethod();
return context.getEvaluator().isMemberInClass(method, LogDetector.LOG_CLS);
}
}
return false;
}
private static void checkFormat(
@NonNull JavaContext context,
@NonNull PsiMethod method,
@NonNull PsiMethodCallExpression call) {
// Only check the non-locale version of String.format
if (method.getParameterList().getParametersCount() == 0
|| !context.getEvaluator().parameterHasType(method, 0, TYPE_STRING)) {
return;
}
PsiExpression[] expressions = call.getArgumentList().getExpressions();
if (expressions.length == 0) {
return;
}
// Find the formatting string
PsiExpression first = expressions[0];
Object value = ConstantEvaluator.evaluate(context, first);
if (!(value instanceof String)) {
return;
}
String format = (String) value;
if (StringFormatDetector.isLocaleSpecific(format)) {
if (isLoggingParameter(context, call)) {
return;
}
Location location = context.getLocation(call);
String message =
"Implicitly using the default locale is a common source of bugs: " +
"Use `String.format(Locale, ...)` instead";
context.report(STRING_LOCALE, call, location, message);
}
}
}