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