/
ImplicitDefaultLocale.kt
113 lines (103 loc) · 4.22 KB
/
ImplicitDefaultLocale.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package io.gitlab.arturbosch.detekt.rules.bugs
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import org.jetbrains.kotlin.builtins.KotlinBuiltIns.isStringOrNullableString
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.resolve.calls.util.getCalleeExpressionIfAny
import org.jetbrains.kotlin.resolve.calls.util.getType
/**
* Prefer passing [java.util.Locale] explicitly than using implicit default value when formatting
* strings or performing a case conversion.
*
* The default locale is almost always inappropriate for machine-readable text like HTTP headers.
* For example, if locale with tag `ar-SA-u-nu-arab` is a current default then `%d` placeholders
* will be evaluated to a number consisting of Eastern-Arabic (non-ASCII) digits.
* [java.util.Locale.US] is recommended for machine-readable output.
*
* <noncompliant>
* String.format("Timestamp: %d", System.currentTimeMillis())
*
* val str: String = getString()
* str.toUpperCase()
* str.toLowerCase()
* </noncompliant>
*
* <compliant>
* String.format(Locale.US, "Timestamp: %d", System.currentTimeMillis())
*
* val str: String = getString()
* str.toUpperCase(Locale.US)
* str.toLowerCase(Locale.US)
* </compliant>
*/
@Suppress("ViolatesTypeResolutionRequirements")
@ActiveByDefault(since = "1.16.0")
class ImplicitDefaultLocale(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
"ImplicitDefaultLocale",
Severity.CodeSmell,
"Implicit default locale used for string processing. Consider using explicit locale.",
Debt.FIVE_MINS
)
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
super.visitDotQualifiedExpression(expression)
checkStringFormatting(expression)
checkCaseConversion(expression)
}
override fun visitSafeQualifiedExpression(expression: KtSafeQualifiedExpression) {
super.visitSafeQualifiedExpression(expression)
checkStringFormatting(expression)
checkCaseConversion(expression)
}
private fun checkStringFormatting(expression: KtQualifiedExpression) {
if (expression.receiverExpression.text == "String" &&
expression.getCalleeExpressionIfAny()?.text == "format" &&
expression.containsStringTemplate()
) {
report(
CodeSmell(
issue,
Entity.from(expression),
"${expression.text} uses implicitly default locale for string formatting."
)
)
}
}
private fun checkCaseConversion(expression: KtQualifiedExpression) {
if (isStringOrNullableString(expression.receiverExpression.getType(bindingContext)) &&
expression.isCalleeCaseConversion() &&
expression.isCalleeNoArgs()
) {
report(
CodeSmell(
issue,
Entity.from(expression),
"${expression.text} uses implicitly default locale for case conversion."
)
)
}
}
}
private fun KtQualifiedExpression.isCalleeCaseConversion(): Boolean {
return getCalleeExpressionIfAny()?.text in arrayOf("toLowerCase", "toUpperCase")
}
private fun KtQualifiedExpression.isCalleeNoArgs(): Boolean {
val lastCallExpression = lastChild as? KtCallExpression
return lastCallExpression?.valueArguments.isNullOrEmpty()
}
private fun KtQualifiedExpression.containsStringTemplate(): Boolean {
val lastCallExpression = lastChild as? KtCallExpression
return lastCallExpression?.valueArguments
?.firstOrNull()
?.run { children.firstOrNull() } is KtStringTemplateExpression
}