1 /*
<lambda>null2  * Copyright 2020 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 @file:Suppress("UnstableApiUsage")
18 
19 package androidx.compose.ui.lint
20 
21 import androidx.compose.lint.Names
22 import androidx.compose.lint.inheritsFrom
23 import androidx.compose.lint.isComposable
24 import androidx.compose.lint.returnsUnit
25 import com.android.tools.lint.client.api.UElementHandler
26 import com.android.tools.lint.detector.api.Category
27 import com.android.tools.lint.detector.api.Detector
28 import com.android.tools.lint.detector.api.Implementation
29 import com.android.tools.lint.detector.api.Issue
30 import com.android.tools.lint.detector.api.JavaContext
31 import com.android.tools.lint.detector.api.LintFix
32 import com.android.tools.lint.detector.api.Scope
33 import com.android.tools.lint.detector.api.Severity
34 import com.android.tools.lint.detector.api.SourceCodeScanner
35 import java.util.EnumSet
36 import java.util.Locale
37 import org.jetbrains.kotlin.psi.KtNameReferenceExpression
38 import org.jetbrains.kotlin.psi.KtParameter
39 import org.jetbrains.uast.UElement
40 import org.jetbrains.uast.UMethod
41 
42 /**
43  * [Detector] that checks Composable functions with Modifiers parameters for consistency with
44  * guidelines.
45  *
46  * For functions with one / more modifier parameters, the first modifier parameter must:
47  * - Be named `modifier`
48  * - Have a type of `Modifier`
49  * - Either have no default value, or have a default value of `Modifier`
50  * - If optional, be the first optional parameter in the parameter list
51  */
52 class ModifierParameterDetector : Detector(), SourceCodeScanner {
53     override fun getApplicableUastTypes() = listOf(UMethod::class.java)
54 
55     override fun createUastHandler(context: JavaContext) =
56         object : UElementHandler() {
57             override fun visitMethod(node: UMethod) {
58                 // Ignore non-composable functions
59                 if (!node.isComposable) return
60 
61                 // Ignore non-unit composable functions
62                 if (!node.returnsUnit) return
63 
64                 val modifierParameter =
65                     node.uastParameters.firstOrNull { parameter ->
66                         parameter.sourcePsi is KtParameter &&
67                             parameter.type.inheritsFrom(Names.Ui.Modifier)
68                     } ?: return
69 
70                 // Need to strongly type this or else Kotlinc cannot resolve overloads for
71                 // getNameLocation
72                 val modifierParameterElement: UElement = modifierParameter
73 
74                 val source = modifierParameter.sourcePsi as KtParameter
75 
76                 val modifierName = Names.Ui.Modifier.shortName
77 
78                 if (modifierParameter.name != ModifierParameterName) {
79                     context.report(
80                         ModifierParameter,
81                         modifierParameterElement,
82                         context.getNameLocation(modifierParameterElement),
83                         "$modifierName parameter should be named $ModifierParameterName",
84                         LintFix.create()
85                             .replace()
86                             .name("Change name to $ModifierParameterName")
87                             .text(modifierParameter.name)
88                             .with(ModifierParameterName)
89                             .autoFix()
90                             .build()
91                     )
92                 }
93 
94                 if (modifierParameter.type.canonicalText != Names.Ui.Modifier.javaFqn) {
95                     context.report(
96                         ModifierParameter,
97                         modifierParameterElement,
98                         context.getNameLocation(modifierParameterElement),
99                         "$modifierName parameter should have a type of $modifierName",
100                         LintFix.create()
101                             .replace()
102                             .range(context.getLocation(modifierParameterElement))
103                             .name("Change type to $modifierName")
104                             .text(source.typeReference!!.text)
105                             .with(modifierName)
106                             .autoFix()
107                             .build()
108                     )
109                 }
110 
111                 if (source.hasDefaultValue()) {
112                     val defaultValue = source.defaultValue!!
113                     // If the default value is not a reference expression, then it isn't `Modifier`
114                     // anyway and we can just report an error
115                     val referenceExpression = source.defaultValue as? KtNameReferenceExpression
116                     if (referenceExpression?.getReferencedName() != modifierName) {
117                         context.report(
118                             ModifierParameter,
119                             modifierParameterElement,
120                             context.getNameLocation(modifierParameterElement),
121                             "Optional $modifierName parameter should have a default value " +
122                                 "of `$modifierName`",
123                             LintFix.create()
124                                 .replace()
125                                 .range(context.getLocation(modifierParameterElement))
126                                 .name("Change default value to $modifierName")
127                                 .text(defaultValue.text)
128                                 .with(modifierName)
129                                 .autoFix()
130                                 .build()
131                         )
132                     }
133                     val index = node.uastParameters.indexOf(modifierParameter)
134                     val optionalParameterIndex =
135                         node.uastParameters.indexOfFirst { parameter ->
136                             (parameter.sourcePsi as? KtParameter)?.hasDefaultValue() == true
137                         }
138                     if (index != optionalParameterIndex) {
139                         context.report(
140                             ModifierParameter,
141                             modifierParameterElement,
142                             context.getNameLocation(modifierParameterElement),
143                             "$modifierName parameter should be the first optional parameter",
144                             // Hard to make a lint fix for this and keep parameter formatting, so
145                             // ignore it
146                         )
147                     }
148                 }
149             }
150         }
151 
152     companion object {
153         val ModifierParameter =
154             Issue.create(
155                 "ModifierParameter",
156                 "Guidelines for Modifier parameters in a Composable function",
157                 "The first (or only) Modifier parameter in a Composable function should follow the " +
158                     "following rules:" +
159                     "\n- Be named `$ModifierParameterName`" +
160                     "\n- Have a type of `${Names.Ui.Modifier.shortName}`" +
161                     "\n- Either have no default value, or have a default value of " +
162                     "`${Names.Ui.Modifier.shortName}`" +
163                     "\n- If optional, be the first optional parameter in the parameter list",
164                 Category.CORRECTNESS,
165                 3,
166                 Severity.WARNING,
167                 Implementation(
168                     ModifierParameterDetector::class.java,
169                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
170                 )
171             )
172     }
173 }
174 
175 private val ModifierParameterName =
<lambda>null176     Names.Ui.Modifier.shortName.replaceFirstChar { it.lowercase(Locale.ROOT) }
177