Skip to content

Commit 6ac964c

Browse files
committed
support generated manifest files
fixes #196
1 parent 19493d2 commit 6ac964c

File tree

7 files changed

+279
-44
lines changed

7 files changed

+279
-44
lines changed

modulecheck-parsing/xml/src/main/kotlin/modulecheck/parsing/xml/AndroidManifestParser.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package modulecheck.parsing.xml
1818
import groovy.util.Node
1919
import groovy.xml.XmlParser
2020
import java.io.File
21+
import kotlin.collections.MutableMap.MutableEntry
2122

2223
object AndroidManifestParser {
2324
private val parser = XmlParser()
@@ -28,8 +29,6 @@ object AndroidManifestParser {
2829
.mapNotNull { it.attributes() }
2930
.flatMap { it.entries }
3031
.filterNotNull()
31-
// .flatMap { it.values.mapNotNull { value -> value } }
32-
.filterIsInstance<MutableMap.MutableEntry<String, String>>()
33-
.map { it.key to it.value }
34-
.toMap()
32+
.filterIsInstance<MutableEntry<String, String>>()
33+
.associate { it.key to it.value }
3534
}

modulecheck-plugin/src/main/kotlin/modulecheck/gradle/GradleProjectProvider.kt

+16-9
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import modulecheck.api.*
3030
import modulecheck.api.anvil.AnvilGradlePlugin
3131
import modulecheck.core.parse
3232
import modulecheck.core.rule.KAPT_PLUGIN_ID
33+
import modulecheck.gradle.internal.androidManifests
3334
import modulecheck.gradle.internal.existingFiles
34-
import modulecheck.gradle.internal.srcRoot
35+
import modulecheck.gradle.internal.testedExtensionOrNull
3536
import modulecheck.parsing.DependencyBlockParser
3637
import modulecheck.parsing.MavenCoordinates
3738
import modulecheck.parsing.xml.AndroidManifestParser
@@ -243,19 +244,25 @@ class GradleProjectProvider(
243244
}
244245

245246
private fun GradleProject.androidPackageOrNull(): String? {
246-
val manifest = File("$srcRoot/main/AndroidManifest.xml".replace("/", File.separator))
247247

248-
if (!manifest.exists()) return null
249-
250-
return AndroidManifestParser.parse(manifest)["package"]
248+
return androidManifests()
249+
?.filter { it.exists() }
250+
?.map { AndroidManifestParser.parse(it)["package"] }
251+
?.distinct()
252+
?.also {
253+
require(it.size == 1) {
254+
"""ModuleCheck only supports a single base package. The following packages are present for module `$path`:
255+
|
256+
|${it.joinToString("\n")}
257+
""".trimMargin()
258+
}
259+
}
260+
?.single()
251261
}
252262

253263
private fun GradleProject.androidResourceFiles(): Set<File> {
254-
val testedExtension =
255-
extensions.findByType(LibraryExtension::class.java)
256-
?: extensions.findByType(AppExtension::class.java)
257264

258-
return testedExtension
265+
return testedExtensionOrNull()
259266
?.sourceSets
260267
?.flatMap { sourceSet ->
261268
sourceSet.res.getSourceFiles().toList()

modulecheck-plugin/src/main/kotlin/modulecheck/gradle/ModuleCheckPlugin.kt

+38-7
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,22 @@
1515

1616
package modulecheck.gradle
1717

18+
import com.android.build.gradle.tasks.GenerateBuildConfig
19+
import com.android.build.gradle.tasks.ManifestProcessorTask
1820
import modulecheck.api.Finding
1921
import modulecheck.api.Project2
2022
import modulecheck.core.rule.ModuleCheckRule
2123
import modulecheck.core.rule.ModuleCheckRuleFactory
24+
import modulecheck.gradle.internal.isMissingManifestFile
2225
import modulecheck.gradle.task.ModuleCheckAllTask
2326
import modulecheck.gradle.task.ModuleCheckTask
2427
import org.gradle.api.Plugin
2528
import org.gradle.api.Project
29+
import org.gradle.api.Task
2630
import org.gradle.api.tasks.Internal
2731
import org.gradle.kotlin.dsl.configure
28-
import org.gradle.kotlin.dsl.register
2932
import javax.inject.Inject
33+
import kotlin.reflect.KClass
3034

3135
fun Project.moduleCheck(config: ModuleCheckExtension.() -> Unit) {
3236
extensions.configure(ModuleCheckExtension::class, config)
@@ -47,17 +51,44 @@ class ModuleCheckPlugin : Plugin<Project> {
4751

4852
val rules = factory.create(settings)
4953

50-
rules
51-
.onEach { rule ->
52-
target.tasks.register("moduleCheck${rule.id}", DynamicModuleCheckTask::class, rule)
53-
}
54+
rules.map { rule ->
55+
target.registerTask(
56+
name = "moduleCheck${rule.id}",
57+
type = DynamicModuleCheckTask::class,
58+
rules = rule
59+
)
60+
}
61+
62+
target.registerTask(
63+
name = "moduleCheck",
64+
type = ModuleCheckAllTask::class,
65+
rules = rules
66+
)
67+
}
5468

55-
target.tasks.register("moduleCheck", ModuleCheckAllTask::class.java, rules)
69+
private fun Project.registerTask(
70+
name: String,
71+
type: KClass<out Task>,
72+
rules: Any
73+
) {
74+
tasks.register(name, type.java, rules)
75+
.configure {
76+
77+
allprojects
78+
.filter { it.isMissingManifestFile() }
79+
.flatMap { it.tasks.withType(ManifestProcessorTask::class.java) }
80+
.forEach { dependsOn(it) }
81+
82+
allprojects
83+
.flatMap { it.tasks.withType(GenerateBuildConfig::class.java) }
84+
.forEach { dependsOn(it) }
85+
}
5686
}
5787
}
5888

5989
abstract class DynamicModuleCheckTask<T : Finding> @Inject constructor(
60-
@Internal val rule: ModuleCheckRule<T>
90+
@Internal
91+
val rule: ModuleCheckRule<T>
6192
) : ModuleCheckTask() {
6293

6394
init {

modulecheck-plugin/src/main/kotlin/modulecheck/gradle/internal/project.kt

+24-20
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,6 @@ val Project.mainKotlinRoot get() = File("$srcRoot/main/kotlin")
2828
val Project.androidTestKotlinRoot get() = File("$srcRoot/androidTest/kotlin")
2929
val Project.testKotlinRoot get() = File("$srcRoot/test/kotlin")
3030

31-
fun Project.mainLayoutRootOrNull(): File? {
32-
val file = File("$srcRoot/main/res/layout")
33-
return if (file.exists()) file else null
34-
}
35-
36-
fun Project.androidTestResRootOrNull(): File? {
37-
val file = File("$srcRoot/androidTest/res")
38-
return if (file.exists()) file else null
39-
}
40-
41-
fun Project.mainResRootOrNull(): File? {
42-
val file = File("$srcRoot/main/res")
43-
return if (file.exists()) file else null
44-
}
45-
46-
fun Project.testResRootOrNull(): File? {
47-
val file = File("$srcRoot/test/res")
48-
return if (file.exists()) file else null
49-
}
50-
5131
fun FileTreeWalk.dirs(): Sequence<File> = asSequence().filter { it.isDirectory }
5232
fun FileTreeWalk.files(): Sequence<File> = asSequence().filter { it.isFile }
5333

@@ -59,3 +39,27 @@ fun createFile(
5939
}
6040

6141
fun Project.isAndroid(): Boolean = extensions.findByType(TestedExtension::class) != null
42+
43+
fun Project.testedExtensionOrNull(): TestedExtension? = extensions
44+
.findByType(TestedExtension::class)
45+
46+
fun Project.androidManifests() = testedExtensionOrNull()
47+
?.sourceSets
48+
?.map { it.manifest.srcFile }
49+
50+
/**
51+
* @return the main src `AndroidManifest.xml` file if it exists. This will typically be
52+
* `$projectDir/src/main/AndroidManifest.xml`, but if the position has
53+
* been changed in the Android extension, the new path will be used.
54+
*/
55+
fun Project.mainAndroidManifest() = testedExtensionOrNull()
56+
?.sourceSets
57+
?.getByName("main")
58+
?.manifest
59+
?.srcFile
60+
61+
/**
62+
* @return true if the project is an Android project and no manifest file exists at the location
63+
* defined in the Android extension
64+
*/
65+
fun Project.isMissingManifestFile() = mainAndroidManifest()?.exists() ?: false

modulecheck-plugin/src/test/kotlin/modulecheck/gradle/BasePluginTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ abstract class BasePluginTest : BaseTest() {
4848
}
4949

5050
fun BuildResult.shouldSucceed() {
51-
tasks.forEach { it.outcome shouldBe TaskOutcome.SUCCESS }
51+
tasks.last().outcome shouldBe TaskOutcome.SUCCESS
5252
}
5353

5454
fun shouldFailWithMessage(vararg tasks: String, messageBlock: (String) -> Unit) {

modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt

+138
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,144 @@ class UnusedDependenciesTest : BasePluginTest() {
852852
build("moduleCheckUnusedDependency").shouldSucceed()
853853
}
854854

855+
@Test
856+
fun `module with an auto-generated manifest and a string resource used in subject module should not be unused`() {
857+
858+
val appFile = FileSpec.builder("com.example.app", "MyApp")
859+
.addType(
860+
TypeSpec.classBuilder("MyApp")
861+
.addProperty(
862+
PropertySpec.builder("appNameRes", Int::class.asTypeName())
863+
.getter(
864+
FunSpec.getterBuilder()
865+
.addCode(
866+
"""return %T.string.app_name""",
867+
ClassName.bestGuess("com.example.app.R")
868+
)
869+
.build()
870+
)
871+
.build()
872+
)
873+
.build()
874+
)
875+
.build()
876+
877+
val appProject = ProjectSpec("app") {
878+
addBuildSpec(
879+
ProjectBuildSpec {
880+
addPlugin("""id("com.android.library")""")
881+
addPlugin("kotlin(\"android\")")
882+
android = true
883+
addProjectDependency("api", jvmSub1)
884+
}
885+
)
886+
addSrcSpec(
887+
ProjectSrcSpec(Path.of("src/main/java")) {
888+
addFileSpec(appFile)
889+
}
890+
)
891+
addSrcSpec(
892+
ProjectSrcSpec(Path.of("src/main")) {
893+
addRawFile(
894+
RawFile(
895+
"AndroidManifest.xml",
896+
"""<manifest package="com.example.app" />
897+
""".trimMargin()
898+
)
899+
)
900+
}
901+
)
902+
}
903+
904+
val androidSub1 = ProjectSpec("lib-1") {
905+
906+
// without this, the standard manifest will be generated and this test won't be testing anything
907+
disableAutoManifest = true
908+
909+
addSrcSpec(
910+
ProjectSrcSpec(Path.of("src/main/res/values")) {
911+
addRawFile(
912+
RawFile(
913+
"strings.xml",
914+
"""<resources>
915+
| <string name="app_name" translatable="false">MyApp</string>
916+
|</resources>
917+
""".trimMargin()
918+
)
919+
)
920+
}
921+
)
922+
addBuildSpec(
923+
ProjectBuildSpec {
924+
addPlugin("""id("com.android.library")""")
925+
addPlugin("kotlin(\"android\")")
926+
android = true
927+
// This reproduces the behavior of Auto-Manifest:
928+
// https://github.com/GradleUp/auto-manifest
929+
// For some reason, that plugin doesn't work with Gradle TestKit. Its task is never
930+
// registered, and the manifest location is never changed from the default. When I open
931+
// the generated project dir and execute the task from terminal, it works fine...
932+
// This does the same thing, but uses a different default directory.
933+
addBlock(
934+
"""
935+
|val manifestFile = file("${'$'}buildDir/generated/my-custom-manifest-location/AndroidManifest.xml")
936+
|
937+
|android {
938+
| sourceSets {
939+
| findByName("main")?.manifest {
940+
| srcFile(manifestFile.path)
941+
| }
942+
| }
943+
|}
944+
|
945+
|val makeFile by tasks.registering {
946+
|
947+
| doFirst {
948+
|
949+
| manifestFile.parentFile.mkdirs()
950+
| manifestFile.writeText(
951+
| ""${'"'}<manifest package="com.example.lib1" /> ""${'"'}.trimMargin()
952+
| )
953+
| }
954+
|}
955+
|
956+
|afterEvaluate {
957+
|
958+
| tasks.withType(com.android.build.gradle.tasks.GenerateBuildConfig::class.java)
959+
| .configureEach { dependsOn(makeFile) }
960+
| tasks.withType(com.android.build.gradle.tasks.MergeResources::class.java)
961+
| .configureEach { dependsOn(makeFile) }
962+
| tasks.withType(com.android.build.gradle.tasks.ManifestProcessorTask::class.java)
963+
| .configureEach { dependsOn(makeFile)}
964+
|
965+
|}
966+
""".trimMargin()
967+
)
968+
}
969+
)
970+
}
971+
972+
ProjectSpec("project") {
973+
addSubproject(appProject)
974+
addSubproject(androidSub1)
975+
addSettingsSpec(projectSettings.build())
976+
addBuildSpec(
977+
projectBuild
978+
.addBlock(
979+
"""moduleCheck {
980+
| autoCorrect = false
981+
|}
982+
""".trimMargin()
983+
).build()
984+
)
985+
}
986+
.writeIn(testProjectDir.toPath())
987+
988+
build("moduleCheckUnusedDependency").shouldSucceed()
989+
// one last check to make sure the manifest wasn't generated, since that would invalidate the test
990+
File(testProjectDir, "/lib1/src/main/AndroidManifest.xml").exists() shouldBe false
991+
}
992+
855993
@Test
856994
fun `module with a declaration used via a class reference with wildcard import should not be unused`() {
857995
val appProject = ProjectSpec("app") {

0 commit comments

Comments
 (0)