Skip to content
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

feat: support parsing gradleApi()-like dependency declarations. #12

Merged
merged 2 commits into from
Oct 10, 2024
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 @@ -114,8 +114,23 @@ public data class DependencyDeclaration(
FILE,
FILES,
FILE_TREE,
GRADLE_DISTRIBUTION,
MODULE,
PROJECT,
;

public fun or(identifier: Identifier): Type {
return if (identifier.path in GRADLE_DISTRIBUTIONS) {
GRADLE_DISTRIBUTION
} else {
// In this case, might just be a user-supplied function that returns a dependency declaration
this
}
}

private companion object {
/** Well-known dependencies available directly from the local Gradle distribution. */
val GRADLE_DISTRIBUTIONS = listOf("gradleApi()", "gradleTestKit()", "localGroovy()")
}
}
}
14 changes: 6 additions & 8 deletions core/src/main/kotlin/cash/grammar/kotlindsl/utils/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ import org.antlr.v4.runtime.misc.Interval
public object Context {

/**
* Returns the "full text", from [input], represented by [this][ParserRuleContext]. The full text
* includes tokens that are sent to hidden channels by the lexer. cf.
* [ParserRuleContext.text][ParserRuleContext.getText], which only considers tokens which have
* been added to the parse tree (i.e., not comments or whitespace).
* Returns the "full text", from [input], represented by [this][ParserRuleContext]. The full text includes tokens that
* are sent to hidden channels by the lexer. cf. [ParserRuleContext.text][ParserRuleContext.getText], which only
* considers tokens which have been added to the parse tree (i.e., not comments or whitespace).
*
* Returns null if `this` has a null [ParserRuleContext.start] or [ParserRuleContext.stop], which
* can happen when, e.g., `this` is a
* [ScriptContext][com.squareup.cash.grammar.KotlinParser.ScriptContext]. (I don't
* fully understand why those tokens might be null.)
* Returns null if `this` has a null [ParserRuleContext.start] or [ParserRuleContext.stop], which can happen when,
* e.g., `this` is a [ScriptContext][com.squareup.cash.grammar.KotlinParser.ScriptContext]. (I don't fully understand
* why those tokens might be null.)
*/
public fun ParserRuleContext.fullText(input: CharStream): String? {
val a = start?.startIndex ?: return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,24 @@ public class DependencyExtractor(
type = DependencyDeclaration.Type.PROJECT

// TODO(tsr): use findIdentifier everywhere?
identifier = findIdentifier(leaf)
identifier = leaf.findIdentifier()
} else if (maybeCapability == "file") {
type = DependencyDeclaration.Type.FILE

// TODO(tsr): use findIdentifier everywhere?
identifier = findIdentifier(leaf)
identifier = leaf.findIdentifier()
} else if (maybeCapability == "files") {
type = DependencyDeclaration.Type.FILES

// TODO(tsr): use findIdentifier everywhere?
identifier = findIdentifier(leaf)
identifier = leaf.findIdentifier()
} else if (maybeCapability == "fileTree") {
type = DependencyDeclaration.Type.FILE_TREE

// TODO(tsr): use findIdentifier everywhere?
identifier = findIdentifier(leaf)
identifier = leaf.findIdentifier()
} else if (maybeCapability != null) {
identifier = leaf.findIdentifier(maybeCapability)
}

// 2. Determine if `PROJECT` type.
Expand Down Expand Up @@ -183,13 +185,16 @@ public class DependencyExtractor(
}

val precedingComment = comments.getCommentsToLeft(declaration)
val fullText = declaration.fullText(input)
?: error("Could not determine 'full text' of dependency declaration. Failed to parse expression:\n ${declaration.text}")

return DependencyDeclaration(
configuration = configuration,
identifier = identifier!!,
identifier = identifier
?: error("Could not determine dependency identifier. Failed to parse expression:\n `$fullText`"),
capability = capability,
type = type,
fullText = declaration.fullText(input)!!,
type = type.or(identifier),
fullText = fullText,
precedingComment = precedingComment,
)
}
Expand Down Expand Up @@ -217,8 +222,8 @@ public class DependencyExtractor(
return statement.fullText(input)!!
}

private fun findIdentifier(ctx: PostfixUnaryExpressionContext): Identifier? {
val args = ctx.postfixUnarySuffix().single()
private fun PostfixUnaryExpressionContext.findIdentifier(): Identifier? {
val args = postfixUnarySuffix().single()
.callSuffix()
.valueArguments()
.valueArgument()
Expand Down Expand Up @@ -287,4 +292,27 @@ public class DependencyExtractor(
explicitPath = explicitPath,
)
}

/**
* Looking for something like this:
* ```
* gradleApi()
* ```
*
* which is a proper dependency, to be used like this:
* ```
* dependencies {
* api(gradleApi())
* }
* ```
*/
private fun PostfixUnaryExpressionContext.findIdentifier(name: String): Identifier? {
val suffix = postfixUnarySuffix().singleOrNull() ?: return null
val valueArguments = suffix.callSuffix()?.valueArguments() ?: return null
// empty value arguments indicates "()", i.e., no arguments to the function call.
if (valueArguments.valueArgument().isNotEmpty()) return null

// e.g. "gradleApi()"
return "$name()".asSimpleIdentifier()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cash.grammar.kotlindsl.utils

import cash.grammar.kotlindsl.model.DependencyDeclaration
import cash.grammar.kotlindsl.model.DependencyDeclaration.Capability
import cash.grammar.kotlindsl.model.DependencyDeclaration.Identifier.Companion.asSimpleIdentifier
import cash.grammar.kotlindsl.model.DependencyDeclaration.Type
import cash.grammar.kotlindsl.parse.Parser
import cash.grammar.kotlindsl.utils.test.TestErrorListener
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource

internal class DependencyExtractorTest {

@ParameterizedTest(name = "{0}")
@MethodSource("declarations")
fun `can parse dependency declarations`(testCase: TestCase) {
// Given
val buildScript = """
dependencies {
${testCase.fullText}
}
""".trimIndent()

// When
val scriptListener = Parser(
file = buildScript,
errorListener = TestErrorListener {
throw RuntimeException("Syntax error: ${it?.message}", it)
},
listenerFactory = { input, tokens, _ -> TestListener(input, tokens) }
).listener()

// Then
assertThat(scriptListener.dependencyDeclarations).containsExactly(testCase.toDependencyDeclaration())
}

private companion object {
// TODO(tsr): test for Capability.PLATFORM
@JvmStatic fun declarations(): List<TestCase> = listOf(
TestCase(
displayName = "raw string coordinates",
fullText = "api(\"g:a:v\")",
configuration = "api",
identifier = "\"g:a:v\"",
capability = Capability.DEFAULT,
type = Type.MODULE,
),
TestCase(
displayName = "version catalog accessor",
fullText = "implementation(libs.gAV)",
configuration = "implementation",
identifier = "libs.gAV",
capability = Capability.DEFAULT,
type = Type.MODULE,
),
TestCase(
displayName = "project dependency",
fullText = "textFixturesApi(project(\":has-test-fixtures\"))",
configuration = "textFixturesApi",
identifier = "\":has-test-fixtures\"",
capability = Capability.DEFAULT,
type = Type.PROJECT,
),
TestCase(
displayName = "testFixtures capability for project dependency",
fullText = "testImplementation(testFixtures(project(\":has-test-fixtures\")))",
configuration = "testImplementation",
identifier = "\":has-test-fixtures\"",
capability = Capability.TEST_FIXTURES,
type = Type.PROJECT,
),
TestCase(
displayName = "gradleApi",
fullText = "api(gradleApi())",
configuration = "api",
identifier = "gradleApi()",
capability = Capability.DEFAULT,
type = Type.GRADLE_DISTRIBUTION,
),
)
}

internal class TestCase(
val displayName: String,
val fullText: String,
val configuration: String,
val identifier: String,
val capability: Capability,
val type: Type,
val precedingComment: String? = null
) {
override fun toString(): String = displayName

fun toDependencyDeclaration() = DependencyDeclaration(
configuration = configuration,
identifier = identifier.asSimpleIdentifier()!!,
capability = capability,
type = type,
fullText = fullText,
precedingComment = precedingComment,
)
}
}