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.animation.core.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.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
31 import java.util.EnumSet
32 import org.jetbrains.uast.UCallExpression
33 import org.jetbrains.uast.UClass
34 
35 private const val ANIMATION_CORE_PACKAGE = "androidx.compose.animation.core"
36 private const val GEOMETRY_PACKAGE = "androidx.compose.ui.geometry"
37 private const val UNIT_PACKAGE = "androidx.compose.ui.unit"
38 private const val ARC_ANIMATION_SPEC_NAME = "ArcAnimationSpec"
39 private const val ARC_KEYFRAMES_SPEC_NAME = "keyframesWithArcs"
40 private const val OFFSET_NAME = "Offset"
41 private const val INT_OFFSET_NAME = "IntOffset"
42 private const val DP_OFFSET_NAME = "DpOffset"
43 private const val ARC_SPEC_FQ_NAME = "$ANIMATION_CORE_PACKAGE.$ARC_ANIMATION_SPEC_NAME"
44 private const val OFFSET_FQ_NAME = "$GEOMETRY_PACKAGE.$OFFSET_NAME"
45 private const val INT_OFFSET_FQ_NAME = "$UNIT_PACKAGE.$INT_OFFSET_NAME"
46 private const val DP_OFFSET_FQ_NAME = "$UNIT_PACKAGE.$DP_OFFSET_NAME"
47 private val preferredArcAnimationTypes by
<lambda>null48     lazy(LazyThreadSafetyMode.NONE) { setOf(OFFSET_FQ_NAME, INT_OFFSET_FQ_NAME, DP_OFFSET_FQ_NAME) }
49 
50 /**
51  * Lint to inform of the expected usage for `ArcAnimationSpec` (and its derivative)
52  * `keyframesWithArcs`.
53  */
54 class ArcAnimationSpecTypeDetector : Detector(), SourceCodeScanner {
getApplicableUastTypesnull55     override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
56 
57     override fun createUastHandler(context: JavaContext) =
58         object : UElementHandler() {
59             override fun visitCallExpression(node: UCallExpression) {
60                 when (node.classReference?.resolvedName) {
61                     ARC_ANIMATION_SPEC_NAME -> detectTypeParameterInArcAnimation(node)
62                 }
63             }
64 
65             private fun detectTypeParameterInArcAnimation(node: UCallExpression) {
66                 val typeArg = node.typeArguments.firstOrNull() ?: return
67                 val qualifiedTypeName = typeArg.canonicalText
68                 // Check that the given type to the call is one of: Offset, IntOffset, DpOffset
69                 if (preferredArcAnimationTypes.contains(qualifiedTypeName)) {
70                     return
71                 }
72                 // Node class resolution might be slower, do last
73                 val fqClassName =
74                     (node.classReference?.tryResolveUDeclaration() as? UClass)?.qualifiedName
75                 // Verify that the method calls are from the expected animation classes, otherwise,
76                 // skip
77                 // check
78                 if (fqClassName != ARC_SPEC_FQ_NAME) {
79                     return
80                 }
81                 // Generate Lint
82                 context.report(
83                     issue = ArcAnimationSpecTypeIssue,
84                     scope = node,
85                     location = context.getNameLocation(node),
86                     message =
87                         "Arc animation is intended for 2D values such as Offset, IntOffset or " +
88                             "DpOffset.\nOtherwise, the animation might not be what you expect."
89                 )
90             }
91         }
92 
93     companion object {
94         val ArcAnimationSpecTypeIssue =
95             Issue.create(
96                 id = "ArcAnimationSpecTypeIssue",
97                 briefDescription =
98                     "$ARC_ANIMATION_SPEC_NAME is " +
99                         "designed for 2D values. Particularly, for positional values such as Offset.",
100                 explanation =
101                     "$ARC_ANIMATION_SPEC_NAME is designed for" +
102                         " 2D values. Particularly, for positional values such as Offset.\nTrying to use " +
103                         "it for values of different dimensions (Float, Size, Color, etc.) will result " +
104                         "in unpredictable animation behavior.",
105                 category = Category.CORRECTNESS,
106                 priority = 5,
107                 severity = Severity.INFORMATIONAL,
108                 implementation =
109                     Implementation(
110                         ArcAnimationSpecTypeDetector::class.java,
111                         EnumSet.of(Scope.JAVA_FILE)
112                     )
113             )
114     }
115 }
116