Skip to content

Commit

Permalink
Expose Dependency Declarations with ANTLR parsing context for manipul…
Browse files Browse the repository at this point in the history
…ation (#17)

- Introduce DependencyElement module and used as elements type in DependencyContainer for clarity and type safety
- Add a function to expose declaration with context
  • Loading branch information
wsutina authored Nov 8, 2024
1 parent 1449a20 commit 7c8a4d9
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cash.grammar.kotlindsl.model

import com.squareup.cash.grammar.KotlinParser.StatementContext

/**
* Base class representing an element within the `dependencies` block.
*
* @property statement The context of the statement in the dependencies block.
*/
public sealed class DependencyElement(public open val statement: StatementContext)

/**
* Represents a dependency declaration within the `dependencies` block.
*
* @property declaration The parsed dependency declaration.
* @property statement The context of the statement in the dependencies block.
*/
public data class DependencyDeclarationElement(
val declaration: DependencyDeclaration,
override val statement: StatementContext
) : DependencyElement(statement)

/**
* Represents a statement within the `dependencies` block that is **not** a dependency declaration.
*
* @property statement The context of the statement within the dependencies block.
*/
public data class NonDependencyDeclarationElement(override val statement: StatementContext) : DependencyElement(statement)
Original file line number Diff line number Diff line change
@@ -1,46 +1,33 @@
package cash.grammar.kotlindsl.model.gradle

import cash.grammar.kotlindsl.model.DependencyDeclaration
import cash.grammar.kotlindsl.model.*
import com.squareup.cash.grammar.KotlinParser.StatementContext

/**
* A container for all the [Statements][com.squareup.cash.grammar.KotlinParser.StatementsContext] in
* a `dependencies` block in a Gradle build script. These statements are an ordered (not sorted!)
* list of "raw" statements and modeled
* [DependencyDeclarations][cash.grammar.kotlindsl.model.DependencyDeclaration].
* list of statements, each classified as a [DependencyElement]
*
* Rather than attempt to model everything that might possibly be found inside a build script, we
* declare defeat on anything that isn't a standard dependency declaration and simply retain it
* as-is.
* Each statement in this container is classified as a [DependencyElement], which can represent either:
* - A parsed [DependencyDeclaration][cash.grammar.kotlindsl.model.DependencyDeclaration] element, or
* - A non-dependency declaration statement, retained as-is.
*/
public class DependencyContainer(
/** The raw, ordered, list of statements for more complex use-cases. */
public val elements: List<Any>,
/** The ordered list of [DependencyElement] instances, representing each classified statement within the `dependencies` block. */
public val elements: List<DependencyElement>,
) {

public fun getDependencyDeclarations(): List<DependencyDeclaration> {
return elements.filterIsInstance<DependencyDeclaration>()
}

@Deprecated(
message = "use getExpressions",
replaceWith = ReplaceWith("getExpressions()")
)
public fun getNonDeclarations(): List<String> {
return getExpressions()
public fun getDependencyDeclarationsWithContext(): List<DependencyDeclarationElement> {
return elements.filterIsInstance<DependencyDeclarationElement>()
}

/**
* A common example of an expression in a dependencies block is
* ```
* add("extraImplementation", "com.foo:bar:1.0")
* ```
*/
public fun getExpressions(): List<String> {
return elements.filterIsInstance<String>()
public fun getDependencyDeclarations(): List<DependencyDeclaration> {
return getDependencyDeclarationsWithContext().map { it.declaration }
}

/**
* Get non-dependency declaration statements.
*
* Might include an [if-expression][com.squareup.cash.grammar.KotlinParser.IfExpressionContext] like
* ```
* if (functionReturningABoolean()) { ... }
Expand All @@ -49,9 +36,13 @@ public class DependencyContainer(
* ```
* val string = "a:complex:$value"
* ```
* or a common example of an expression in a dependencies block like
* ```
* add("extraImplementation", "com.foo:bar:1.0")
* ```
*/
public fun getStatements(): List<StatementContext> {
return elements.filterIsInstance<StatementContext>()
return elements.filterIsInstance<NonDependencyDeclarationElement>().map { it.statement }
}

internal companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package cash.grammar.kotlindsl.utils

import cash.grammar.kotlindsl.model.DependencyDeclaration
import cash.grammar.kotlindsl.model.*
import cash.grammar.kotlindsl.model.DependencyDeclaration.Capability
import cash.grammar.kotlindsl.model.DependencyDeclaration.Identifier
import cash.grammar.kotlindsl.model.DependencyDeclaration.Identifier.Companion.asSimpleIdentifier
Expand Down Expand Up @@ -47,11 +47,10 @@ public class DependencyExtractor(
return statements
.map { stmt ->
val leaf = stmt.leafRule()
if (leaf is PostfixUnaryExpressionContext) {
parseDependencyDeclaration(leaf)
if (leaf is PostfixUnaryExpressionContext && leaf.isDependencyDeclaration()) {
DependencyDeclarationElement(parseDependencyDeclaration(leaf), stmt)
} else {
// If it's not a known type, we just grab the raw KotlinParser.StatementContext
stmt
NonDependencyDeclarationElement(stmt)
}
}
.asContainer()
Expand All @@ -75,13 +74,14 @@ public class DependencyExtractor(
val statements = ctx.statements().statement()
if (statements == null || statements.isEmpty()) return DependencyContainer.EMPTY

return statements
.mapNotNull { it.leafRule() as? PostfixUnaryExpressionContext }
.map { parseDependencyDeclaration(it) }
.asContainer()
return statements.mapNotNull { statement ->
val leaf = statement.leafRule() as? PostfixUnaryExpressionContext ?: return@mapNotNull null
if (!leaf.isDependencyDeclaration()) return@mapNotNull null
DependencyDeclarationElement(parseDependencyDeclaration(leaf), statement)
}.asContainer()
}

private fun List<Any>.asContainer() = DependencyContainer(this)
private fun List<DependencyElement>.asContainer() = DependencyContainer(this)

private fun isInBuildscriptDependenciesBlock(
blockStack: ArrayDeque<NamedBlockContext>,
Expand All @@ -91,18 +91,11 @@ public class DependencyExtractor(
&& blockStack[1].isBuildscript
}

private fun parseDependencyDeclaration(
declaration: PostfixUnaryExpressionContext,
): Any {
private fun parseDependencyDeclaration(declaration: PostfixUnaryExpressionContext): DependencyDeclaration {
// This is everything after the configuration, including optionally a trailing lambda
val rawDependency = declaration.postfixUnarySuffix().single().callSuffix()
val args = rawDependency.valueArguments().valueArgument()

// Not a normal declaration, but a function call
if (args.size > 1) {
return parseFunctionCall(declaration)
}

// e.g., `classpath`, `implementation`, etc.
val configuration = declaration.primaryExpression().text
var identifier: Identifier?
Expand Down Expand Up @@ -221,15 +214,13 @@ public class DependencyExtractor(
return literalText(ctx)?.let { "\"$it\"" }
}

/**
* E.g.,
* ```
* add("extraImplementation", "com.foo:bar:1.0")
* ```
*/
private fun parseFunctionCall(statement: PostfixUnaryExpressionContext): Any {
// TODO(tsr): we should consider modeling function calls separately, since it's a well-known use-case.
return statement.fullText(input)!!
private fun PostfixUnaryExpressionContext.isDependencyDeclaration(): Boolean {
// This is everything after the configuration, including optionally a trailing lambda
val rawDependency = this.postfixUnarySuffix().single().callSuffix()
val args = rawDependency.valueArguments().valueArgument()

// If there are more than one argument, it's a function call, not a dependency declaration
return args.size <= 1
}

private fun PostfixUnaryExpressionContext.findIdentifier(): Identifier? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal class DependencyExtractorTest {

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

@Test fun `a complex script can be fully parsed`() {
Expand Down Expand Up @@ -61,8 +62,9 @@ internal class DependencyExtractorTest {
fullText = "api(libs.magic)",
)
)
assertThat(scriptListener.expressions).containsExactly("add(\"extraImplementation\", libs.fortyTwo)")
assertThat(scriptListener.dependencyDeclarationsStatements).containsExactly("api(libs.magic)")
assertThat(scriptListener.statements).containsExactly(
"add(\"extraImplementation\", libs.fortyTwo)",
"val complex = \"a:complex:${'$'}expression\"",
// The whitespace below is a bit wonky, but it's an artifact of the test fixture, not the API.
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cash.grammar.kotlindsl.utils

import cash.grammar.kotlindsl.model.DependencyDeclaration
import cash.grammar.kotlindsl.model.DependencyDeclarationElement
import cash.grammar.kotlindsl.utils.Blocks.isBuildscript
import cash.grammar.kotlindsl.utils.Blocks.isDependencies
import cash.grammar.kotlindsl.utils.Blocks.isSubprojects
Expand All @@ -25,7 +26,7 @@ internal class TestListener(
val dependencyExtractor = DependencyExtractor(input, tokens, indent)

val dependencyDeclarations = mutableListOf<DependencyDeclaration>()
val expressions = mutableListOf<String>()
val dependencyDeclarationsStatements = mutableListOf<String>()
val statements = mutableListOf<String>()

override fun exitNamedBlock(ctx: KotlinParser.NamedBlockContext) {
Expand All @@ -39,7 +40,7 @@ internal class TestListener(
val dependencyContainer = dependencyExtractor.collectDependencies(ctx)

dependencyDeclarations += dependencyContainer.getDependencyDeclarations()
expressions += dependencyContainer.getExpressions()
dependencyDeclarationsStatements += dependencyContainer.getDependencyDeclarationsWithContext().map { it.statement.fullText(input)!!.trim() }
statements += dependencyContainer.getStatements().map { it.fullText(input)!!.trim() }
}
}
Expand Down

0 comments on commit 7c8a4d9

Please sign in to comment.