Skip to content

Support generated manifests #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package modulecheck.parsing.xml
import groovy.util.Node
import groovy.xml.XmlParser
import java.io.File
import kotlin.collections.MutableMap.MutableEntry

object AndroidManifestParser {
private val parser = XmlParser()
Expand All @@ -28,8 +29,6 @@ object AndroidManifestParser {
.mapNotNull { it.attributes() }
.flatMap { it.entries }
.filterNotNull()
// .flatMap { it.values.mapNotNull { value -> value } }
.filterIsInstance<MutableMap.MutableEntry<String, String>>()
.map { it.key to it.value }
.toMap()
.filterIsInstance<MutableEntry<String, String>>()
.associate { it.key to it.value }
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ import modulecheck.api.*
import modulecheck.api.anvil.AnvilGradlePlugin
import modulecheck.core.parse
import modulecheck.core.rule.KAPT_PLUGIN_ID
import modulecheck.gradle.internal.androidManifests
import modulecheck.gradle.internal.existingFiles
import modulecheck.gradle.internal.srcRoot
import modulecheck.gradle.internal.testedExtensionOrNull
import modulecheck.parsing.DependencyBlockParser
import modulecheck.parsing.MavenCoordinates
import modulecheck.parsing.xml.AndroidManifestParser
Expand Down Expand Up @@ -243,19 +244,25 @@ class GradleProjectProvider(
}

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

if (!manifest.exists()) return null

return AndroidManifestParser.parse(manifest)["package"]
return androidManifests()
?.filter { it.exists() }
?.map { AndroidManifestParser.parse(it)["package"] }
?.distinct()
?.also {
require(it.size == 1) {
"""ModuleCheck only supports a single base package. The following packages are present for module `$path`:
|
|${it.joinToString("\n")}
""".trimMargin()
}
}
?.single()
}

private fun GradleProject.androidResourceFiles(): Set<File> {
val testedExtension =
extensions.findByType(LibraryExtension::class.java)
?: extensions.findByType(AppExtension::class.java)

return testedExtension
return testedExtensionOrNull()
?.sourceSets
?.flatMap { sourceSet ->
sourceSet.res.getSourceFiles().toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@

package modulecheck.gradle

import com.android.build.gradle.tasks.GenerateBuildConfig
import com.android.build.gradle.tasks.ManifestProcessorTask
import modulecheck.api.Finding
import modulecheck.api.Project2
import modulecheck.core.rule.ModuleCheckRule
import modulecheck.core.rule.ModuleCheckRuleFactory
import modulecheck.gradle.internal.isMissingManifestFile
import modulecheck.gradle.task.ModuleCheckAllTask
import modulecheck.gradle.task.ModuleCheckTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.Internal
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.register
import javax.inject.Inject
import kotlin.reflect.KClass

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

val rules = factory.create(settings)

rules
.onEach { rule ->
target.tasks.register("moduleCheck${rule.id}", DynamicModuleCheckTask::class, rule)
}
rules.map { rule ->
target.registerTask(
name = "moduleCheck${rule.id}",
type = DynamicModuleCheckTask::class,
rules = rule
)
}

target.registerTask(
name = "moduleCheck",
type = ModuleCheckAllTask::class,
rules = rules
)
}

target.tasks.register("moduleCheck", ModuleCheckAllTask::class.java, rules)
private fun Project.registerTask(
name: String,
type: KClass<out Task>,
rules: Any
) {
tasks.register(name, type.java, rules)
.configure {

allprojects
.filter { it.isMissingManifestFile() }
.flatMap { it.tasks.withType(ManifestProcessorTask::class.java) }
.forEach { dependsOn(it) }

allprojects
.flatMap { it.tasks.withType(GenerateBuildConfig::class.java) }
.forEach { dependsOn(it) }
}
}
}

abstract class DynamicModuleCheckTask<T : Finding> @Inject constructor(
@Internal val rule: ModuleCheckRule<T>
@Internal
val rule: ModuleCheckRule<T>
) : ModuleCheckTask() {

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,6 @@ val Project.mainKotlinRoot get() = File("$srcRoot/main/kotlin")
val Project.androidTestKotlinRoot get() = File("$srcRoot/androidTest/kotlin")
val Project.testKotlinRoot get() = File("$srcRoot/test/kotlin")

fun Project.mainLayoutRootOrNull(): File? {
val file = File("$srcRoot/main/res/layout")
return if (file.exists()) file else null
}

fun Project.androidTestResRootOrNull(): File? {
val file = File("$srcRoot/androidTest/res")
return if (file.exists()) file else null
}

fun Project.mainResRootOrNull(): File? {
val file = File("$srcRoot/main/res")
return if (file.exists()) file else null
}

fun Project.testResRootOrNull(): File? {
val file = File("$srcRoot/test/res")
return if (file.exists()) file else null
}

fun FileTreeWalk.dirs(): Sequence<File> = asSequence().filter { it.isDirectory }
fun FileTreeWalk.files(): Sequence<File> = asSequence().filter { it.isFile }

Expand All @@ -59,3 +39,30 @@ fun createFile(
}

fun Project.isAndroid(): Boolean = extensions.findByType(TestedExtension::class) != null

fun Project.testedExtensionOrNull(): TestedExtension? = extensions
.findByType(TestedExtension::class)

fun Project.androidManifests() = testedExtensionOrNull()
?.sourceSets
?.map { it.manifest.srcFile }

/**
* @return the main src `AndroidManifest.xml` file if it exists. This will typically be
* `$projectDir/src/main/AndroidManifest.xml`, but if the position has
* been changed in the Android extension, the new path will be used.
*/
fun Project.mainAndroidManifest() = testedExtensionOrNull()
?.sourceSets
?.getByName("main")
?.manifest
?.srcFile

/**
* @return true if the project is an Android project and no manifest file exists at the location
* defined in the Android extension
*/
fun Project.isMissingManifestFile() = mainAndroidManifest()
// the file must be declared, but not exist in order for this to be triggered
?.let { !it.exists() }
?: false
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ abstract class BasePluginTest : BaseTest() {
}

fun BuildResult.shouldSucceed() {
tasks.forEach { it.outcome shouldBe TaskOutcome.SUCCESS }
tasks.last().outcome shouldBe TaskOutcome.SUCCESS
}

fun shouldFailWithMessage(vararg tasks: String, messageBlock: (String) -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,144 @@ class UnusedDependenciesTest : BasePluginTest() {
build("moduleCheckUnusedDependency").shouldSucceed()
}

@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 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(appFile)
}
)
addSrcSpec(
ProjectSrcSpec(Path.of("src/main")) {
addRawFile(
RawFile(
"AndroidManifest.xml",
"""<manifest package="com.example.app" />
""".trimMargin()
)
)
}
)
}

val androidSub1 = ProjectSpec("lib-1") {

// without this, the standard manifest will be generated and this test won't be testing anything
disableAutoManifest = true

addSrcSpec(
ProjectSrcSpec(Path.of("src/main/res/values")) {
addRawFile(
RawFile(
"strings.xml",
"""<resources>
| <string name="app_name" translatable="false">MyApp</string>
|</resources>
""".trimMargin()
)
)
}
)
addBuildSpec(
ProjectBuildSpec {
addPlugin("""id("com.android.library")""")
addPlugin("kotlin(\"android\")")
android = true
// This reproduces the behavior of Auto-Manifest:
// https://github.com/GradleUp/auto-manifest
// For some reason, that plugin doesn't work with Gradle TestKit. Its task is never
// registered, and the manifest location is never changed from the default. When I open
// the generated project dir and execute the task from terminal, it works fine...
// This does the same thing, but uses a different default directory.
addBlock(
"""
|val manifestFile = file("${'$'}buildDir/generated/my-custom-manifest-location/AndroidManifest.xml")
|
|android {
| sourceSets {
| findByName("main")?.manifest {
| srcFile(manifestFile.path)
| }
| }
|}
|
|val makeFile by tasks.registering {
|
| doFirst {
|
| manifestFile.parentFile.mkdirs()
| manifestFile.writeText(
| ""${'"'}<manifest package="com.example.lib1" /> ""${'"'}.trimMargin()
| )
| }
|}
|
|afterEvaluate {
|
| tasks.withType(com.android.build.gradle.tasks.GenerateBuildConfig::class.java)
| .configureEach { dependsOn(makeFile) }
| tasks.withType(com.android.build.gradle.tasks.MergeResources::class.java)
| .configureEach { dependsOn(makeFile) }
| tasks.withType(com.android.build.gradle.tasks.ManifestProcessorTask::class.java)
| .configureEach { dependsOn(makeFile)}
|
|}
""".trimMargin()
)
}
)
}

ProjectSpec("project") {
addSubproject(appProject)
addSubproject(androidSub1)
addSettingsSpec(projectSettings.build())
addBuildSpec(
projectBuild
.addBlock(
"""moduleCheck {
| autoCorrect = false
|}
""".trimMargin()
).build()
)
}
.writeIn(testProjectDir.toPath())

build("moduleCheckUnusedDependency").shouldSucceed()
// one last check to make sure the manifest wasn't generated, since that would invalidate the test
File(testProjectDir, "/lib1/src/main/AndroidManifest.xml").exists() shouldBe false
}

@Test
fun `module with a declaration used via a class reference with wildcard import should not be unused`() {
val appProject = ProjectSpec("app") {
Expand Down
Loading