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