1 /*
2  * Copyright 2023 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.lint
20 
21 import com.android.tools.lint.client.api.UElementHandler
22 import com.android.tools.lint.detector.api.Category
23 import com.android.tools.lint.detector.api.Detector
24 import com.android.tools.lint.detector.api.Implementation
25 import com.android.tools.lint.detector.api.Issue
26 import com.android.tools.lint.detector.api.JavaContext
27 import com.android.tools.lint.detector.api.Scope
28 import com.android.tools.lint.detector.api.Severity
29 import com.android.tools.lint.detector.api.SourceCodeScanner
30 import com.intellij.lang.jvm.types.JvmType
31 import com.intellij.psi.PsiClass
32 import com.intellij.psi.PsiParameter
33 import com.intellij.psi.PsiPrimitiveType
34 import com.intellij.psi.PsiWildcardType
35 import com.intellij.psi.impl.source.PsiClassReferenceType
36 import java.util.EnumSet
37 import org.jetbrains.kotlin.asJava.elements.KtLightField
38 import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
39 import org.jetbrains.kotlin.psi.KtParameter
40 import org.jetbrains.uast.UElement
41 import org.jetbrains.uast.UField
42 import org.jetbrains.uast.UMethod
43 import org.jetbrains.uast.UVariable
44 import org.jetbrains.uast.getContainingUClass
45 
46 /**
47  * This detects uses of Set, List, and Map with primitive type arguments. Internally, these should
48  * be replaced by the primitive-specific collections in androidx.
49  */
50 class PrimitiveInCollectionDetector : Detector(), SourceCodeScanner {
getApplicableUastTypesnull51     override fun getApplicableUastTypes() =
52         listOf<Class<out UElement>>(UMethod::class.java, UVariable::class.java)
53 
54     override fun createUastHandler(context: JavaContext) =
55         object : UElementHandler() {
56             override fun visitMethod(node: UMethod) {
57                 if (
58                     context.evaluator.isOverride(node) || node.isDataClassGeneratedMethod(context)
59                 ) {
60                     return
61                 }
62 
63                 val primitiveCollection = node.returnType?.primitiveCollectionReplacement(context)
64                 if (primitiveCollection != null) {
65                     // The location doesn't appear to work with property types with getters rather
66                     // than
67                     // full fields. Target the property name instead if we don't have a location.
68                     val target =
69                         if (context.getLocation(node.returnTypeReference).start == null) {
70                             node
71                         } else {
72                             node.returnTypeReference
73                         }
74                     report(
75                         context,
76                         node,
77                         target,
78                         "return type ${node.returnType?.presentableText} of ${node.name}:" +
79                             " replace with $primitiveCollection"
80                     )
81                 }
82             }
83 
84             override fun visitVariable(node: UVariable) {
85                 // Kotlin destructuring expression is desugared. E.g.,
86                 //
87                 //   val (x, y) = pair
88                 //
89                 // is mapped to
90                 //
91                 //   val varHash = pair // temp variable
92                 //   val x = varHash.component1()
93                 //   val y = varHash.component2()
94                 //
95                 // and thus we don't need to analyze the temporary variable.
96                 // Their `sourcePsi`s are different:
97                 //   KtDestructuringDeclaration (for overall expression) v.s.
98                 //   KtDestructuringDeclarationEntry (for individual local variables)
99                 if (node.sourcePsi is KtDestructuringDeclaration) return
100 
101                 val primitiveCollection =
102                     node.type.primitiveCollectionReplacement(context) ?: return
103                 if (node.isLambdaParameter()) {
104                     // Don't notify for lambda parameters. We'll be notifying for the method
105                     // that accepts the lambda, so we already have it flagged there. The
106                     // person using it doesn't really have a choice about the parameters that
107                     // are passed.
108                     return
109                 }
110                 val parent = node.uastParent
111                 val messageContext =
112                     if (parent is UMethod) {
113                         // Data class constructor parameters are caught 4 times:
114                         // 1) constructor method parameter
115                         // 2) the field of the backing 'val'
116                         // 3) the getter for the field
117                         // 4) the generated copy() method.
118                         // We can eliminate the copy() at least, even if we get duplicates for the
119                         // other 3. It would be ideal to eliminate 2 of the other 3, but it isn't
120                         // easy to do and still catch all uses.
121                         if (
122                             context.evaluator.isOverride(parent) ||
123                                 (context.evaluator.isData(parent) && parent.name.startsWith("copy"))
124                         ) {
125                             return
126                         }
127                         val methodName =
128                             if (parent.isConstructor) {
129                                 "constructor ${parent.getContainingUClass()?.name}"
130                             } else {
131                                 "method ${parent.name}"
132                             }
133                         "$methodName has parameter ${node.name} " +
134                             "with type ${node.type.presentableText}: replace with $primitiveCollection"
135                     } else {
136                         val varOrField = if (node is UField) "field" else "variable"
137 
138                         "$varOrField ${node.name} with type ${node.type.presentableText}: replace" +
139                             " with $primitiveCollection"
140                     }
141                 report(context, node, node.typeReference, messageContext)
142             }
143         }
144 
reportnull145     private fun report(context: JavaContext, node: UElement, target: Any?, message: String) {
146         val location =
147             if (target == null) {
148                 context.getLocation(node)
149             } else {
150                 context.getLocation(target)
151             }
152         context.report(issue = ISSUE, scope = node, location = location, message = message)
153     }
154 
155     companion object {
156         private const val PrimitiveInCollectionId = "PrimitiveInCollection"
157 
158         val ISSUE =
159             Issue.create(
160                 id = PrimitiveInCollectionId,
161                 briefDescription =
162                     "A primitive (Short, Int, Long, Char, Float, Double) or " +
163                         "a value class wrapping a primitive was used as in a collection. Primitive " +
164                         "versions of collections exist.",
165                 explanation =
166                     "Using a primitive type in a collection will autobox the primitive " +
167                         "value, causing an allocation. To avoid the allocation, use one of the androidx " +
168                         "collections designed for use with primitives. For example instead of Set<Int>, " +
169                         "use IntSet.",
170                 category = Category.PERFORMANCE,
171                 priority = 3,
172                 severity = Severity.ERROR,
173                 implementation =
174                     Implementation(
175                         PrimitiveInCollectionDetector::class.java,
176                         EnumSet.of(Scope.JAVA_FILE)
177                     )
178             )
179     }
180 }
181 
182 private val SetType = java.util.Set::class.java.canonicalName
183 private val ListType = java.util.List::class.java.canonicalName
184 private val MapType = java.util.Map::class.java.canonicalName
185 
186 // Map from the kotlin type to the primitive type used in the collection
187 // e.g. Set<Byte> -> IntSet
188 private val BoxedTypeToSuggestedPrimitive =
189     mapOf(
190         "java.lang.Byte" to "Int",
191         "java.lang.Character" to "Int",
192         "java.lang.Short" to "Int",
193         "java.lang.Integer" to "Int",
194         "java.lang.Long" to "Long",
195         "java.lang.Float" to "Float",
196         "java.lang.Double" to "Float",
197         "kotlin.UByte" to "Int",
198         "kotlin.UShort" to "Int",
199         "kotlin.UInt" to "Int",
200         "kotlin.ULong" to "Long",
201     )
202 
primitiveCollectionReplacementnull203 private fun JvmType.primitiveCollectionReplacement(context: JavaContext): String? {
204     if (this !is PsiClassReferenceType) return null
205     val resolved = resolve() ?: return null
206     val evaluator = context.evaluator
207     if (evaluator.inheritsFrom(resolved, SetType, false)) {
208         val typeArgument = typeArguments().firstOrNull() ?: return null
209         val elementPrimitive = typeArgument.primitiveName()
210         if (elementPrimitive != null) {
211             return "${elementPrimitive}Set"
212         }
213     } else if (evaluator.inheritsFrom(resolved, ListType, false)) {
214         val typeArgument = typeArguments().firstOrNull() ?: return null
215         val elementPrimitive = typeArgument.primitiveName()
216         if (elementPrimitive != null) {
217             return "${elementPrimitive}List"
218         }
219     } else if (evaluator.inheritsFrom(resolved, MapType, false)) {
220         val keyType = typeArguments().firstOrNull() ?: return null
221         val valueType = typeArguments().lastOrNull() ?: return null
222         val keyPrimitive = keyType.primitiveName()
223         val valuePrimitive = valueType.primitiveName()
224         if (keyPrimitive != null) {
225             return if (valuePrimitive != null) {
226                 "$keyPrimitive${valuePrimitive}Map"
227             } else {
228                 "${keyPrimitive}ObjectMap"
229             }
230         } else if (valuePrimitive != null) {
231             return "Object${valuePrimitive}Map"
232         }
233     }
234     return null
235 }
236 
JvmTypenull237 private fun JvmType.primitiveName(): String? =
238     when (this) {
239         is PsiClassReferenceType -> toPrimitiveName()
240         is PsiWildcardType -> {
241             val bound =
242                 if (isBounded) {
243                     bound!!
244                 } else {
245                     superBound
246                 }
247             when (bound) {
248                 is PsiClassReferenceType -> bound.toPrimitiveName()
249                 is PsiPrimitiveType -> BoxedTypeToSuggestedPrimitive[bound.boxedTypeName]
250                 else -> null
251             }
252         }
253         else -> null
254     }
255 
toPrimitiveNamenull256 private fun PsiClassReferenceType.toPrimitiveName(): String? {
257     val resolvedType = resolve() ?: return null
258     if (hasJvmInline(resolvedType)) {
259         // Depending on where the inline class is coming from, there are a couple places to check
260         // for what the value is.
261         val valueType =
262             // For value classes in this compilation, find the field which corresponds to the value
263             // class constructor parameter (the constructor is not modeled in k2 psi). There may be
264             // other fields (a companion object and companion object fields).
265             resolvedType.fields
266                 .singleOrNull { (it as? KtLightField)?.kotlinOrigin is KtParameter }
267                 ?.type
268                 // For value classes from a different compilation, the fields may not have a kotlin
269                 // origin attached, so there's nothing to differentiate companion fields and the
270                 // value class field. Instead find the "constructor-impl" method (which is not
271                 // present for value classes from this compilation).
272                 ?: resolvedType.methods
273                     .firstOrNull { it.parameters.size == 1 && it.name == "constructor-impl" }
274                     ?.parameters
275                     ?.first()
276                     ?.type
277         if (valueType is PsiPrimitiveType) {
278             return BoxedTypeToSuggestedPrimitive[valueType.boxedTypeName]
279         }
280         if (valueType is PsiClassReferenceType) {
281             return valueType.toPrimitiveName()
282         }
283     }
284     return BoxedTypeToSuggestedPrimitive[resolvedType.qualifiedName]
285 }
286 
287 private val JvmInlineAnnotation = JvmInline::class.qualifiedName!!
288 
isDataClassGeneratedMethodnull289 private fun UMethod.isDataClassGeneratedMethod(context: JavaContext): Boolean =
290     context.evaluator.isData(containingClass) &&
291         (name.startsWith("copy") || name.startsWith("component"))
292 
293 private fun UVariable.isLambdaParameter(): Boolean {
294     val sourcePsi = sourcePsi
295     return ((sourcePsi == null && (javaPsi as? PsiParameter)?.name == "it") ||
296         (sourcePsi as? KtParameter)?.isLambdaParameter == true)
297 }
298 
hasJvmInlinenull299 private fun hasJvmInline(type: PsiClass): Boolean {
300     for (annotation in type.annotations) {
301         if (annotation.qualifiedName == JvmInlineAnnotation) {
302             return true
303         }
304     }
305     return false
306 }
307