diff --git a/modulecheck-api/src/main/kotlin/modulecheck/api/context/AndroidUnqualifiedDeclarationNames.kt b/modulecheck-api/src/main/kotlin/modulecheck/api/context/AndroidUnqualifiedDeclarationNames.kt index d8221eeafd..1003340358 100644 --- a/modulecheck-api/src/main/kotlin/modulecheck/api/context/AndroidUnqualifiedDeclarationNames.kt +++ b/modulecheck-api/src/main/kotlin/modulecheck/api/context/AndroidUnqualifiedDeclarationNames.kt @@ -16,15 +16,16 @@ package modulecheck.api.context import modulecheck.parsing.android.AndroidResourceParser +import modulecheck.parsing.gradle.AndroidPlatformPlugin import modulecheck.parsing.gradle.SourceSetName import modulecheck.parsing.source.UnqualifiedAndroidResourceDeclaredName import modulecheck.project.McProject import modulecheck.project.ProjectContext -import modulecheck.project.isAndroid import modulecheck.utils.LazySet import modulecheck.utils.SafeCache import modulecheck.utils.dataSource import modulecheck.utils.emptyLazySet +import modulecheck.utils.flatMapToSet import modulecheck.utils.lazySet import modulecheck.utils.toLazySet @@ -37,7 +38,8 @@ data class AndroidUnqualifiedDeclarationNames( get() = Key suspend fun get(sourceSetName: SourceSetName): LazySet { - if (!project.isAndroid()) return emptyLazySet() + val platformPlugin = project.platformPlugin as? AndroidPlatformPlugin + ?: return emptyLazySet() return delegate.getOrPut(sourceSetName) { @@ -57,6 +59,13 @@ data class AndroidUnqualifiedDeclarationNames( } } + val resValues = dataSource { + sourceSetName.withUpstream(project) + .flatMapToSet { sourceSetOrUpstream -> + platformPlugin.resValues[sourceSetOrUpstream].orEmpty() + } + } + val declarations = project .resourceFilesForSourceSetName(sourceSetOrUpstream) .map { file -> @@ -66,6 +75,7 @@ data class AndroidUnqualifiedDeclarationNames( } } .plus(layoutsAndIds) + .plus(resValues) lazySet(declarations) }.toLazySet() diff --git a/modulecheck-parsing/gradle/build.gradle.kts b/modulecheck-parsing/gradle/build.gradle.kts index 8c8f35480e..1785bc0f1a 100644 --- a/modulecheck-parsing/gradle/build.gradle.kts +++ b/modulecheck-parsing/gradle/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { api(libs.rickBusarow.dispatch.core) api(libs.semVer) + api(project(path = ":modulecheck-parsing:source")) api(project(path = ":modulecheck-utils")) compileOnly(gradleApi()) diff --git a/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/PlatformPlugin.kt b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/PlatformPlugin.kt index bbf55a278c..25a72b41e3 100644 --- a/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/PlatformPlugin.kt +++ b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/PlatformPlugin.kt @@ -15,6 +15,7 @@ package modulecheck.parsing.gradle +import modulecheck.parsing.source.UnqualifiedAndroidResourceDeclaredName import java.io.File import kotlin.contracts.contract @@ -61,6 +62,7 @@ sealed interface AndroidPlatformPlugin : PlatformPlugin { val viewBindingEnabled: Boolean val kotlinAndroidExtensionEnabled: Boolean val manifests: Map + val resValues: Map> interface CanDisableAndroidResources { val androidResourcesEnabled: Boolean @@ -76,7 +78,8 @@ sealed interface AndroidPlatformPlugin : PlatformPlugin { override val nonTransientRClass: Boolean, override val viewBindingEnabled: Boolean, override val kotlinAndroidExtensionEnabled: Boolean, - override val manifests: Map + override val manifests: Map, + override val resValues: Map> ) : PlatformPlugin, AndroidPlatformPlugin data class AndroidLibraryPlugin( @@ -87,7 +90,8 @@ sealed interface AndroidPlatformPlugin : PlatformPlugin { override val kotlinAndroidExtensionEnabled: Boolean, override val manifests: Map, override val androidResourcesEnabled: Boolean, - override val buildConfigEnabled: Boolean + override val buildConfigEnabled: Boolean, + override val resValues: Map> ) : PlatformPlugin, AndroidPlatformPlugin, CanDisableAndroidResources, @@ -100,7 +104,8 @@ sealed interface AndroidPlatformPlugin : PlatformPlugin { override val viewBindingEnabled: Boolean, override val kotlinAndroidExtensionEnabled: Boolean, override val manifests: Map, - override val buildConfigEnabled: Boolean + override val buildConfigEnabled: Boolean, + override val resValues: Map> ) : PlatformPlugin, AndroidPlatformPlugin, CanDisableAndroidBuildConfig @@ -112,7 +117,8 @@ sealed interface AndroidPlatformPlugin : PlatformPlugin { override val viewBindingEnabled: Boolean, override val kotlinAndroidExtensionEnabled: Boolean, override val manifests: Map, - override val buildConfigEnabled: Boolean + override val buildConfigEnabled: Boolean, + override val resValues: Map> ) : PlatformPlugin, AndroidPlatformPlugin, CanDisableAndroidBuildConfig diff --git a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/AndroidPlatformPluginFactory.kt b/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/AndroidPlatformPluginFactory.kt index 722db40777..651ffd5625 100644 --- a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/AndroidPlatformPluginFactory.kt +++ b/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/AndroidPlatformPluginFactory.kt @@ -19,7 +19,11 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.DynamicFeatureExtension import com.android.build.api.dsl.TestExtension +import com.android.build.gradle.AppExtension import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.internal.api.ApplicationVariantImpl +import com.android.build.gradle.internal.api.LibraryVariantImpl +import com.android.build.gradle.internal.core.InternalBaseVariant.MergedFlavor import modulecheck.core.rule.KOTLIN_ANDROID_EXTENSIONS_PLUGIN_ID import modulecheck.gradle.AndroidPlatformPluginFactory.Type.Application import modulecheck.gradle.AndroidPlatformPluginFactory.Type.DynamicFeature @@ -32,6 +36,10 @@ import modulecheck.parsing.gradle.AndroidPlatformPlugin.AndroidApplicationPlugin import modulecheck.parsing.gradle.AndroidPlatformPlugin.AndroidDynamicFeaturePlugin import modulecheck.parsing.gradle.AndroidPlatformPlugin.AndroidLibraryPlugin import modulecheck.parsing.gradle.AndroidPlatformPlugin.AndroidTestPlugin +import modulecheck.parsing.gradle.SourceSetName +import modulecheck.parsing.gradle.asSourceSetName +import modulecheck.parsing.source.UnqualifiedAndroidResourceDeclaredName +import modulecheck.utils.cast import javax.inject.Inject typealias AndroidCommonExtension = CommonExtension<*, *, *, *> @@ -59,6 +67,8 @@ class AndroidPlatformPluginFactory @Inject constructor( val manifests = gradleProject.androidManifests().orEmpty() + val resValues = parseResValues(type) + val hasKotlinAndroidExtensions = gradleProject .pluginManager .hasPlugin(KOTLIN_ANDROID_EXTENSIONS_PLUGIN_ID) @@ -90,7 +100,7 @@ class AndroidPlatformPluginFactory @Inject constructor( nonTransientRClass = nonTransientRClass, viewBindingEnabled = viewBindingEnabled, kotlinAndroidExtensionEnabled = hasKotlinAndroidExtensions, - manifests = manifests + manifests = manifests, resValues = resValues ) is DynamicFeature -> AndroidDynamicFeaturePlugin( sourceSets = sourceSets, @@ -99,7 +109,7 @@ class AndroidPlatformPluginFactory @Inject constructor( viewBindingEnabled = viewBindingEnabled, kotlinAndroidExtensionEnabled = hasKotlinAndroidExtensions, manifests = manifests, - buildConfigEnabled = buildConfigEnabled + buildConfigEnabled = buildConfigEnabled, resValues = resValues ) is Library -> AndroidLibraryPlugin( sourceSets = sourceSets, @@ -109,7 +119,7 @@ class AndroidPlatformPluginFactory @Inject constructor( kotlinAndroidExtensionEnabled = hasKotlinAndroidExtensions, manifests = manifests, androidResourcesEnabled = androidResourcesEnabled, - buildConfigEnabled = buildConfigEnabled + buildConfigEnabled = buildConfigEnabled, resValues = resValues ) is Test -> AndroidTestPlugin( sourceSets = sourceSets, @@ -118,11 +128,53 @@ class AndroidPlatformPluginFactory @Inject constructor( viewBindingEnabled = viewBindingEnabled, kotlinAndroidExtensionEnabled = hasKotlinAndroidExtensions, manifests = manifests, - buildConfigEnabled = buildConfigEnabled + buildConfigEnabled = buildConfigEnabled, resValues = resValues ) } } + private fun parseResValues( + type: Type<*> + ): MutableMap> { + fun AndroidCommonExtension.mergedFlavors(): List { + return when (this) { + is AppExtension -> applicationVariants.map { it.cast().mergedFlavor } + is LibraryExtension -> libraryVariants.map { it.cast().mergedFlavor } + else -> emptyList() + } + } + + fun AndroidCommonExtension.buildTypes(): List { + return when (this) { + is AppExtension -> applicationVariants.mapNotNull { it.buildType } + is LibraryExtension -> libraryVariants.mapNotNull { it.buildType } + else -> emptyList() + } + } + + val mfs = type.extension.mergedFlavors() + .associate { mf -> + val sourceSetName = mf.name.asSourceSetName() + + sourceSetName to mf.resValues.values + .mapNotNull { classField -> + UnqualifiedAndroidResourceDeclaredName.fromValuePair(classField.type, classField.name) + }.toSet() + }.toMutableMap() + + type.extension.buildTypes() + .forEach { buildType -> + val sourceSetName = buildType.name.asSourceSetName() + + mfs[sourceSetName] = buildType.resValues.values + .mapNotNull { classField -> + UnqualifiedAndroidResourceDeclaredName.fromValuePair(classField.type, classField.name) + }.toSet() + } + + return mfs + } + sealed interface Type { val extension: T diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt index 4c4b6690fc..07e3b9f065 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt @@ -26,6 +26,7 @@ import modulecheck.specs.ProjectBuildSpecBuilder import modulecheck.specs.ProjectSettingsSpecBuilder import modulecheck.specs.ProjectSpec import modulecheck.specs.ProjectSrcSpec +import modulecheck.specs.ProjectSrcSpecBuilder.RawFile import modulecheck.utils.applyEach import org.junit.jupiter.api.Test import java.io.File @@ -125,24 +126,7 @@ class UnusedDependenciesPluginTest : BasePluginTest() { @Test fun `module with an auto-generated manifest and a string resource used in subject module should not be unused`() { - val appFile = FileSpec.builder("com.example.app", "MyApp") - .addType( - TypeSpec.classBuilder("MyApp") - .addProperty( - PropertySpec.builder("appNameRes", Int::class.asTypeName()) - .getter( - FunSpec.getterBuilder() - .addCode( - """return %T.string.app_name""", - ClassName.bestGuess("com.example.app.R") - ) - .build() - ) - .build() - ) - .build() - ) - .build() + val appFile = createAppFile() val appProject = ProjectSpec("app") { addBuildSpec( @@ -321,4 +305,88 @@ class UnusedDependenciesPluginTest : BasePluginTest() { shouldSucceed("moduleCheck") } + + @Test + fun `module with generated string resource used in subject module should not be unused`() { + val appProject = ProjectSpec("app") { + addBuildSpec( + ProjectBuildSpec { + addPlugin("""id("com.android.library")""") + addPlugin("kotlin(\"android\")") + android = true + addProjectDependency("api", jvmSub1) + } + ) + addSrcSpec( + ProjectSrcSpec(Path.of("src/main/java")) { + addFileSpec(createAppFile()) + } + ) + addSrcSpec( + ProjectSrcSpec(Path.of("src/main")) { + addRawFile( + RawFile( + "AndroidManifest.xml", + """ + """.trimMargin() + ) + ) + } + ) + } + + val androidSub1 = ProjectSpec("lib-1") { + + addBuildSpec( + ProjectBuildSpec { + addPlugin("""id("com.android.library")""") + addPlugin("kotlin(\"android\")") + android = true + addBlock( + """ + android { + defaultConfig { + resValue("string", "app_name", "AppName") + } + buildTypes { + getByName("debug") { + resValue("string", "debug_thing", "debug!") + } + } + } + """ + ) + } + ) + } + + ProjectSpec("project") { + addSubproject(appProject) + addSubproject(androidSub1) + addSettingsSpec(projectSettings.build()) + addBuildSpec(projectBuild.build()) + } + .writeIn(testProjectDir.toPath()) + + shouldSucceed("moduleCheck") + } + + fun createAppFile() = FileSpec.builder("com.example.app", "MyApp") + .addType( + TypeSpec.classBuilder("MyApp") + .addProperty( + PropertySpec.builder("appNameRes", Int::class.asTypeName()) + .getter( + FunSpec.getterBuilder() + .addCode( + """return %T.string.app_name""", + ClassName.bestGuess("com.example.app.R") + ) + .build() + ) + .build() + ) + .build() + ) + .build() } diff --git a/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/PlatformPluginBuilder.kt b/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/PlatformPluginBuilder.kt index 7ecd5b5f1a..3ec1b066dd 100644 --- a/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/PlatformPluginBuilder.kt +++ b/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/PlatformPluginBuilder.kt @@ -27,6 +27,7 @@ import modulecheck.parsing.gradle.JvmPlatformPlugin.KotlinJvmPlugin import modulecheck.parsing.gradle.PlatformPlugin import modulecheck.parsing.gradle.SourceSetName import modulecheck.parsing.gradle.SourceSets +import modulecheck.parsing.source.UnqualifiedAndroidResourceDeclaredName import java.io.File interface PlatformPluginBuilder { @@ -62,6 +63,7 @@ interface AndroidPlatformPluginBuilder : PlatformPlug var nonTransientRClass: Boolean var kotlinAndroidExtensionEnabled: Boolean val manifests: MutableMap + val resValues: MutableMap> } data class AndroidApplicationPluginBuilder( @@ -70,7 +72,8 @@ data class AndroidApplicationPluginBuilder( override var kotlinAndroidExtensionEnabled: Boolean = true, override val manifests: MutableMap = mutableMapOf(), override val sourceSets: MutableMap = mutableMapOf(), - override val configurations: MutableMap = mutableMapOf() + override val configurations: MutableMap = mutableMapOf(), + override val resValues: MutableMap> = mutableMapOf() ) : AndroidPlatformPluginBuilder { override fun toPlugin(): AndroidApplicationPlugin = AndroidApplicationPlugin( sourceSets = SourceSets(sourceSets.mapValues { it.value.toSourceSet() }), @@ -78,7 +81,8 @@ data class AndroidApplicationPluginBuilder( nonTransientRClass = nonTransientRClass, viewBindingEnabled = viewBindingEnabled, kotlinAndroidExtensionEnabled = kotlinAndroidExtensionEnabled, - manifests = manifests + manifests = manifests, + resValues = resValues ) } @@ -90,7 +94,8 @@ data class AndroidLibraryPluginBuilder( var androidResourcesEnabled: Boolean = true, override val manifests: MutableMap = mutableMapOf(), override val sourceSets: MutableMap = mutableMapOf(), - override val configurations: MutableMap = mutableMapOf() + override val configurations: MutableMap = mutableMapOf(), + override val resValues: MutableMap> = mutableMapOf() ) : AndroidPlatformPluginBuilder { override fun toPlugin(): AndroidLibraryPlugin = AndroidLibraryPlugin( sourceSets = SourceSets(sourceSets.mapValues { it.value.toSourceSet() }), @@ -100,7 +105,8 @@ data class AndroidLibraryPluginBuilder( kotlinAndroidExtensionEnabled = kotlinAndroidExtensionEnabled, manifests = manifests, androidResourcesEnabled = androidResourcesEnabled, - buildConfigEnabled = buildConfigEnabled + buildConfigEnabled = buildConfigEnabled, + resValues = resValues ) } @@ -111,7 +117,8 @@ data class AndroidDynamicFeaturePluginBuilder( var buildConfigEnabled: Boolean = true, override val manifests: MutableMap = mutableMapOf(), override val sourceSets: MutableMap = mutableMapOf(), - override val configurations: MutableMap = mutableMapOf() + override val configurations: MutableMap = mutableMapOf(), + override val resValues: MutableMap> = mutableMapOf() ) : AndroidPlatformPluginBuilder { override fun toPlugin(): AndroidDynamicFeaturePlugin = AndroidDynamicFeaturePlugin( sourceSets = SourceSets(sourceSets.mapValues { it.value.toSourceSet() }), @@ -120,7 +127,8 @@ data class AndroidDynamicFeaturePluginBuilder( viewBindingEnabled = viewBindingEnabled, kotlinAndroidExtensionEnabled = kotlinAndroidExtensionEnabled, manifests = manifests, - buildConfigEnabled = buildConfigEnabled + buildConfigEnabled = buildConfigEnabled, + resValues = resValues ) } @@ -131,7 +139,8 @@ data class AndroidTestPluginBuilder( var buildConfigEnabled: Boolean = true, override val manifests: MutableMap = mutableMapOf(), override val sourceSets: MutableMap = mutableMapOf(), - override val configurations: MutableMap = mutableMapOf() + override val configurations: MutableMap = mutableMapOf(), + override val resValues: MutableMap> = mutableMapOf() ) : AndroidPlatformPluginBuilder { override fun toPlugin(): AndroidTestPlugin = AndroidTestPlugin( sourceSets = SourceSets(sourceSets.mapValues { it.value.toSourceSet() }), @@ -140,6 +149,7 @@ data class AndroidTestPluginBuilder( viewBindingEnabled = viewBindingEnabled, kotlinAndroidExtensionEnabled = kotlinAndroidExtensionEnabled, manifests = manifests, - buildConfigEnabled = buildConfigEnabled + buildConfigEnabled = buildConfigEnabled, + resValues = resValues ) }