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 package androidx.compose.runtime.lint
18 
19 import com.android.tools.lint.detector.api.AnnotationInfo
20 import com.android.tools.lint.detector.api.AnnotationUsageInfo
21 import com.android.tools.lint.detector.api.AnnotationUsageType
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.LintFix
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import com.android.tools.lint.detector.api.SourceCodeScanner
31 import com.android.tools.lint.detector.api.UastLintUtils
32 import java.util.EnumSet
33 import org.jetbrains.uast.UAnnotation
34 import org.jetbrains.uast.UElement
35 import org.jetbrains.uast.USimpleNameReferenceExpression
36 
37 class AutoboxingStateValuePropertyDetector : Detector(), SourceCodeScanner {
38 
39     private val UAnnotation.preferredPropertyName: String?
40         get() = UastLintUtils.getAnnotationStringValue(this, "preferredPropertyName")
41 
42     private val UElement.identifier: String?
43         get() = (this as? USimpleNameReferenceExpression)?.identifier
44 
45     private val UElement.resolvedName: String?
46         get() = (this as? USimpleNameReferenceExpression)?.resolvedName
47 
applicableAnnotationsnull48     override fun applicableAnnotations(): List<String> {
49         return listOf("androidx.compose.runtime.snapshots.AutoboxingStateValueProperty")
50     }
51 
isApplicableAnnotationUsagenull52     override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean {
53         return type == AnnotationUsageType.FIELD_REFERENCE
54     }
55 
visitAnnotationUsagenull56     override fun visitAnnotationUsage(
57         context: JavaContext,
58         element: UElement,
59         annotationInfo: AnnotationInfo,
60         usageInfo: AnnotationUsageInfo
61     ) {
62         val resolvedPropertyName = element.identifier ?: "<unknown identifier>"
63         val preferredPropertyName =
64             annotationInfo.annotation.preferredPropertyName ?: "<unknown replacement>"
65 
66         val accessKind =
67             when (element.resolvedName?.takeWhile { it.isLowerCase() }) {
68                 "get" -> "Reading"
69                 "set" -> "Assigning"
70                 else -> "Accessing"
71             }
72 
73         context.report(
74             AutoboxingStateValueProperty,
75             element,
76             context.getLocation(element),
77             "$accessKind `$resolvedPropertyName` will cause an autoboxing operation. " +
78                 "Use `$preferredPropertyName` to avoid unnecessary allocations.",
79             createPropertyReplacementQuickFix(
80                 resolvedPropertyName = resolvedPropertyName,
81                 preferredPropertyName = preferredPropertyName
82             )
83         )
84     }
85 
createPropertyReplacementQuickFixnull86     private fun createPropertyReplacementQuickFix(
87         resolvedPropertyName: String,
88         preferredPropertyName: String
89     ): LintFix {
90         return fix()
91             .name("Replace with `$preferredPropertyName`")
92             .replace()
93             .text(resolvedPropertyName)
94             .with(preferredPropertyName)
95             .build()
96     }
97 
98     companion object {
99 
100         val AutoboxingStateValueProperty =
101             Issue.create(
102                 "AutoboxingStateValueProperty",
103                 "State access causes value to be autoboxed",
104                 "Avoid using the generic `value` property when using a specialized State type. " +
105                     "Reading or writing to the state's generic `value` property will result in an " +
106                     "unnecessary autoboxing operation. Prefer the specialized value property " +
107                     "(e.g. `intValue` for `MutableIntState`), or use property delegation to " +
108                     "avoid unnecessary allocations.",
109                 Category.PERFORMANCE,
110                 3,
111                 Severity.WARNING,
112                 Implementation(
113                     AutoboxingStateValuePropertyDetector::class.java,
114                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
115                 )
116             )
117     }
118 }
119