diff --git a/core/src/main/kotlin/cash/grammar/kotlindsl/model/DependencyElement.kt b/core/src/main/kotlin/cash/grammar/kotlindsl/model/DependencyElement.kt new file mode 100644 index 0000000..4c51b7c --- /dev/null +++ b/core/src/main/kotlin/cash/grammar/kotlindsl/model/DependencyElement.kt @@ -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) diff --git a/core/src/main/kotlin/cash/grammar/kotlindsl/model/gradle/DependencyContainer.kt b/core/src/main/kotlin/cash/grammar/kotlindsl/model/gradle/DependencyContainer.kt index 5f8c8bf..a6b9967 100644 --- a/core/src/main/kotlin/cash/grammar/kotlindsl/model/gradle/DependencyContainer.kt +++ b/core/src/main/kotlin/cash/grammar/kotlindsl/model/gradle/DependencyContainer.kt @@ -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, + /** The ordered list of [DependencyElement] instances, representing each classified statement within the `dependencies` block. */ + public val elements: List, ) { - public fun getDependencyDeclarations(): List { - return elements.filterIsInstance() - } - - @Deprecated( - message = "use getExpressions", - replaceWith = ReplaceWith("getExpressions()") - ) - public fun getNonDeclarations(): List { - return getExpressions() + public fun getDependencyDeclarationsWithContext(): List { + return elements.filterIsInstance() } - /** - * A common example of an expression in a dependencies block is - * ``` - * add("extraImplementation", "com.foo:bar:1.0") - * ``` - */ - public fun getExpressions(): List { - return elements.filterIsInstance() + public fun getDependencyDeclarations(): List { + return getDependencyDeclarationsWithContext().map { it.declaration } } /** + * Get non-dependency declaration statements. + * * Might include an [if-expression][com.squareup.cash.grammar.KotlinParser.IfExpressionContext] like * ``` * if (functionReturningABoolean()) { ... } @@ -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 { - return elements.filterIsInstance() + return elements.filterIsInstance().map { it.statement } } internal companion object { diff --git a/core/src/main/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractor.kt b/core/src/main/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractor.kt index 83b1d06..8c12219 100644 --- a/core/src/main/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractor.kt +++ b/core/src/main/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractor.kt @@ -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 @@ -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() @@ -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.asContainer() = DependencyContainer(this) + private fun List.asContainer() = DependencyContainer(this) private fun isInBuildscriptDependenciesBlock( blockStack: ArrayDeque, @@ -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? @@ -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? { diff --git a/core/src/test/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractorTest.kt b/core/src/test/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractorTest.kt index 499f304..a515606 100644 --- a/core/src/test/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractorTest.kt +++ b/core/src/test/kotlin/cash/grammar/kotlindsl/utils/DependencyExtractorTest.kt @@ -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`() { @@ -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. """ diff --git a/core/src/test/kotlin/cash/grammar/kotlindsl/utils/TestListener.kt b/core/src/test/kotlin/cash/grammar/kotlindsl/utils/TestListener.kt index 630d66a..a067fdf 100644 --- a/core/src/test/kotlin/cash/grammar/kotlindsl/utils/TestListener.kt +++ b/core/src/test/kotlin/cash/grammar/kotlindsl/utils/TestListener.kt @@ -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 @@ -25,7 +26,7 @@ internal class TestListener( val dependencyExtractor = DependencyExtractor(input, tokens, indent) val dependencyDeclarations = mutableListOf() - val expressions = mutableListOf() + val dependencyDeclarationsStatements = mutableListOf() val statements = mutableListOf() override fun exitNamedBlock(ctx: KotlinParser.NamedBlockContext) { @@ -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() } } }