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