diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 49f7129a6..9acd917dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,8 +29,6 @@ android { versionName = project.findProperty("versionName") as? String ?: "1.0" testInstrumentationRunner = AppConfig.ANDROID_TEST_INSTRUMENTATION - - } signingConfigs { @@ -103,6 +101,8 @@ dependencies { implementation(libs.bundles.koin) ksp(libs.bundles.room.ksp) implementation(libs.timber) + implementation(projects.linting) + lintChecks(projects.linting) } ksp { diff --git a/build.gradle.kts b/build.gradle.kts index 48168cc64..984fc468d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,6 +17,7 @@ plugins { alias(libs.plugins.google.gms.google.services) apply false alias(libs.plugins.google.firebase.crashlytics) apply false alias(libs.plugins.kotlinx.kover) apply true + alias(libs.plugins.android.lint) apply false } subprojects { diff --git a/buildSrc/src/main/kotlin/com/london/buildsrc/AppConfig.kt b/buildSrc/src/main/kotlin/com/london/buildsrc/AppConfig.kt index c06c09b2c..7c5008c0d 100644 --- a/buildSrc/src/main/kotlin/com/london/buildsrc/AppConfig.kt +++ b/buildSrc/src/main/kotlin/com/london/buildsrc/AppConfig.kt @@ -8,8 +8,8 @@ object AppConfig { object Version { const val MIN_SDK = 26 - const val TARGET_SDK = 34 - const val COMPILE_SDK = 35 + const val TARGET_SDK = 36 + const val COMPILE_SDK = 36 val JVM = JavaVersion.VERSION_17 const val BUILD_TOOLS = "35.0.0" } @@ -28,6 +28,8 @@ object AppConfig { } val freeCompilerArgs = listOf( + "-opt-in=kotlin.time.ExperimentalTime", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", ) diff --git a/data/src/main/java/com/london/data/di/NetworkModule.kt b/data/src/main/java/com/london/data/di/NetworkModule.kt index bd05f39b6..599c50ed8 100644 --- a/data/src/main/java/com/london/data/di/NetworkModule.kt +++ b/data/src/main/java/com/london/data/di/NetworkModule.kt @@ -3,9 +3,10 @@ package com.london.data.di import android.content.Context import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.london.data.BuildConfig -import com.london.data.remote.interceptor.AuthInterceptor import com.london.data.local.preference.AuthPreferences import com.london.data.local.preference.SharedPrefsTokenProvider +import com.london.data.local.source.device.DeviceConfigurationDataSource +import com.london.data.remote.interceptor.AuthInterceptor import com.london.data.remote.service.auth.AuthApiService import com.london.data.remote.service.details.actor.ActorDetailsApiService import com.london.data.remote.service.details.movie.MovieDetailsApiService @@ -15,7 +16,6 @@ import com.london.data.remote.service.reviews.ReviewsApiService import com.london.data.remote.service.search.SearchApiService import com.london.data.remote.service.toprated.movie.TopRatedMovieApiService import com.london.data.remote.service.toprated.tvseries.TopRatedTvSeriesApiService -import com.london.data.local.source.device.DeviceConfigurationDataSource import com.london.domain.repository.SessionTokenProvider import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json diff --git a/data/src/main/java/com/london/data/local/source/recent/watched/RecentWatchedTvShowsDataSource.kt b/data/src/main/java/com/london/data/local/source/recent/watched/RecentWatchedTvShowsDataSource.kt index f727c8e1c..1bcf6e1e7 100644 --- a/data/src/main/java/com/london/data/local/source/recent/watched/RecentWatchedTvShowsDataSource.kt +++ b/data/src/main/java/com/london/data/local/source/recent/watched/RecentWatchedTvShowsDataSource.kt @@ -2,9 +2,9 @@ package com.london.data.local.source.recent.watched import com.london.data.local.database.dao.recent.whatched.tvshow.RecentWatchedTvShowsDao import com.london.data.local.model.recent.watched.RecentWatchedTvShowLocal +import org.koin.core.annotation.Named import org.koin.core.annotation.Provided import org.koin.core.annotation.Single -import javax.inject.Named @Single @Named("recentWatchedTvShowsDataSource") diff --git a/data/src/main/java/com/london/data/remote/source/search/SearchRemoteDataSourceImpl.kt b/data/src/main/java/com/london/data/remote/source/search/SearchRemoteDataSourceImpl.kt index 3d4b34caf..5ef47c881 100644 --- a/data/src/main/java/com/london/data/remote/source/search/SearchRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/london/data/remote/source/search/SearchRemoteDataSourceImpl.kt @@ -10,11 +10,13 @@ import com.london.data.remote.service.search.SearchApiService import com.london.data.remote.source.base.BaseRemoteDatasource import com.london.data.utils.getCurrentDate import com.london.domain.KoverIgnore +import org.koin.core.annotation.Provided import org.koin.core.annotation.Single @Single @KoverIgnore class SearchRemoteDataSourceImpl( + @Provided private val searchApiService: SearchApiService ) : SearchRemoteDataSource, BaseRemoteDatasource { diff --git a/data/src/main/java/com/london/data/utils/currentDate.kt b/data/src/main/java/com/london/data/utils/currentDate.kt index 52e5e073b..6e3e3c8b6 100644 --- a/data/src/main/java/com/london/data/utils/currentDate.kt +++ b/data/src/main/java/com/london/data/utils/currentDate.kt @@ -2,10 +2,10 @@ package com.london.data.utils import com.london.domain.KoverIgnore -import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock - fun getCurrentDate(): String = Clock.System.now() +fun getCurrentDate(): String = Clock.System.now() .toLocalDateTime(TimeZone.currentSystemDefault()) - .date.toString() \ No newline at end of file + .date.toString() diff --git a/designSystem/build.gradle.kts b/designSystem/build.gradle.kts index 5a2577c8c..53f8ce587 100644 --- a/designSystem/build.gradle.kts +++ b/designSystem/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { debugImplementation(libs.bundles.compose.debug) androidTestImplementation(libs.bundles.base.testing) testImplementation(libs.bundles.testing) + lintChecks(projects.linting) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b214a0ea..0abfb46f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,16 @@ [versions] # Build Tools -agp = "8.8.2" +agp = "8.11.0" constraintlayoutCompose = "1.1.1" -firebaseBom = "33.16.0" -firebaseMlModeldownloader = "25.0.1" +firebaseBom = "34.0.0" +firebaseMlModeldownloader = "26.0.0" gson = "2.13.1" -kotlin = "2.0.21" -kotlinxCoroutinesTestVersion = "1.10.2" -ksp = "2.0.21-1.0.27" +kotlin = "2.1.21" +ksp = "2.1.21-2.0.1" # Add Retrofit versions retrofit = "3.0.0" -okhttp = "4.12.0" +okhttp = "5.1.0" # AndroidX Core coreKtx = "1.16.0" @@ -50,7 +49,7 @@ koinKspCompiler = "2.1.0" # Coroutines & Date/Time kotlinxCoroutinesAndroid = "1.10.2" kotlinxCoroutinesTest = "1.8.1" -kotlinxDatetime = "0.6.0" +kotlinxDatetime = "0.7.1" # TensorFlow Lite tensorflowLiteSupport = "0.5.0" @@ -64,78 +63,78 @@ rememberPreference = "1.1.1" # Testing junit = "4.13.2" junitVersion = "1.2.1" -junitJupiter = "5.13.3" +junitJupiter = "5.13.4" espressoCore = "3.6.1" -mockk = "1.14.4" +mockk = "1.14.5" timber = "5.0.1" truth = "1.4.4" -jetbrainsKotlinJvm = "2.0.21" appcompat = "1.7.1" material = "1.12.0" -firebasePerf = "21.0.5" -googleFirebaseFirebasePerf = "1.4.2" +firebasePerf = "22.0.0" +googleFirebaseFirebasePerf = "2.0.0" googleGmsGoogleServices = "4.4.3" -firebaseCrashlytics = "19.4.4" -googleFirebaseCrashlytics = "3.0.4" -firebaseAnalytics = "22.5.0" +firebaseCrashlytics = "20.0.0" +googleFirebaseCrashlytics = "3.0.5" +firebaseAnalytics = "23.0.0" +lint = "31.11.1" kotlinxKover = "0.9.1" firebaseCrashlyticsKtx = "19.4.4" [libraries] # AndroidX Core androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } # Splash Screen androidx-material = { module = "androidx.compose.material:material", version.ref = "materialVersion" } androidx-paging-common = { module = "androidx.paging:paging-common", version.ref = "pagingRuntime" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" } androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingRuntime" } -androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" } +androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashScreen" } # Compose -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "composeUi" } -androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "composeUi" } -androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "composeUi" } -androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "composeUi" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeMaterial3" } -androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "composeUi" } +androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics", version.ref = "composeUi" } +androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "composeUi" } +androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "composeUi" } +androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } # Room Database -androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "roomRuntime" } -androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomRuntime" } -androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "roomRuntime" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } androidx-room-rxjava3 = { module = "androidx.room:room-rxjava3", version.ref = "roomRuntime" } # Data Storage -androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" } -androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } +androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "datastoreCore" } # Dependency Injection (Koin) firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } firebase-ml-modeldownloader = { module = "com.google.firebase:firebase-ml-modeldownloader", version.ref = "firebaseMlModeldownloader" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } -koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } -koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koinAndroidxCompose" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" } koin-androidx-navigation = { module = "io.insert-koin:koin-androidx-navigation", version.ref = "koin" } -koin-annotations = { group = "io.insert-koin", name = "koin-annotations", version.ref = "koinAnnotations" } -koin-ksp-compiler = { group = "io.insert-koin", name = "koin-ksp-compiler", version.ref = "koinKspCompiler" } +koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotations" } +koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koinKspCompiler" } # Coroutines & Date/Time -kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } -kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } -kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } # UI Libraries -coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxCoroutinesTest" } -lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottieCompose" } +lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottieCompose" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } -remember-preference = { group = "dev.burnoo", name = "compose-remember-preference", version.ref = "rememberPreference" } +remember-preference = { module = "dev.burnoo:compose-remember-preference", version.ref = "rememberPreference" } # tensorflow Lite retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" } @@ -143,29 +142,30 @@ tensorflow-lite-support = { module = "org.tensorflow:tensorflow-lite-support", v tensorflow-lite = { module = "org.tensorflow:tensorflow-lite", version.ref = "tensorflowLite" } # Testing -junit = { group = "junit", name = "junit", version.ref = "junit" } -junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiter" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "composeUi" } -androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "composeUi" } +junit = { module = "junit:junit", version.ref = "junit" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junitJupiter" } +androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "composeUi" } +androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "composeUi" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } -truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } -firebase-perf = { group = "com.google.firebase", name = "firebase-perf", version.ref = "firebasePerf" } -firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version.ref = "firebaseCrashlytics" } -firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics", version.ref = "firebaseAnalytics" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +material = { module = "com.google.android.material:material", version.ref = "material" } +firebase-perf = { module = "com.google.firebase:firebase-perf", version.ref = "firebasePerf" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version.ref = "firebaseCrashlytics" } +firebase-analytics = { module = "com.google.firebase:firebase-analytics", version.ref = "firebaseAnalytics" } #noinspection SimilarGradleDependency mockk-agent-jvm = { module = "io.mockk:mockk-agent-jvm", version.ref = "mockk" } -firebase-crashlytics-ktx = { group = "com.google.firebase", name = "firebase-crashlytics-ktx", version.ref = "firebaseCrashlyticsKtx" } +firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx", version.ref = "firebaseCrashlyticsKtx" } +lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" } # Add Retrofit libraries -retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } -retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } -okhttp-core = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } -okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } +retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } [plugins] @@ -174,12 +174,13 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" } +jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } android-library = { id = "com.android.library", version.ref = "agp" } google-firebase-firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "googleFirebaseFirebasePerf" } google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" } google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "googleFirebaseCrashlytics" } kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kotlinxKover" } +android-lint = { id = "com.android.lint", version.ref = "agp" } [bundles] @@ -270,4 +271,4 @@ retrofit = [ "retrofit-converter-gson", "okhttp-core", "okhttp-logging" -] \ No newline at end of file +] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5bd9cee7b..6c51a1dc8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Jun 27 12:04:10 EEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/linting/.gitignore b/linting/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/linting/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/linting/build.gradle.kts b/linting/build.gradle.kts new file mode 100644 index 000000000..f77826f87 --- /dev/null +++ b/linting/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` + alias(libs.plugins.jetbrains.kotlin.jvm) + alias(libs.plugins.android.lint) +} + +lint { + htmlReport = true + htmlOutput = file("lint-report.html") + textReport = true + absolutePaths = false + abortOnError = true + ignoreTestSources = true +} + +dependencies { + compileOnly(libs.lint.api) +} diff --git a/linting/src/main/java/com/london/linting/ResExtensionDetector.kt b/linting/src/main/java/com/london/linting/ResExtensionDetector.kt new file mode 100644 index 000000000..94b8f4043 --- /dev/null +++ b/linting/src/main/java/com/london/linting/ResExtensionDetector.kt @@ -0,0 +1,75 @@ +package com.london.linting + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiElement +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.UReferenceExpression +import java.util.EnumSet + + +class ResExtensionDetector : Detector(), SourceCodeScanner { + + override fun getApplicableReferenceNames(): List = trackedExtensions.toList() + + override fun visitReference( + context: JavaContext, + reference: UReferenceExpression, + referenced: PsiElement + ) { + val call = reference.uastParent as? UQualifiedReferenceExpression ?: return + val receiver = call.receiver + + // 1. If it's a raw int literal like `1.string` + if (receiver is ULiteralExpression && receiver.value is Int) { + context.report( + ISSUE, + receiver, + context.getLocation(call), + "Avoid using raw Int literals with `.string` or `.painter`. Use a valid resource ID like R.string.app_name." + ) + return + } + + // 2. If it's not a valid R.string or R.drawable constant + if (receiver is UQualifiedReferenceExpression) { + val receiverText = receiver.asRenderString() + val isValid = allowedPackages.any { receiverText.startsWith(it) } + if (!isValid) + context.report( + ISSUE, + receiver, + context.getLocation(call), + "Only use constants from R.string or R.drawable with `.string` or `.painter`." + ) + } + } + + companion object { + val ISSUE = Issue.create( + id = "UnsafeResourceExtensionUsage", + briefDescription = "Raw int used with @StringRes or @DrawableRes extension", + explanation = """ + Avoid using raw Ints with `.string` or `.painter`. + Only use valid R.string or R.drawable resource IDs. + """.trimIndent(), + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = Implementation( + ResExtensionDetector::class.java, + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) + ) + ) + + private val trackedExtensions = setOf("string", "painter") + private val allowedPackages = setOf("R.string", "R.drawable") + } +} diff --git a/linting/src/main/java/com/london/linting/ResExtensionIssueRegistry.kt b/linting/src/main/java/com/london/linting/ResExtensionIssueRegistry.kt new file mode 100644 index 000000000..e4dd69f33 --- /dev/null +++ b/linting/src/main/java/com/london/linting/ResExtensionIssueRegistry.kt @@ -0,0 +1,9 @@ +package com.london.linting + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.detector.api.CURRENT_API + +class ResExtensionIssueRegistry : IssueRegistry() { + override val issues = listOf(ResExtensionDetector.ISSUE) + override val api = CURRENT_API +} diff --git a/linting/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/linting/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry new file mode 100644 index 000000000..03970b0df --- /dev/null +++ b/linting/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry @@ -0,0 +1 @@ +com.london.linting.ResExtensionIssueRegistry diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index c1d179cda..4e5ecd401 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(libs.timber) implementation(libs.remember.preference) + lintChecks(projects.linting) } ksp { diff --git a/settings.gradle.kts b/settings.gradle.kts index a643d08ec..12f1bba7a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,8 +24,9 @@ dependencyResolutionManagement { rootProject.name = "Novix" include(":app") -include(":domain") include(":data") +include(":domain") +include(":linting") include(":presentation") include(":designSystem") include(":feature:ImageHaramBlur")