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