1 /*
<lambda>null2  * Copyright (C) 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 android.adservices.lint.prod
18 
19 import com.android.tools.lint.detector.api.Category
20 import com.android.tools.lint.detector.api.ConstantEvaluator
21 import com.android.tools.lint.detector.api.Context
22 import com.android.tools.lint.detector.api.Detector
23 import com.android.tools.lint.detector.api.Implementation
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.Scope
27 import com.android.tools.lint.detector.api.Severity
28 import com.android.tools.lint.detector.api.SourceCodeScanner
29 import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
30 import com.intellij.psi.PsiClassType
31 import com.intellij.psi.PsiType
32 import org.jetbrains.uast.UAnnotation
33 import org.jetbrains.uast.UCallExpression
34 import org.jetbrains.uast.UClass
35 import org.jetbrains.uast.UExpression
36 import org.jetbrains.uast.UField
37 import org.jetbrains.uast.UNamedExpression
38 import org.jetbrains.uast.UastVisibility
39 
40 class RoomDatabaseMigrationDetector : Detector(), SourceCodeScanner {
41 
42     private val databaseList: MutableList<UClass> = mutableListOf()
43     private var registrationClassFound: Boolean = false
44 
45     override fun applicableSuperClasses(): List<String> {
46         return listOf(java.lang.Object::class.java.canonicalName)
47     }
48 
49     override fun afterCheckRootProject(context: Context) {
50         if (context.phase == 1 && databaseList.isNotEmpty()) {
51             context.driver.requestRepeat(this, Scope.JAVA_FILE_SCOPE)
52         }
53         if (context.phase == 2) {
54             if (!registrationClassFound) {
55                 context.report(
56                     issue = ISSUE_ERROR,
57                     location = context.getLocation(ROOM_DATABASE_REGISTRATION_CLASS_NAME),
58                     message = DATABASE_REGISTRATION_CLASS_MISSING_ERROR,
59                 )
60             }
61         }
62     }
63 
64     override fun visitClass(context: JavaContext, declaration: UClass) {
65         when (context.phase) {
66             1 -> visitClassPhaseOne(context, declaration)
67             2 -> visitClassPhaseTwo(context, declaration)
68         }
69     }
70 
71     private fun visitClassPhaseOne(context: JavaContext, declaration: UClass) {
72         val qualifiedName: String = declaration.qualifiedName ?: return
73         if (
74             qualifiedName == ROOM_DATABASE_CLASS_NAME ||
75                 declaration.supers.find { it.qualifiedName == ROOM_DATABASE_CLASS_NAME } == null
76         ) {
77             return
78         }
79         databaseList.add(declaration)
80         val databaseAnnotation = getDatabaseAnnotation(context, declaration) ?: return
81 
82         checkSchemaExportInAnnotation(context, databaseAnnotation)
83 
84         val databaseVersion =
85             getAndValidateDatabaseVersion(context, declaration, databaseAnnotation) ?: return
86 
87         validateMigrationPath(databaseAnnotation, context, databaseVersion)
88     }
89 
90     private fun visitClassPhaseTwo(context: JavaContext, declaration: UClass) {
91         if (declaration.qualifiedName?.contains(ROOM_DATABASE_REGISTRATION_CLASS_NAME) != true) {
92             return
93         }
94 
95         registrationClassFound = true
96         val registeredDatabase =
97             declaration.fields
98                 .mapNotNull { (it.type as PsiClassType).resolve() }
99                 .map { it.qualifiedName }
100         for (database in databaseList) {
101             if (database.qualifiedName !in registeredDatabase) {
102                 context.report(
103                     issue = ISSUE_ERROR,
104                     location = context.getNameLocation(declaration),
105                     message = DATABASE_NOT_REGISTERED_ERROR.format(database.qualifiedName),
106                 )
107             }
108         }
109     }
110 
111     private fun validateMigrationPath(
112         databaseAnnotation: UAnnotation,
113         context: JavaContext,
114         databaseVersion: Int
115     ) {
116         // If database version is 1, it is not necessary to set up any migration plan.
117         if (databaseVersion == 1) {
118             return
119         }
120         val autoMigrationsAttribute =
121             databaseAnnotation.findDeclaredAttributeValue(
122                 DATABASE_ANNOTATION_ATTRIBUTE_AUTO_MIGRATIONS
123             )
124         if (autoMigrationsAttribute == null) {
125             context.report(
126                 issue = ISSUE_ERROR,
127                 location = context.getNameLocation(databaseAnnotation),
128                 message = MISSING_AUTO_MIGRATION_ATTRIBUTE_ERROR,
129             )
130             return
131         }
132 
133         val autoMigrations = (autoMigrationsAttribute as UCallExpression).valueArguments
134 
135         if (!isAutoMigrationPathComplete(autoMigrations, databaseVersion)) {
136             context.report(
137                 issue = ISSUE_WARNING,
138                 location = context.getNameLocation(databaseAnnotation),
139                 message = INCOMPLETE_MIGRATION_PATH_ERROR
140             )
141         }
142     }
143 
144     private fun checkSchemaExportInAnnotation(
145         context: JavaContext,
146         databaseAnnotation: UAnnotation
147     ) {
148         val exportSchemaAttribute = databaseAnnotation.findDeclaredAttributeValue("exportSchema")
149         if (
150             exportSchemaAttribute != null &&
151                 !(exportSchemaAttribute.let { ConstantEvaluator.evaluate(null, it) } as Boolean)
152         ) {
153             context.report(
154                 issue = ISSUE_ERROR,
155                 location = context.getNameLocation(databaseAnnotation),
156                 message = SCHEMA_EXPORT_FALSE_ERROR,
157             )
158         }
159     }
160 
161     private fun getDatabaseAnnotation(context: JavaContext, declaration: UClass): UAnnotation? {
162         for (annotation in declaration.uAnnotations) {
163             if (DATABASE_ANNOTATION_CLASS_NAME == annotation.qualifiedName) {
164                 return annotation
165             }
166         }
167         context.report(
168             issue = ISSUE_ERROR,
169             location = context.getNameLocation(declaration),
170             message = MISSING_DATABASE_ANNOTATION_ERROR,
171         )
172         return null
173     }
174 
175     private fun getAndValidateDatabaseVersion(
176         context: JavaContext,
177         declaration: UClass,
178         databaseAnnotation: UAnnotation
179     ): Int? {
180         val versionInAnnotation =
181             databaseAnnotation.findAttributeValue(DATABASE_ANNOTATION_ATTRIBUTE_VERSION)
182 
183         val versionField = getDatabaseVersionField(context, declaration)
184 
185         if (versionInAnnotation == null) {
186             context.report(
187                 issue = ISSUE_ERROR,
188                 location = context.getLocation(databaseAnnotation),
189                 message = MISSING_DATABASE_VERSION_ANNOTATION_ATTRIBUTE_ERROR,
190             )
191             return null
192         }
193 
194         if (versionInAnnotation.tryResolveUDeclaration() != versionField) {
195             context.report(
196                 issue = ISSUE_ERROR,
197                 location = context.getLocation(versionInAnnotation),
198                 message = FAILED_REF_VERSION_FIELD_IN_ANNOTATION_ERROR,
199             )
200         }
201 
202         return versionInAnnotation.evaluate() as Int
203     }
204 
205     private fun getDatabaseVersionField(context: JavaContext, declaration: UClass): UField? {
206         var versionField: UField? = null
207         for (field in declaration.fields) {
208             if (DATABASE_VERSION_FIELD_NAME == field.name) {
209                 versionField = field
210                 if (
211                     field.isFinal &&
212                         field.isStatic &&
213                         field.visibility == UastVisibility.PUBLIC &&
214                         field.type == PsiType.INT
215                 ) {
216                     return versionField
217                 }
218             }
219         }
220         context.report(
221             issue = ISSUE_ERROR,
222             location = context.getNameLocation(versionField ?: declaration),
223             message = MISSING_DATABASE_VERSION_FIELD_ERROR,
224         )
225         return null
226     }
227 
228     private fun isAutoMigrationPathComplete(
229         autoMigrations: List<UExpression>,
230         databaseVersion: Int
231     ): Boolean {
232         var i = 1
233         val migrationPath =
234             autoMigrations.map {
235                 Pair(
236                     (it as UCallExpression)
237                         .valueArguments
238                         .find { va -> (va as UNamedExpression).name == "from" }
239                         ?.evaluate() as Int,
240                     it.valueArguments
241                         .find { va -> (va as UNamedExpression).name == "to" }
242                         ?.evaluate() as Int
243                 )
244             }
245         for (edge in migrationPath.sortedBy { it.first }) {
246             val from = edge.first
247             val to = edge.second
248             if (from != i || to != i + 1) {
249                 return false
250             }
251             i++
252         }
253         return i == databaseVersion
254     }
255 
256     companion object {
257         const val ROOM_DATABASE_CLASS_NAME = "androidx.room.RoomDatabase"
258         const val DATABASE_ANNOTATION_CLASS_NAME = "androidx.room.Database"
259         const val DATABASE_VERSION_FIELD_NAME = "DATABASE_VERSION"
260         const val DATABASE_ANNOTATION_ATTRIBUTE_AUTO_MIGRATIONS = "autoMigrations"
261         const val DATABASE_ANNOTATION_ATTRIBUTE_VERSION = "version"
262         const val ROOM_DATABASE_REGISTRATION_CLASS_NAME = "RoomDatabaseRegistration"
263 
264         const val MISSING_DATABASE_ANNOTATION_ERROR =
265             "Class extends $ROOM_DATABASE_CLASS_NAME must have @$DATABASE_ANNOTATION_CLASS_NAME " +
266                 "annotation."
267         const val MISSING_AUTO_MIGRATION_ATTRIBUTE_ERROR =
268             "@$DATABASE_ANNOTATION_CLASS_NAME annotation attribute " +
269                 "$DATABASE_ANNOTATION_ATTRIBUTE_AUTO_MIGRATIONS missing for database version higher than 1."
270         const val INCOMPLETE_MIGRATION_PATH_ERROR =
271             "$DATABASE_ANNOTATION_ATTRIBUTE_AUTO_MIGRATIONS in $DATABASE_ANNOTATION_CLASS_NAME " +
272                 "should contain migration path in increment of 1 from 1 to " +
273                 "$DATABASE_VERSION_FIELD_NAME."
274         const val MISSING_DATABASE_VERSION_FIELD_ERROR =
275             "Must declare public static final int $DATABASE_VERSION_FIELD_NAME in file."
276         const val MISSING_DATABASE_VERSION_ANNOTATION_ATTRIBUTE_ERROR =
277             "@$DATABASE_ANNOTATION_CLASS_NAME must contain attribute $DATABASE_ANNOTATION_ATTRIBUTE_VERSION."
278         const val FAILED_REF_VERSION_FIELD_IN_ANNOTATION_ERROR =
279             "$DATABASE_ANNOTATION_ATTRIBUTE_VERSION in annotation $DATABASE_ANNOTATION_CLASS_NAME " +
280                 "must refer to the static field $DATABASE_VERSION_FIELD_NAME."
281         const val SCHEMA_EXPORT_FALSE_ERROR = "Export schema must be set to true or absent"
282         const val DATABASE_REGISTRATION_CLASS_MISSING_ERROR =
283             "Class $ROOM_DATABASE_REGISTRATION_CLASS_NAME is required and should contain all DB classes as field."
284         const val DATABASE_NOT_REGISTERED_ERROR = "Database class %s is missing from registration."
285 
286         val ISSUE_ERROR =
287             Issue.create(
288                 id = "RoomDatabaseChange",
289                 briefDescription = "Updated Room Database must have migration path and test.",
290                 explanation =
291                     "Room database update requires migration path configuration and testing.",
292                 moreInfo = "documentation/RoomDatabaseMigrationDetector.md",
293                 category = Category.COMPLIANCE,
294                 severity = Severity.ERROR,
295                 implementation =
296                     Implementation(RoomDatabaseMigrationDetector::class.java, Scope.JAVA_FILE_SCOPE)
297             )
298         val ISSUE_WARNING =
299             Issue.create(
300                 id = "RoomDatabaseChange",
301                 briefDescription = "Updated Room Database must have migration path and test.",
302                 explanation =
303                     "Room database update requires migration path configuration and testing.",
304                 moreInfo = "documentation/RoomDatabaseMigrationDetector.md",
305                 category = Category.COMPLIANCE,
306                 severity = Severity.WARNING,
307                 implementation =
308                     Implementation(RoomDatabaseMigrationDetector::class.java, Scope.JAVA_FILE_SCOPE)
309             )
310     }
311 }
312