1 /*
2  * Copyright 2021 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.psi.impl.compiled.ClsMethodImpl
31 import kotlin.metadata.KmClassifier
32 import org.jetbrains.kotlin.asJava.unwrapped
33 import org.jetbrains.kotlin.psi.KtForExpression
34 import org.jetbrains.kotlin.psi.KtNamedFunction
35 import org.jetbrains.uast.UCallExpression
36 import org.jetbrains.uast.UForEachExpression
37 import org.jetbrains.uast.UTypeReferenceExpression
38 import org.jetbrains.uast.toUElement
39 
40 /**
41  * Lint [Detector] to prevent allocating Iterators when iterating on a [List]. Instead of using `for
42  * (e in list)` or `list.forEach {}`, more efficient iteration methods should be used, such as `for
43  * (i in list.indices) { list[i]... }` or `list.fastForEach`.
44  */
45 class ListIteratorDetector : Detector(), SourceCodeScanner {
getApplicableUastTypesnull46     override fun getApplicableUastTypes() =
47         listOf(UForEachExpression::class.java, UCallExpression::class.java)
48 
49     override fun createUastHandler(context: JavaContext) =
50         object : UElementHandler() {
51             override fun visitForEachExpression(node: UForEachExpression) {
52                 // Type of the variable we are iterating on, i.e the type of `b` in `for (a in b)`
53                 val iteratedValueType = node.iteratedValue.getExpressionType()
54                 // We are iterating on a List
55                 if (iteratedValueType?.inheritsFrom(JavaList) == true) {
56                     // Find the `in` keyword to use as location
57                     val inKeyword = (node.sourcePsi as? KtForExpression)?.inKeyword
58                     val location =
59                         if (inKeyword == null) {
60                             context.getNameLocation(node)
61                         } else {
62                             context.getNameLocation(inKeyword)
63                         }
64                     context.report(
65                         ISSUE,
66                         node,
67                         location,
68                         "Creating an unnecessary Iterator to iterate through a List"
69                     )
70                 }
71             }
72 
73             override fun visitCallExpression(node: UCallExpression) {
74                 val receiverType = node.receiverType
75 
76                 // We are calling a method on a `List` type
77                 if (receiverType?.inheritsFrom(JavaList) == true) {
78                     when (val method = node.resolve()?.unwrapped) {
79                         // Parsing a class file
80                         is ClsMethodImpl -> {
81                             method.checkForIterableReceiver(node)
82                         }
83                         // Parsing Kotlin source
84                         is KtNamedFunction -> {
85                             method.checkForIterableReceiver(node)
86                         }
87                     }
88                 }
89             }
90 
91             private fun ClsMethodImpl.checkForIterableReceiver(node: UCallExpression) {
92                 val kmFunction = this.toKmFunction()
93 
94                 kmFunction?.let {
95                     if (it.receiverParameterType?.classifier == KotlinIterableClassifier) {
96                         context.report(
97                             ISSUE,
98                             node,
99                             context.getNameLocation(node),
100                             "Creating an unnecessary Iterator to iterate through a List"
101                         )
102                     }
103                 }
104             }
105 
106             private fun KtNamedFunction.checkForIterableReceiver(node: UCallExpression) {
107                 val receiver = receiverTypeReference
108                 // If there is no receiver, or the receiver isn't an Iterable, ignore
109                 if (
110                     (receiver.toUElement() as? UTypeReferenceExpression)?.getQualifiedName() !=
111                         JavaIterable.javaFqn
112                 )
113                     return
114 
115                 context.report(
116                     ISSUE,
117                     node,
118                     context.getNameLocation(node),
119                     "Creating an unnecessary Iterator to iterate through a List"
120                 )
121             }
122         }
123 
124     companion object {
125         val ISSUE =
126             Issue.create(
127                 "ListIterator",
128                 "Creating an unnecessary Iterator to iterate through a List",
129                 "Iterable<T> extension methods and using `for (a in list)` will create an " +
130                     "Iterator object - in hot code paths this can cause a lot of extra allocations " +
131                     "which is something we want to avoid. Instead, use a method that doesn't " +
132                     "allocate, such as `fastForEach`, or use `for (a in list.indices)` as iterating " +
133                     "through an `IntRange` does not allocate an Iterator, and becomes just a simple " +
134                     "for loop.",
135                 Category.PERFORMANCE,
136                 5,
137                 Severity.ERROR,
138                 Implementation(ListIteratorDetector::class.java, Scope.JAVA_FILE_SCOPE)
139             )
140     }
141 }
142 
143 // Kotlin collections on JVM are just the underlying Java collections
144 private val JavaLangPackageName = Package("java.lang")
145 private val JavaUtilPackageName = Package("java.util")
146 private val JavaList = Name(JavaUtilPackageName, "List")
147 private val JavaIterable = Name(JavaLangPackageName, "Iterable")
148 
149 private val KotlinCollectionsPackageName = Package("kotlin.collections")
150 private val KotlinIterable = Name(KotlinCollectionsPackageName, "Iterable")
151 private val KotlinIterableClassifier = KmClassifier.Class(KotlinIterable.kmClassName)
152