Skip to content

Commit

Permalink
Don't add .lightsaber files inside the classpath (#292)
Browse files Browse the repository at this point in the history
* Improve code

* Pass output path as argument
  • Loading branch information
BraisGabin authored Jan 17, 2025
1 parent ec92f58 commit 5eedaa5
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,74 @@ class LightsaberPluginIntegrationTest {
assertThat(buildResult).contains("> Analysis failed with 2 errors")
}

@Test
fun annotationProcessor_cache() {
val runner = GradleRunner.create()
.withProjectDirFromResources("annotationProcessor")
.withPluginClasspath()
.withArguments("lightsaberCheck", "--build-cache")

runner.buildAndFail()

runner.projectDir.resolve("build/generated/lightsaber").deleteRecursively()

val buildResult = runner.buildAndFail()

assertThat(buildResult).hasTask(":compileJava").hasOutcome(TaskOutcome.FROM_CACHE)
assertThat(buildResult).hasTask(":compileTestJava")
assertThat(buildResult).hasTask(":lightsaberCheck").hasOutcome(TaskOutcome.FAILED)

assertThat(buildResult).contains("MyModule.java:15:17: The @Provides `myLong` declared in `com.example.MyModule` is not used. [UnusedBindsAndProvides]")
assertThat(buildResult).contains("Foo.java:5:8: The @Inject in `com.example.Foo` constructor is unused because there is a @Provides defined in `com.example.MyModule.foo`. [UnusedInject]")
assertThat(buildResult).contains("> Analysis failed with 2 errors")
assertThat(buildResult).doesNotContain("warning:")
}

@Test
fun kapt_cache() {
val runner = GradleRunner.create()
.withProjectDirFromResources("kapt")
.withPluginClasspath()
.withArguments("lightsaberCheck", "--build-cache")

runner.buildAndFail()

runner.projectDir.resolve("build/generated/lightsaber").deleteRecursively()

val buildResult = runner.buildAndFail()

assertThat(buildResult).hasTask(":kaptKotlin").hasOutcome(TaskOutcome.FROM_CACHE)
assertThat(buildResult).hasTask(":kaptTestKotlin")
assertThat(buildResult).hasTask(":lightsaberCheck").hasOutcome(TaskOutcome.FAILED)

assertThat(buildResult).contains("MyModule.java:26:27: The @Provides `myLong` declared in `com.example.MyModule` is not used. [UnusedBindsAndProvides]")
assertThat(buildResult).contains("Foo.java:4:14: The @Inject in `com.example.Foo` constructor is unused because there is a @Provides defined in `com.example.MyModule.Companion.provideFoo`. [UnusedInject]")
assertThat(buildResult).contains("> Analysis failed with 2 errors")
assertThat(buildResult).doesNotContain("warning:")
}

@Test
fun ksp_cache() {
val runner = GradleRunner.create()
.withProjectDirFromResources("ksp")
.withPluginClasspath()
.withArguments("lightsaberCheck", "--build-cache")

runner.buildAndFail()

runner.projectDir.resolve("build/generated/lightsaber").deleteRecursively()

val buildResult = runner.buildAndFail()

assertThat(buildResult).hasTask(":kspKotlin").hasOutcome(TaskOutcome.FROM_CACHE)
assertThat(buildResult).hasTask(":kspTestKotlin")
assertThat(buildResult).hasTask(":lightsaberCheck").hasOutcome(TaskOutcome.FAILED)

assertThat(buildResult).contains("MyComponent.kt:24: The @Provides `myLong` declared in `com.example.MyModule` is not used. [UnusedBindsAndProvides]")
assertThat(buildResult).contains("MyComponent.kt:33: The @Inject in `com.example.Foo` constructor is unused because there is a @Provides defined in `com.example.MyModule.Companion.provideFoo`. [UnusedInject]")
assertThat(buildResult).contains("> Analysis failed with 2 errors")
}

@Test
fun androidAnnotationProcessor() {
val buildResult = GradleRunner.create()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@ internal fun Project.registerAnnotationProcessorTask(
val variantName = variant?.capitalized()
val lightsaberCheck = registerTask(extension, variantName.orEmpty())
lightsaberCheck.configure { task ->
val lightsaberOutputDir = lightsaberOutputDir("annotationProcessor")
val taskProvider = provider {
if (variantName == null) {
tasks.withType(JavaCompile::class.java)
} else {
tasks.withType(JavaCompile::class.java)
.matching { it.name.startsWith("compile$variantName") }
}
tasks.withType(JavaCompile::class.java)
.matching { it.name.startsWith("compile${variantName.orEmpty()}") }
.apply {
configureEach { javacTask ->
val sourceSet = javacTask.name
.removePrefix("kapt")
.removeSuffix("Kotlin")
.ifEmpty { "main" }
.replaceFirstChar { it.lowercaseChar() }
val output = lightsaberOutputDir.map { it.dir(sourceSet) }
javacTask.options.compilerArgumentProviders.add(LightsaberArgumentProvider(output))

javacTask.outputs.dir(output)
}
}
}
task.dependsOn(taskProvider)

task.source = taskProvider.get()
.map { fileTree(it.destinationDirectory.dir("schwarz/it/lightsaber")).asFileTree }
.reduce { acc, fileTree -> acc.plus(fileTree) }
.matching { it.include("*.lightsaber") }
task.source += fileTree(lightsaberOutputDir)
}
return lightsaberCheck
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,28 @@ internal fun Project.registerKaptTask(
val variantName = variant?.capitalized()
val lightsaberCheck = registerTask(extension, variantName.orEmpty())
lightsaberCheck.configure { task ->
val lightsaberOutputDir = lightsaberOutputDir("kapt")
val taskProvider = provider {
if (variant == null) {
tasks.withType(BaseKapt::class.java)
} else {
tasks.withType(BaseKapt::class.java)
.matching { it.name.startsWith("kapt$variantName") }
}
tasks.withType(BaseKapt::class.java)
.matching { it.name.startsWith("kapt${variantName.orEmpty()}") }
.apply {
configureEach { kaptTask ->
val sourceSet = kaptTask.name
.removePrefix("kapt")
.removeSuffix("Kotlin")
.ifEmpty { "main" }
.replaceFirstChar { it.lowercaseChar() }
val output = lightsaberOutputDir.map { it.dir(sourceSet) }
kaptTask.annotationProcessorOptionProviders
.add(listOf(LightsaberArgumentProvider(output)))

kaptTask.outputs.dir(output)
}
}
}
task.dependsOn(taskProvider)

task.source = taskProvider.get()
.map { fileTree(it.classesDir.dir("schwarz/it/lightsaber")).asFileTree }
.reduce { acc, fileTree -> acc.plus(fileTree) }
.matching { it.include("*.lightsaber") }
task.source += fileTree(lightsaberOutputDir)
}
return lightsaberCheck
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,27 @@ internal fun Project.registerKspTask(
val variantName = variant?.capitalized()
val lightsaberCheck = registerTask(extension, variantName.orEmpty())
lightsaberCheck.configure { task ->
val lightsaberOutputDir = lightsaberOutputDir("ksp")
val taskProvider = provider {
if (variantName == null) {
tasks.withType(KspTaskJvm::class.java)
} else {
tasks.withType(KspTaskJvm::class.java)
.matching { it.name.startsWith("ksp$variantName") }
}
tasks.withType(KspTaskJvm::class.java)
.matching { it.name.startsWith("ksp${variantName.orEmpty()}") }
.apply {
configureEach { kspTask ->
val sourceSet = kspTask.name
.removePrefix("kapt")
.removeSuffix("Kotlin")
.ifEmpty { "main" }
.replaceFirstChar { it.lowercaseChar() }
val output = lightsaberOutputDir.map { it.dir(sourceSet) }
kspTask.commandLineArgumentProviders.add(LightsaberArgumentProvider(output, ksp = true))

kspTask.outputs.dir(output)
}
}
}
task.dependsOn(taskProvider)

task.source = taskProvider.get()
.map { fileTree(it.destination.get().resolve("resources/schwarz/it/lightsaber")).asFileTree }
.reduce { acc, fileTree -> acc.plus(fileTree) }
.matching { it.include("*.lightsaber") }
task.source += fileTree(lightsaberOutputDir)
}
return lightsaberCheck
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package schwarz.it.lightsaber.gradle.processors

import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
import org.gradle.process.CommandLineArgumentProvider

enum class Processor { AnnotationProcessor, Kapt, Ksp }

Expand Down Expand Up @@ -39,3 +42,12 @@ private fun Dependency.isAtLeastVersion(major: Int, minor: Int): Boolean {

private const val MAJOR = 2
private const val MINOR = 48

internal fun Project.lightsaberOutputDir(tech: String) = layout.buildDirectory.dir("generated/lightsaber/$tech")

internal class LightsaberArgumentProvider(
private val outputDir: Provider<Directory>,
private val ksp: Boolean = false,
) : CommandLineArgumentProvider {
override fun asArguments() = listOf("${if (ksp) "" else "-A"}Lightsaber.path=${outputDir.get().asFile.path}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import schwarz.it.lightsaber.checkers.checkUnusedScopes
import schwarz.it.lightsaber.utils.FileGenerator
import schwarz.it.lightsaber.utils.getQualifiedName
import schwarz.it.lightsaber.utils.writeFile
import kotlin.io.path.Path

@AutoService(BindingGraphPlugin::class)
public class LightsaberDaggerProcessor : BindingGraphPlugin {
Expand Down Expand Up @@ -67,7 +68,8 @@ public class LightsaberDaggerProcessor : BindingGraphPlugin {
checkUnusedScopes = options["Lightsaber.CheckUnusedScopes"] != "false",
)
this.daggerProcessingEnv = processingEnv
this.fileGenerator = FileGenerator(processingEnv)
val path = checkNotNull(options["Lightsaber.path"]) { "Lightsaber.path argument not provided" }
this.fileGenerator = FileGenerator(Path(path))
}

override fun supportedOptions(): Set<String> {
Expand All @@ -79,6 +81,7 @@ public class LightsaberDaggerProcessor : BindingGraphPlugin {
"Lightsaber.CheckUnusedMembersInjectionMethods",
"Lightsaber.CheckUnusedModules",
"Lightsaber.CheckUnusedScopes",
"Lightsaber.path",
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement
import kotlin.io.path.Path

class LightsaberJavacProcessor : AbstractProcessor() {
private lateinit var fileGenerator: FileGenerator
private lateinit var rules: Set<Pair<String, LightsaberJavacRule>>

override fun init(processingEnv: ProcessingEnvironment) {
fileGenerator = FileGenerator(processingEnv.filer)
val path = checkNotNull(processingEnv.options["Lightsaber.path"]) { "Lightsaber.path argument not provided" }
fileGenerator = FileGenerator(Path(path))
val elements = processingEnv.elementUtils
rules = buildSet {
if (processingEnv.options["Lightsaber.CheckUnusedInject"] != "false") {
Expand Down Expand Up @@ -46,7 +48,11 @@ class LightsaberJavacProcessor : AbstractProcessor() {
return false
}

override fun getSupportedOptions() = setOf("Lightsaber.CheckUnusedInject", "Lightsaber.CheckUnusedScopes")
override fun getSupportedOptions() = setOf(
"Lightsaber.CheckUnusedInject",
"Lightsaber.CheckUnusedScopes",
"Lightsaber.path",
)

override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import schwarz.it.lightsaber.checkers.UnusedInjectKsp
import schwarz.it.lightsaber.checkers.UnusedScopesKsp
import schwarz.it.lightsaber.utils.FileGenerator
import schwarz.it.lightsaber.utils.writeFile
import kotlin.io.path.Path

internal class LightsaberKspProcessor(
private val fileGenerator: FileGenerator,
Expand Down Expand Up @@ -55,7 +56,8 @@ class LightsaberKspProcessorProvider : SymbolProcessorProvider {
checkUnusedInject = environment.options["Lightsaber.CheckUnusedInject"] != "false",
checkUnusedScopes = environment.options["Lightsaber.CheckUnusedScopes"] != "false",
)
return LightsaberKspProcessor(FileGenerator(environment.codeGenerator), config)
val path = checkNotNull(environment.options["Lightsaber.path"]) { "Lightsaber.path argument not provided" }
return LightsaberKspProcessor(FileGenerator(Path(path)), config)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,21 @@
package schwarz.it.lightsaber.utils

import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import dagger.spi.model.DaggerProcessingEnv
import schwarz.it.lightsaber.Issue
import java.io.OutputStream
import java.io.PrintWriter
import javax.annotation.processing.Filer
import javax.tools.StandardLocation

internal interface FileGenerator {
fun createFile(packageName: String, fileName: String, extension: String): OutputStream

companion object {
operator fun invoke(processingEnv: DaggerProcessingEnv): FileGenerator {
return processingEnv.fold({ FileGenerator(it.filer) }, { FileGenerator(it.codeGenerator) })
}

operator fun invoke(codeGenerator: CodeGenerator): FileGenerator {
return FileGeneratorKsp(codeGenerator)
}

operator fun invoke(filer: Filer): FileGenerator {
return FileGeneratorJavac(filer)
}
}
}

private class FileGeneratorJavac(private val filer: Filer) : FileGenerator {
override fun createFile(packageName: String, fileName: String, extension: String): OutputStream {
return filer
.createResource(StandardLocation.CLASS_OUTPUT, packageName, "$fileName.$extension")
.openOutputStream()
}
}

private class FileGeneratorKsp(private val codeGenerator: CodeGenerator) : FileGenerator {
override fun createFile(packageName: String, fileName: String, extension: String): OutputStream {
return codeGenerator.createNewFile(Dependencies.ALL_FILES, packageName, fileName, extension)
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.outputStream

internal class FileGenerator(private val path: Path) {
fun createFile(fileName: String): OutputStream {
path.createDirectories()
return path.resolve(fileName).outputStream()
}
}

internal fun FileGenerator.writeFile(fileName: String, issues: List<Issue>) {
this.createFile("schwarz.it.lightsaber", fileName, "lightsaber")
this.createFile("$fileName.lightsaber")
.let(::PrintWriter)
.use { writer -> issues.forEach { writer.println("${it.codePosition}: ${it.message} [${it.rule}]") } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.PluginOption
import com.tschuchort.compiletesting.SourceFile
import com.tschuchort.compiletesting.kspProcessorOptions
import com.tschuchort.compiletesting.kspSourcesDir
import com.tschuchort.compiletesting.kspWithCompilation
import com.tschuchort.compiletesting.symbolProcessorProviders
import dagger.internal.codegen.ComponentProcessor
Expand Down Expand Up @@ -74,7 +73,7 @@ internal class KaptKotlinCompiler(
compiler.sources = sourceFiles.asList()
return CompilationResult(
compiler.compile(),
findGeneratedFiles(compiler.classesDir),
compiler.workingDir.resolve("lightsaber").listFiles().orEmpty().asList(),
compiler.kaptStubsDir,
CompilationResult.Type.Kapt,
)
Expand All @@ -100,7 +99,7 @@ internal class KspKotlinCompiler(
compiler.sources = sourceFiles.asList()
return CompilationResult(
compiler.compile(),
findGeneratedFiles(compiler.kspSourcesDir),
compiler.workingDir.resolve("lightsaber").listFiles().orEmpty().asList(),
compiler.workingDir.resolve("sources"),
CompilationResult.Type.Ksp,
)
Expand Down Expand Up @@ -140,18 +139,12 @@ internal val CompilationResult.Type.extension
CompilationResult.Type.Ksp -> "kt"
}

private fun findGeneratedFiles(file: File): List<File> {
return file
.walkTopDown()
.filter { it.isFile }
.toList()
}

private fun getLightsaberArguments(
private fun KotlinCompilation.getLightsaberArguments(
vararg rules: Rule,
): MutableMap<String, String> {
return Rule.entries
.associate { "Lightsaber.Check${it.name}" to (it in rules).toString() }
.plus("Lightsaber.path" to workingDir.resolve("lightsaber").absolutePath)
.toMutableMap()
}

Expand Down

0 comments on commit 5eedaa5

Please sign in to comment.