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.psi.PsiClassType 31 import com.intellij.psi.PsiType 32 import org.jetbrains.uast.UBinaryExpression 33 import org.jetbrains.uast.UElement 34 import org.jetbrains.uast.UExpression 35 import org.jetbrains.uast.UForEachExpression 36 import org.jetbrains.uast.kotlin.isKotlin 37 import org.jetbrains.uast.skipParenthesizedExprDown 38 39 /** 40 * Lint [Detector] to prevent allocating ranges and progression when using `step()` in a for loops. 41 * For instance: `for (i in a..b step 2)` . See https://youtrack.jetbrains.com/issue/KT-59115 42 */ 43 class SteppedForLoopDetector : Detector(), SourceCodeScanner { getApplicableUastTypesnull44 override fun getApplicableUastTypes() = listOf(UForEachExpression::class.java) 45 46 override fun createUastHandler(context: JavaContext) = 47 object : UElementHandler() { 48 override fun visitForEachExpression(node: UForEachExpression) { 49 if (!isKotlin(node.lang)) return 50 51 when (val type = node.iteratedValue.skipParenthesizedExprDown()) { 52 is UBinaryExpression -> { 53 // Check the expression is of the form a step b, where a is a Progression 54 // type 55 if ( 56 isIntegerProgression(type.leftOperand.getExpressionType()) && 57 isUntilRange(type.leftOperand.skipParenthesizedExprDown()) && 58 type.operatorIdentifier?.name == "step" && 59 isInteger(type.rightOperand.getExpressionType()) 60 ) { 61 report(context, node, type, type.rightOperand.textRepresentation()) 62 } 63 } 64 } 65 } 66 } 67 isIntegerProgressionnull68 private fun isIntegerProgression(type: PsiType?): Boolean { 69 if (type == null) return false 70 71 if (type is PsiClassType) { 72 val cls = type.resolve() 73 return cls != null && 74 (IntegerProgressionTypes.contains(cls.qualifiedName) || 75 cls.superTypes.any { 76 IntegerProgressionTypes.contains(it.resolve()?.qualifiedName) 77 }) 78 } 79 80 return false 81 } 82 UElementnull83 private fun UElement.textRepresentation() = sourcePsi?.text ?: asRenderString() 84 85 // https://youtrack.jetbrains.com/issue/KT-59115 86 private fun isUntilRange(expression: UExpression?) = 87 expression is UBinaryExpression && 88 (expression.operatorIdentifier?.name == "..<" || 89 expression.operatorIdentifier?.name == "until") 90 91 // TODO: Use PsiTypes.intType() and PsiTypes.longType() when they are available 92 private fun isInteger(type: PsiType?) = 93 type?.canonicalText == "int" || type?.canonicalText == "long" 94 95 private fun report(context: JavaContext, node: UElement, target: Any?, messageContext: String) { 96 context.report( 97 issue = ISSUE, 98 scope = node, 99 location = context.getLocation(target), 100 message = "stepping the integer range by $messageContext." 101 ) 102 } 103 104 companion object { 105 val ISSUE = 106 Issue.create( 107 "SteppedForLoop", 108 "A loop over an 'until' or '..<' primitive range (Int/Long/ULong/Char)" + 109 " creates unnecessary allocations", 110 "Using 'until' or '..<' to create an iteration range bypasses a compiler" + 111 " optimization. Consider until '..' instead. " + 112 "See https://youtrack.jetbrains.com/issue/KT-59115", 113 Category.PERFORMANCE, 114 5, 115 Severity.ERROR, 116 Implementation(SteppedForLoopDetector::class.java, Scope.JAVA_FILE_SCOPE) 117 ) 118 val IntegerProgressionTypes = 119 listOf( 120 "kotlin.ranges.IntProgression", 121 "kotlin.ranges.LongProgression", 122 "kotlin.ranges.CharProgression", 123 "kotlin.ranges.UIntProgression", 124 "kotlin.ranges.ULongProgression" 125 ) 126 } 127 } 128