diff --git a/recipes/dependencies/build.gradle.kts b/recipes/dependencies/build.gradle.kts new file mode 100644 index 0000000..fd39aa1 --- /dev/null +++ b/recipes/dependencies/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("cash.lib") +} + +dependencies { + api(project(":core")) + api(project(":grammar")) + api(libs.antlr.runtime) + api(libs.kotlinStdLib) +} diff --git a/recipes/dependencies/src/main/kotlin/cash/recipes/dependencies/DependenciesSimplifier.kt b/recipes/dependencies/src/main/kotlin/cash/recipes/dependencies/DependenciesSimplifier.kt new file mode 100644 index 0000000..859dcd0 --- /dev/null +++ b/recipes/dependencies/src/main/kotlin/cash/recipes/dependencies/DependenciesSimplifier.kt @@ -0,0 +1,138 @@ +package cash.recipes.dependencies + +import cash.grammar.kotlindsl.model.DependencyDeclaration +import cash.grammar.kotlindsl.parse.KotlinParseException +import cash.grammar.kotlindsl.parse.Parser +import cash.grammar.kotlindsl.parse.Rewriter +import cash.grammar.kotlindsl.utils.Blocks.isBuildscript +import cash.grammar.kotlindsl.utils.Blocks.isDependencies +import cash.grammar.kotlindsl.utils.CollectingErrorListener +import cash.grammar.kotlindsl.utils.Context.leafRule +import cash.grammar.kotlindsl.utils.DependencyExtractor +import cash.grammar.kotlindsl.utils.Whitespace +import cash.grammar.kotlindsl.utils.Whitespace.trimGently +import cash.grammar.utils.ifNotEmpty +import com.squareup.cash.grammar.KotlinParser.NamedBlockContext +import com.squareup.cash.grammar.KotlinParser.PostfixUnaryExpressionContext +import com.squareup.cash.grammar.KotlinParser.StatementContext +import com.squareup.cash.grammar.KotlinParserBaseListener +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.CommonTokenStream +import org.antlr.v4.runtime.Token +import java.io.InputStream +import java.nio.file.Path + +public class DependenciesSimplifier private constructor( + private val input: CharStream, + private val tokens: CommonTokenStream, + private val errorListener: CollectingErrorListener, +) : KotlinParserBaseListener() { + + private val rewriter = Rewriter(tokens) + private val indent = Whitespace.computeIndent(tokens, input) + private val terminalNewlines = Whitespace.countTerminalNewlines(tokens) + private val dependencyExtractor = DependencyExtractor(input, tokens, indent) + + private var isInBuildscriptBlock = false + + @Throws(KotlinParseException::class) + public fun rewritten(): String { + errorListener.getErrorMessages().ifNotEmpty { + throw KotlinParseException.withErrors(it) + } + + return rewriter.text.trimGently(terminalNewlines) + } + + override fun enterNamedBlock(ctx: NamedBlockContext) { + dependencyExtractor.onEnterBlock() + + if (ctx.isBuildscript) { + isInBuildscriptBlock = true + } + } + + override fun exitNamedBlock(ctx: NamedBlockContext) { + if (ctx.isDependencies && !isInBuildscriptBlock) { + onExitDependenciesBlock(ctx) + } + + if (ctx.isBuildscript) { + isInBuildscriptBlock = false + } + + dependencyExtractor.onExitBlock() + } + + private fun onExitDependenciesBlock(ctx: NamedBlockContext) { + val container = dependencyExtractor.collectDependencies(ctx) + container.getDependencyDeclarationsWithContext() + // we only care about complex declarations. We will rewrite this in simplified form + .filter { it.declaration.isComplex } + .forEach { element -> + val declaration = element.declaration + val elementCtx = element.statement + + val newText = simplify(declaration) + + if (newText != null) { + rewriter.replace(elementCtx.start, getStop(elementCtx), newText) + } + } + } + + private fun getStop(ctx: StatementContext): Token { + val default = ctx.stop + + val leaf = ctx.leafRule() + if (leaf !is PostfixUnaryExpressionContext) return default + + val postfix = leaf.postfixUnarySuffix().firstOrNull() ?: return default + val preLambda = postfix.callSuffix().valueArguments() + + // we only want to replace everything BEFORE the trailing lambda + return preLambda.stop + } + + private fun simplify(declaration: DependencyDeclaration): String? { + require(declaration.isComplex) { "Expected complex declaration, was $declaration" } + + // TODO(tsr): For now, ignore those that have ext, classifier, producerConfiguration + if (declaration.ext != null || declaration.classifier != null || declaration.producerConfiguration != null) { + return null + } + + return buildString { + append(declaration.configuration) + append("(") + append(declaration.identifier) + append(")") + } + } + + public companion object { + public fun of(buildScript: Path): DependenciesSimplifier { + return of(Parser.readOnlyInputStream(buildScript)) + } + + public fun of(buildScript: String): DependenciesSimplifier { + return of(buildScript.byteInputStream()) + } + + private fun of(buildScript: InputStream): DependenciesSimplifier { + val errorListener = CollectingErrorListener() + + return Parser( + file = buildScript, + errorListener = errorListener, + listenerFactory = { input, tokens, _ -> + DependenciesSimplifier( + input = input, + tokens = tokens, + errorListener = errorListener, + ) + } + ).listener() + } + } +} diff --git a/recipes/dependencies/src/test/kotlin/cash/recipes/dependencies/DependenciesSimplifierTest.kt b/recipes/dependencies/src/test/kotlin/cash/recipes/dependencies/DependenciesSimplifierTest.kt new file mode 100644 index 0000000..39df181 --- /dev/null +++ b/recipes/dependencies/src/test/kotlin/cash/recipes/dependencies/DependenciesSimplifierTest.kt @@ -0,0 +1,41 @@ +package cash.recipes.dependencies + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class DependenciesSimplifierTest { + + @Test fun `can simplify dependency declarations`() { + // Given + val buildScript = """ + dependencies { + implementation(libs.foo) + api("com.foo:bar:1.0") + runtimeOnly(group = "foo", name = "bar", version = "2.0") + compileOnly(group = "foo", name = "bar", version = libs.versions.bar.get()) { + isTransitive = false + } + devImplementation(group = "io.netty", name = "netty-transport-native-kqueue", classifier = "osx-x86_64") + } + """.trimIndent() + + // When + val simplifier = DependenciesSimplifier.of(buildScript) + val rewrittenContent = simplifier.rewritten() + + // Then all declarations are simplified + assertThat(rewrittenContent).isEqualTo( + """ + dependencies { + implementation(libs.foo) + api("com.foo:bar:1.0") + runtimeOnly("foo:bar:2.0") + compileOnly("foo:bar:${'$'}{libs.versions.bar.get()}") { + isTransitive = false + } + devImplementation(group = "io.netty", name = "netty-transport-native-kqueue", classifier = "osx-x86_64") + } + """.trimIndent() + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7b81729..31edf74 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,5 +25,6 @@ dependencyResolutionManagement { include(":core") include(":grammar") +include(":recipes:dependencies") include(":recipes:plugins") include(":recipes:repos")