1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text.lint
18 
19 import com.android.tools.lint.detector.api.Category
20 import com.android.tools.lint.detector.api.Detector
21 import com.android.tools.lint.detector.api.Implementation
22 import com.android.tools.lint.detector.api.Issue
23 import com.android.tools.lint.detector.api.JavaContext
24 import com.android.tools.lint.detector.api.LintFix
25 import com.android.tools.lint.detector.api.Scope
26 import com.android.tools.lint.detector.api.Severity
27 import com.android.tools.lint.detector.api.SourceCodeScanner
28 import com.intellij.psi.PsiMethod
29 import java.util.EnumSet
30 import org.jetbrains.uast.UCallExpression
31 import org.jetbrains.uast.getParameterForArgument
32 import org.jetbrains.uast.skipParenthesizedExprDown
33 
34 class LocaleInvalidLanguageTagDetector : Detector(), SourceCodeScanner {
getApplicableConstructorTypesnull35     override fun getApplicableConstructorTypes() =
36         listOf("androidx.compose.ui.text.intl.LocaleList", "androidx.compose.ui.text.intl.Locale")
37 
38     override fun visitConstructor(
39         context: JavaContext,
40         node: UCallExpression,
41         constructor: PsiMethod
42     ) {
43         val languageTag =
44             node.valueArguments
45                 .find {
46                     val name = node.getParameterForArgument(it)?.name
47                     name == "languageTag" || name == "languageTags"
48                 }
49                 ?.skipParenthesizedExprDown()
50 
51         val localeValue = languageTag?.evaluate() as? String ?: return
52         val localeInvalid = localeValue.contains('_')
53 
54         if (localeInvalid) {
55             val fixedLocaleValue = localeValue.replace('_', '-')
56             context.report(
57                 InvalidLanguageTagDelimiter,
58                 context.getLocation(languageTag),
59                 "A hyphen (-), not an underscore (_) delimiter should be used in a language tag",
60                 LintFix.create()
61                     .replace()
62                     .name("Change $localeValue to $fixedLocaleValue")
63                     .text(localeValue)
64                     .with(fixedLocaleValue)
65                     .autoFix()
66                     .build()
67             )
68         }
69     }
70 
71     companion object {
72         val InvalidLanguageTagDelimiter =
73             Issue.create(
74                 id = "InvalidLanguageTagDelimiter",
75                 briefDescription = "Undercore (_) is an unsupported delimiter for subtags",
76                 explanation =
77                     "A language tag must be compliant with IETF BCP47, specifically a " +
78                         "sequence of subtags must be separated by hyphens (-) instead of underscores (_)",
79                 category = Category.CORRECTNESS,
80                 priority = 3,
81                 severity = Severity.ERROR,
82                 implementation =
83                     Implementation(
84                         LocaleInvalidLanguageTagDetector::class.java,
85                         EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
86                     )
87             )
88     }
89 }
90