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