Skip to content

Commit

Permalink
feat: add DependenciesSimplifier recipe.
Browse files Browse the repository at this point in the history
  • Loading branch information
autonomousapps committed Dec 4, 2024
1 parent 2ee7806 commit 09b9d0f
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
10 changes: 10 additions & 0 deletions recipes/dependencies/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
id("cash.lib")
}

dependencies {
api(project(":core"))
api(project(":grammar"))
api(libs.antlr.runtime)
api(libs.kotlinStdLib)
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ dependencyResolutionManagement {

include(":core")
include(":grammar")
include(":recipes:dependencies")
include(":recipes:plugins")
include(":recipes:repos")

0 comments on commit 09b9d0f

Please sign in to comment.