| /* |
| * 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 com.android.SdkConstants.FORMAT_METHOD |
| import com.android.tools.lint.checks.DateFormatDetector.Companion.LOCALE_CLS |
| import com.android.tools.lint.client.api.LintClient |
| import com.android.tools.lint.client.api.TYPE_STRING |
| 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.Implementation |
| import com.android.tools.lint.detector.api.Issue |
| import com.android.tools.lint.detector.api.JavaContext |
| import com.android.tools.lint.detector.api.LintFix |
| 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.android.tools.lint.detector.api.SourceCodeScanner |
| import com.android.tools.lint.detector.api.getMethodName |
| import com.intellij.psi.PsiMethod |
| import org.jetbrains.uast.UCallExpression |
| import org.jetbrains.uast.UElement |
| import org.jetbrains.uast.UField |
| import org.jetbrains.uast.UThrowExpression |
| import org.jetbrains.uast.getParentOfType |
| |
| /** |
| * Checks for errors related to locale handling |
| */ |
| /** Constructs a new [LocaleDetector] */ |
| class LocaleDetector : Detector(), SourceCodeScanner { |
| |
| override fun getApplicableMethodNames(): List<String>? { |
| return listOf( |
| TO_LOWER_CASE, |
| TO_UPPER_CASE, |
| FORMAT_METHOD, |
| GET_DEFAULT, |
| CAPITALIZE, |
| DECAPITALIZE |
| ) |
| } |
| |
| override fun visitMethodCall( |
| context: JavaContext, |
| node: UCallExpression, |
| method: PsiMethod |
| ) { |
| if (method.name == GET_DEFAULT) { |
| if (context.evaluator.isMemberInClass(method, LOCALE_CLS)) { |
| checkLocaleGetDefault(context, method, node) |
| } |
| return |
| } |
| |
| if (context.evaluator.isMemberInClass(method, TYPE_STRING)) { |
| when (method.name) { |
| FORMAT_METHOD -> checkFormat(context, method, node) |
| TO_LOWER_CASE, TO_UPPER_CASE -> checkJavaToUpperLowerCase(context, method, node) |
| } |
| } |
| |
| if (context.evaluator.isMemberInClass(method, KOTLIN_STRINGS_JVM_KT)) { |
| when (method.name) { |
| CAPITALIZE, DECAPITALIZE -> checkStringsKt(context, method, node) |
| TO_LOWER_CASE, TO_UPPER_CASE -> checkStringsKt(context, method, node) |
| } |
| } |
| } |
| |
| private fun checkJavaToUpperLowerCase( |
| context: JavaContext, |
| method: PsiMethod, |
| node: UCallExpression |
| ) { |
| // In the IDE, don't flag java toUpperCase/toLowerCase; these |
| // are already flagged by built-in IDE inspections, so we don't |
| // want duplicate warnings. |
| if (LintClient.isStudio) return |
| if (method.parameterList.parametersCount != 0) return |
| val location = context.getNameLocation(node) |
| val message = String.format( |
| "Implicitly using the default locale is a common source of bugs: " + |
| "Use `%1\$s(Locale)` instead. For strings meant to be internal " + |
| "use `Locale.ROOT`, otherwise `Locale.getDefault()`.", |
| method.name |
| ) |
| context.report(STRING_LOCALE, node, location, message) |
| } |
| |
| private fun checkStringsKt( |
| context: JavaContext, |
| method: PsiMethod, |
| node: UCallExpression |
| ) { |
| if (method.parameterList.parametersCount > 1) return |
| val location = context.getNameLocation(node) |
| val message = String.format( |
| "Implicitly using the default locale is a common source of bugs: " + |
| "Use `%1\$s(Locale)` instead. For strings meant to be internal " + |
| "use `Locale.ROOT`, otherwise `Locale.getDefault()`.", |
| method.name |
| ) |
| |
| val range = context.getCallLocation(node, includeReceiver = false, includeArguments = true) |
| val quickfixData = LintFix.create().group().also { groupBuilder -> |
| for (localeName in listOf("ROOT", "getDefault()")) { |
| groupBuilder.add(LintFix.create() |
| .name("Replace with `${method.name}(Locale.$localeName)`") |
| .sharedName("Use explicit locale") |
| .replace() |
| .range(range) |
| .with("${method.name}(java.util.Locale.$localeName)") |
| .shortenNames() |
| .build()) |
| } |
| }.build() |
| context.report(STRING_LOCALE, node, location, message, quickfixData) |
| } |
| |
| private fun checkFormat( |
| context: JavaContext, |
| method: PsiMethod, |
| call: UCallExpression |
| ) { |
| // Only check the non-locale version of String.format |
| if (method.parameterList.parametersCount == 0 || !context.evaluator.parameterHasType( |
| method, |
| 0, |
| TYPE_STRING |
| ) |
| ) { |
| return |
| } |
| |
| val expressions = call.valueArguments |
| if (expressions.isEmpty()) { |
| return |
| } |
| |
| // Find the formatting string |
| val first = expressions[0] |
| val value = ConstantEvaluator.evaluate(context, first) as? String ?: return |
| |
| if (StringFormatDetector.isLocaleSpecific(value)) { |
| if (isLoggingParameter(context, call)) { |
| return |
| } |
| |
| if (call.getParentOfType<UElement>(UThrowExpression::class.java, true) != null) { |
| return |
| } |
| |
| val location: Location = if (FORMAT_METHOD == getMethodName(call)) { |
| // For String#format, include receiver (String), but not for .toUppercase etc |
| // since the receiver can often be a complex expression |
| context.getCallLocation(call, true, true) |
| } else { |
| context.getCallLocation(call, false, true) |
| } |
| val message = |
| "Implicitly using the default locale is a common source of bugs: " + |
| "Use `String.format(Locale, ...)` instead" |
| context.report(STRING_LOCALE, call, location, message) |
| } |
| } |
| |
| private fun checkLocaleGetDefault( |
| context: JavaContext, |
| @Suppress("UNUSED_PARAMETER") |
| method: PsiMethod, |
| node: UCallExpression |
| ) { |
| val field = node.getParentOfType<UField>(UField::class.java, true) ?: return |
| |
| val evaluator = context.evaluator |
| if (evaluator.isStatic(field) && evaluator.isFinal(field)) { |
| context.report( |
| FINAL_LOCALE, |
| node, |
| context.getLocation(node), |
| "Assigning `Locale.getDefault()` to a final static field is suspicious; " + |
| "this code will not work correctly if the user changes locale while " + |
| "the app is running" |
| ) |
| } |
| } |
| |
| /** Returns true if the given node is a parameter to a Logging call */ |
| private fun isLoggingParameter( |
| context: JavaContext, |
| node: UCallExpression |
| ): Boolean { |
| val parentCall = |
| node.getParentOfType<UCallExpression>(UCallExpression::class.java, true) |
| if (parentCall != null) { |
| val name = getMethodName(parentCall) |
| |
| if (name != null && name.length == 1) { // "d", "i", "e" etc in Log |
| val method = parentCall.resolve() |
| return context.evaluator.isMemberInClass(method, LogDetector.LOG_CLS) |
| } |
| } |
| |
| return false |
| } |
| |
| companion object { |
| private val IMPLEMENTATION = |
| Implementation(LocaleDetector::class.java, Scope.JAVA_FILE_SCOPE) |
| |
| const val TO_UPPER_CASE = "toUpperCase" |
| const val TO_LOWER_CASE = "toLowerCase" |
| const val GET_DEFAULT = "getDefault" |
| const val KOTLIN_STRINGS_JVM_KT = "kotlin.text.StringsKt__StringsJVMKt" |
| const val CAPITALIZE = "capitalize" |
| const val DECAPITALIZE = "decapitalize" |
| |
| /** Calling risky convenience methods */ |
| @JvmField |
| val STRING_LOCALE = Issue.create( |
| id = "DefaultLocale", |
| briefDescription = "Implied default locale in case conversion", |
| explanation = """ |
| 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`. |
| |
| 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. |
| """, |
| moreInfo = "https://developer.android.com/reference/java/util/Locale.html#default_locale", |
| category = Category.CORRECTNESS, |
| priority = 6, |
| severity = Severity.WARNING, |
| implementation = IMPLEMENTATION |
| ) |
| |
| /** Assuming locale doesn't change */ |
| @JvmField |
| val FINAL_LOCALE = Issue.create( |
| id = "ConstantLocale", |
| briefDescription = "Constant Locale", |
| explanation = """ |
| Assigning `Locale.getDefault()` to a constant is suspicious, because \ |
| the locale can change while the app is running.""", |
| category = Category.I18N, |
| priority = 6, |
| severity = Severity.WARNING, |
| androidSpecific = true, |
| implementation = IMPLEMENTATION |
| ) |
| } |
| } |