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.animation.core.lint
18 
19 import androidx.compose.lint.test.bytecodeStub
20 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
21 import com.android.tools.lint.detector.api.Detector
22 import com.android.tools.lint.detector.api.Issue
23 import org.junit.Test
24 import org.junit.runner.RunWith
25 import org.junit.runners.JUnit4
26 
27 /**
28  * Detector to discourage the use of arc-based animations on types other than the specified (known
29  * 2-dimensional types such as Offset, IntOffset, DpOffset).
30  *
31  * TODO(b/299477780): Support detecting usages on keyframes. Note that it would only apply to usages
32  *   of `KeyframeEntity<T>.using(arcMode: ArcMode)` where arc mode is ArcAbove/ArcBelow.
33  */
34 @RunWith(JUnit4::class)
35 class ArcAnimationSpecTypeDetectorTest : LintDetectorTest() {
getDetectornull36     override fun getDetector(): Detector = ArcAnimationSpecTypeDetector()
37 
38     override fun getIssues(): MutableList<Issue> =
39         mutableListOf(ArcAnimationSpecTypeDetector.ArcAnimationSpecTypeIssue)
40 
41     // Simplified version of Arc animation classes in AnimationSpec.kt
42     private val ArcAnimationSpecStub =
43         bytecodeStub(
44             filename = "AnimationSpec.kt",
45             filepath = "androidx/compose/animation/core",
46             checksum = 0x9d0cdf8f,
47             source =
48                 """
49             package androidx.compose.animation.core
50 
51             class ArcAnimationSpec<T>(val mode: ArcMode, val durationMillis: Int = 400)
52 
53             sealed class ArcMode
54 
55             object ArcAbove : ArcMode()
56         """,
57             """
58                 META-INF/main.kotlin_module:
59                 H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijg4uViTsvPF2ILSS0u8S5RYtBiAACf
60                 q36HJwAAAA==
61                 """,
62             """
63                 androidx/compose/animation/core/ArcAbove.class:
64                 H4sIAAAAAAAA/41SS2/TQBD+1knzhqbllVDe5ZH0gJuKWyukEECylORAqkio
65                 p42zwDb2brXeRD3mxA/hH1QcKoGEIrjxoxCzJoUDl9jyzM4333yzM/LPX1++
66                 AXiGxwwNrsZGy/GpH+r4RCfC50rG3EqtCDHCb5uwPdIzkQdjeLICu6fHRM4w
67                 5A6kkvY5Q6bRHDK0Gt2JtpFU/vEs9qWywige+S/FOz6NbEerxJppaLXpcTMR
68                 Zr85rGAN+RKyKDBk7QeZMOx0V73vPkPhIIzSKzihXAkeLhEY9AeH7X7nVQXr
69                 KBcJrDJsd7V57x8LOzJcqoQ0lbapaOL3te1Po4j0Ni4G6AnLx9xywrx4lqFd
70                 MmeKzoCBTQg/lS7apdO4RQ0W81LJq3npt5gXfnz0aov5nrfLXuQL3vdPOa/q
71                 OeoeQ3OVEd2SqXu1fZEZnIjw6cQybL2ZKitjEaiZTOQoEu1/s9AaO1TIsN6V
72                 SvSn8UiYQ04chs2uDnk05Ea6eAmWBnpqQvFauqC+FB7+J4sWbTGbjl53SyV/
73                 j6Ic+U3yHr1raXTfbYQi5rI77BzFszT/YMkGCtgmW/nDQIm0HFb5W32D2O4p
74                 f4X39hyXP2PjLAU8PEztXTxKf22GK9T06hEyAa4FuB5QaY2OqAe4ia0jsAS3
75                 cJvyCcoJ7iTI/QY7flqdFwMAAA==
76                 """,
77             """
78                 androidx/compose/animation/core/ArcAnimationSpec.class:
79                 H4sIAAAAAAAA/5VSS28TVxT+7vg1HgyMXUKCCRQID8cujONCH5gipSCkkeyA
80                 cJRNurkZ35obj2eiudcRqyo/odtuWbMACUTVRRV12R9V9Vx7EvLowt3c87jf
81                 Oec7j7//+eNPAPfRZmjyqJ/Esv/aC+LRTqyExyM54lrGEXkS4a0mweqBp7cj
82                 ggIYQ+3R+sPONt/lXsijgfd8a1sEuv34tIvBPekrIMuQfyQjqR8z3K11ZmDQ
83                 jfui7S9vMCx14mTgbQu9lXAZKcJGsZ6AlbcW67VxGFLR7IgCbBQZrg5jHcrI
84                 294deTLSIol46PmRTihYBqqAMwxzwSsRDNPoFzzhI0FAhju10w0d8fRMkkF7
85                 eaOEszjnoITzDJmasfMoO8ihwrA8c3slFHGhCAtzDOf642QC6cowlIqB+SXM
86                 Y8F8X6L29CvjbM2S+9jyaDQ//Y+B+53/mt5T8TMfh/oJjVwn40DHSZcnQ5G0
87                 p50XHCJ5laEwENqkYWjUZp8CQ5ninp5on6bq088Bna7QvM81J7Q12s3QLTPz
88                 FM0DGtaQ/K+lsZqk9VcY1P5e1bEWLGd/z7Fceow8MG3HsnML+3v1rL2/57KW
89                 1bR+nK/k3UzVamYrtm25OdLyf73JW27hZfnQsim8mrVtt0jOCfiz03HPmNIt
90                 YrPODCn32CLuDTXD5ZfjSMuR8KNdqeRWKFY/HzMt+clkeOc7MhJr49GWSNY5
91                 YRgqnTjg4QZPpLFT582TuQ7v+FjSsz3Ng2GX76RhxZ4cRFyPE9KdXjxOAvFM
92                 mo9Lab6NU8ywQvvN0WwtVMzJUm8tsvIkbZIVc6cks2TTMRDqa7J6JM0+5hoV
93                 53e49U/4ot74iIv1xY+ovp8ku58myVPoA9KvTQNwGYtmraRNixnNlLDwjdm5
94                 ldaFa0KvkGXqtSjYcCxfyf3yGwpl9usP9cbiJ3w5rfUtvRkw57Co4VumktdT
95                 vp45I5K5+gdcfHeMH1J+pSkg5Xd0BGXcwFJK5Gii6tsZEmXw3QSVwfcTuYKH
96                 JJ8T5iZhbm0i4+O2jzs+algmFXUfDXy1CaZwF/c2UVJYVPAUmgpFhQsK8xO9
97                 oHBDYUnhmsL1fwEMDsmpAwYAAA==
98                 """,
99             """
100                 androidx/compose/animation/core/ArcMode.class:
101                 H4sIAAAAAAAA/5VRXWsTQRQ9s9lu0jW229aP1O+KYFOx2xbRh4oQK0IgUbCS
102                 lzzIZDPqJLszZXY29DH4U/wHfRJ8kNBHf5R4Z5Pia4Xlfpwz596Zs7///PwF
103                 4BkeMjzmami0HJ7Gic5OdC5irmTGrdSKECPilkm6eiiqYAzRiE94nHL1JX4/
104                 GInEVlFhCF5KJe0rhsp2s1fHEoIQPqoMvv0qc4Zm55I7Dhn2tztjbVOp4tEk
105                 i6Wywiiexm/EZ16k9kir3Joisdp0uRkLc9jshfDcro1HyT/yU1ayDLv/N41h
106                 7ULQFZYPueWEedmkQnYxF5ZdAAMbE34qXbdH1XCf4clsuhJ6DS/0otk0pI/q
107                 2vPGbHrg7bHX1Zp//j3wIu/8G2MVJzlgbtDOZcxpDfTEuRO1LqjjE5Hsji15
108                 fETGMax2pBLvimwgzEc+SAlZ7+iEpz1upOsXYHisC5OIt9I1mx8KZWUmejKX
109                 xLaU0rYcnvtbZKrvXkrZc3+UbnqXutg9nfLSzg/Uzkr6HsWgBAPcp1ifH8Ay
110                 QiBiVF1ZiJ9S9hbi+llpoxPcmINzQVldxUp59EG54A62KL8gZJW4qI9KG2tt
111                 rLexgWtU4nqbZtzsg+VoYLMPP0eY41aOIMftv/zlm/jsAgAA
112                 """
113         )
114 
115     // Simplified version of Offset.kt in geometry package
116     private val GeometryStub =
117         bytecodeStub(
118             filename = "Offset.kt",
119             filepath = "androidx/compose/ui/geometry",
120             checksum = 0x471b639e,
121             source =
122                 """
123             package androidx.compose.ui.geometry
124 
125             class Offset
126         """,
127             """
128                 META-INF/main.kotlin_module:
129                 H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMuKST8xLKcrPTKnQS87PLcgvTtVL
130                 zMvMTSzJzM8DihSlCvE7wvjBBanJ3iVcvFzMafn5QmwhqcUl3iVKDFoMAHnM
131                 zO9bAAAA
132                 """,
133             """
134                 androidx/compose/ui/geometry/Offset.class:
135                 H4sIAAAAAAAA/41RzS5DQRg937S9uIr6r9+NSLBwETsiQSJpUiRIN1bT3sFo
136                 74zcmQq7Pos3sJJYSGPpocR3Lw9gc3J+vpk5M/P1/f4BYBdLhBVp4tTq+Clq
137                 2eTBOhV1dXSrbKJ8+hyd39w45QdAhMq9fJRRR5rb6Lx5r1rsFgjBvjbaHxAK
138                 a+uNMkoIQhQxQCj6O+0Iq/V/7L9HGK+3re9oE50qL2PpJXsieSxwTcpgMAMQ
139                 qM3+k87UFrN4m7Dc74WhqIpQVJj1e9V+b0ds0VHp8yUQFZFN7VC2duj3tM22
140                 53rHNlaEsbo26qybNFV6JZsddibqtiU7DZnqTP+Z4aXtpi11ojMxd9E1Xieq
141                 oZ3m9NAY66XX1jhsQ/Dt/5pmj8FYZRXlGihtvGHwlYnAHGOQm0XMM5Z/BzCE
142                 MM8XcpzFYv5RhGHOytco1DBSw2gNY6gwxXgNE5i8BjlMYZpzh9BhxiH4AWXo
143                 H/7lAQAA
144                 """
145         )
146 
147     // Simplified classes of ui/unit package
148     private val UnitStub =
149         bytecodeStub(
150             filename = "Units.kt",
151             filepath = "androidx/compose/ui/unit",
152             checksum = 0x137591fb,
153             source =
154                 """
155             package androidx.compose.ui.unit
156 
157             class IntOffset
158 
159             class DpOffset
160         """,
161             """
162                 META-INF/main.kotlin_module:
163                 H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMuKST8xLKcrPTKnQS87PLcgvTtVL
164                 zMvMTSzJzM8DihSlCvE7wvjBBanJ3iVcvFzMafn5QmwhqcUl3iVKDFoMAHnM
165                 zO9bAAAA
166                 """,
167             """
168                 androidx/compose/ui/unit/DpOffset.class:
169                 H4sIAAAAAAAA/4VRy0oDMRQ9N7VjHavWd32CuFEXjoo7RfCBUKgKPrpxlXZS
170                 jW0TaTList/iH7gSXEhx6UeJd0b3bg7ncZOcJF/f7x8AdrFEWJEm7lodP0cN
171                 23m0TkWJjhKjfXTyeNFsOuUHQYTSg3ySUVuau+ii/qAa7OYIwb7myQNCbm29
172                 VkQeQYgBDBIG/L12hNXqv7vvEcarLevb2kRnystYesme6DzluCKlUEgBBGqx
173                 /6xTtcUs3iYs93thKMoiFCVm/V6539sRW3SU/3wJREmkUzuUri3c8KFus+W5
174                 27GNFWGsqo06Tzp11b2W9TY7E1XbkO2a7OpU/5nhlU26DXWqUzF3mRivO6qm
175                 neb00BjrpdfWOGxD8NX/iqYvwVhmFWUayG+8ofDKRGCOMcjMAPOMxd8BDCHM
176                 8oUMZ7GY/RFhmLPiLXIVjFQwWsEYSkwxXsEEJm9BDlOY5twhdJhxCH4AObkh
177                 xeABAAA=
178                 """,
179             """
180                 androidx/compose/ui/unit/IntOffset.class:
181                 H4sIAAAAAAAA/4VRTS9rQRh+3ml71FHUd3FZiAUWDmJHJEhucpIiwe3Gatoz
182                 ZbSdkc4csexv8Q+sJBbS3KUfJd5z2Ns8eT7emXlm5uPz7R3APlYIa9IkfauT
183                 p6hlew/WqSjVUWq0j2LjL9ptp/wIiFC9l48y6kpzG10071WL3QIhONQ8ekQo
184                 bGw2KighCFHECKHo77QjrNd/3/6AMFXvWN/VJjpTXibSS/ZE77HAJSmDcgYg
185                 UIf9J52pHWbJLmF1OAhDUROhqDIbDmrDwZ7YoZPS/+dAVEU2tUfZ2vI/PtVt
186                 dzyXO7WJIkzWtVHnaa+p+tey2WVnum5bstuQfZ3pHzO8smm/pf7qTCxepsbr
187                 nmpopzk9NsZ66bU1DrsQfPefotlTMNZYRbkGSluvKL8wEVhkDHKziCXGyvcA
188                 RhHm+XKOC/iT/xJhjLPKDQoxxmNMxJhElSmmYkxj5gbkMIs5zh1Ch3mH4Au3
189                 DmZN4gEAAA==
190                 """
191         )
192 
193     @Test
194     fun testPreferredTypeIssue() {
195         lint()
196             .files(
197                 kotlin(
198                     """
199 package foo
200 
201 import androidx.compose.animation.core.ArcAnimationSpec
202 import androidx.compose.animation.core.ArcAbove
203 import androidx.compose.ui.geometry.Offset
204 import androidx.compose.ui.unit.DpOffset
205 import androidx.compose.ui.unit.IntOffset
206 
207 fun test() {
208     ArcAnimationSpec<Offset>(ArcAbove)
209     ArcAnimationSpec<IntOffset>(ArcAbove)
210     ArcAnimationSpec<DpOffset>(ArcAbove)
211     ArcAnimationSpec<Float>(ArcAbove)
212     ArcAnimationSpec<String>(ArcAbove)
213 }
214             """
215                 ),
216                 ArcAnimationSpecStub,
217                 GeometryStub,
218                 UnitStub
219             )
220             .run()
221             .expect(
222                 """src/foo/test.kt:14: Hint: Arc animation is intended for 2D values such as Offset, IntOffset or DpOffset.
223 Otherwise, the animation might not be what you expect. [ArcAnimationSpecTypeIssue]
224     ArcAnimationSpec<Float>(ArcAbove)
225     ~~~~~~~~~~~~~~~~
226 src/foo/test.kt:15: Hint: Arc animation is intended for 2D values such as Offset, IntOffset or DpOffset.
227 Otherwise, the animation might not be what you expect. [ArcAnimationSpecTypeIssue]
228     ArcAnimationSpec<String>(ArcAbove)
229     ~~~~~~~~~~~~~~~~
230 0 errors, 0 warnings, 2 hints"""
231             )
232     }
233 }
234