Skip to content

Commit

Permalink
feat: Add a utililty class to remove comments from blocks (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
wsutina authored Nov 26, 2024
1 parent 833ce58 commit dd3445e
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 4 deletions.
18 changes: 15 additions & 3 deletions core/src/main/kotlin/cash/grammar/kotlindsl/parse/Rewriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ public class Rewriter(
* This is a complicated process because there can be a mix of whitespace, newlines (not
* considered "whitespace" in this context), and comments, and we want to delete exactly as much
* as necessary to "delete the line" -- nothing more, nothing less.
*
* @return deleted comment tokens
*/
public fun deleteCommentsAndBlankSpaceToLeft(
before: Token,
) {
): List<Token>? {
var comments = deleteCommentsToLeft(before)

val ws = Whitespace.getBlankSpaceToLeft(commonTokens, before).onEach {
Expand All @@ -43,11 +45,15 @@ public class Rewriter(
?.onEach { ws -> delete(ws) }
?.first()?.let { deleteNewlineToLeft(it) }
}

return comments
}

/**
* Deletes all comments "to the left of" [before], returning the list of comment tokens, if they
* exist.
*
* @return deleted comment tokens
*/
public fun deleteCommentsToLeft(
before: Token,
Expand All @@ -70,20 +76,26 @@ public class Rewriter(
*
* Note that this algorithm differs from [deleteCommentsAndBlankSpaceToLeft] because comments "to
* the right of" a statement must start on the same line (no intervening newline characters).
*
* @return deleted comment tokens
*/
public fun deleteCommentsAndBlankSpaceToRight(
after: Token
) {
deleteCommentsToRight(after)
): List<Token>? {
val comments = deleteCommentsToRight(after)

Whitespace.getWhitespaceToRight(commonTokens, after)?.forEach {
delete(it)
}

return comments
}

/**
* Deletes all comments "to the right of" [after], returning the list of comment tokens, if they
* exist.
*
* @return deleted comment tokens
*/
public fun deleteCommentsToRight(
after: Token,
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/kotlin/cash/grammar/kotlindsl/utils/Comments.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cash.grammar.kotlindsl.utils

import com.squareup.cash.grammar.KotlinLexer
import com.squareup.cash.grammar.KotlinParser
import org.antlr.v4.runtime.CommonTokenStream
import org.antlr.v4.runtime.ParserRuleContext
import org.antlr.v4.runtime.Token
Expand Down Expand Up @@ -55,6 +56,22 @@ public class Comments(
}
}

public fun getCommentsInBlock(ctx: KotlinParser.NamedBlockContext): List<Token> {
val comments = mutableListOf<Token>()
var index = ctx.start.tokenIndex

while (index <= ctx.stop.tokenIndex) {
val token = tokens.get(index)

if (token.isComment()) {
comments.add(token)
}
++index
}

return comments
}

private fun Token.isWhitespace(): Boolean {
return text.isBlank()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package cash.grammar.kotlindsl.utils

import cash.grammar.kotlindsl.parse.KotlinParseException
import cash.grammar.kotlindsl.parse.Parser
import cash.grammar.kotlindsl.parse.Rewriter
import cash.grammar.kotlindsl.utils.Context.leafRule
import cash.grammar.kotlindsl.utils.Whitespace.trimGently
import cash.grammar.utils.ifNotEmpty
import com.squareup.cash.grammar.KotlinParser
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

/**
* Removes comments from a specified block in a build script.
*
* Example:
* ```
* dependencies {
* /* This is a block comment
* that spans multiple lines */
* implementation("org.jetbrains.kotlin:kotlin-stdlib") // This is an inline comment
* // This is a single-line comment
* testImplementation("org.junit.jupiter:junit-jupiter")
* }
* ```
*
* The above script would be rewritten to:
* ```
* dependencies {
* implementation("org.jetbrains.kotlin:kotlin-stdlib")
* testImplementation("org.junit.jupiter:junit-jupiter")
* }
* ```
*/
public class CommentsInBlockRemover private constructor(
private val input: CharStream,
private val tokens: CommonTokenStream,
private val errorListener: CollectingErrorListener,
private val blockName: String,
) : KotlinParserBaseListener() {
private var terminalNewlines = 0
private val rewriter = Rewriter(tokens)
private val indent = Whitespace.computeIndent(tokens, input)
private val comments = Comments(tokens, indent)

@Throws(KotlinParseException::class)
public fun rewritten(): String {
errorListener.getErrorMessages().ifNotEmpty {
throw KotlinParseException.withErrors(it)
}

return rewriter.text.trimGently(terminalNewlines)
}

override fun exitNamedBlock(ctx: KotlinParser.NamedBlockContext) {
if (ctx.name().text == blockName) {
// Delete inline comments (a comment after a statement)
val allInlineComments = mutableListOf<Token>()
ctx.statements().statement().forEach {
val leafRule = it.leafRule()
val inlineComments = rewriter.deleteCommentsAndBlankSpaceToRight(leafRule.stop).orEmpty()
allInlineComments += inlineComments
}

val nonInlineComments = comments.getCommentsInBlock(ctx).subtract(allInlineComments)
nonInlineComments.forEach { token ->
rewriter.deleteWhitespaceToLeft(token)
rewriter.deleteNewlineToRight(token)
rewriter.delete(token)
}
}
}

public companion object {
public fun of(
buildScript: Path,
blockName: String,
): CommentsInBlockRemover {
return of(Parser.readOnlyInputStream(buildScript), blockName)
}

public fun of(
buildScript: String,
blockName: String,
): CommentsInBlockRemover {
return of(buildScript.byteInputStream(), blockName)
}

private fun of(
buildScript: InputStream,
blockName: String,
): CommentsInBlockRemover {
val errorListener = CollectingErrorListener()

return Parser(
file = buildScript,
errorListener = errorListener,
listenerFactory = { input, tokens, _ ->
CommentsInBlockRemover(
input = input,
tokens = tokens,
errorListener = errorListener,
blockName = blockName,
)
},
).listener()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cash.grammar.kotlindsl.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class CommentsInBlockRemoverTest {
@Test
fun `remove all comments in the given block`() {
// Given
val buildScript = """
|dependencies {
| /* This is a block comment
| that spans multiple lines */
| implementation("org.jetbrains.kotlin:kotlin-stdlib") // This is an line comment
| // This is a single-line comment
| testImplementation("org.junit.jupiter:junit-jupiter")
| // This is another single-line comment
|}
|
|// This is project bar
|project.name = bar
|
|otherBlock {
| // More comments
|}
""".trimMargin()

// When
val rewrittenBuildScript = CommentsInBlockRemover.of(buildScript, "dependencies").rewritten()

// Then
assertThat(rewrittenBuildScript).isEqualTo("""
|dependencies {
| implementation("org.jetbrains.kotlin:kotlin-stdlib")
| testImplementation("org.junit.jupiter:junit-jupiter")
|}
|
|// This is project bar
|project.name = bar
|
|otherBlock {
| // More comments
|}
""".trimMargin())
}
}
33 changes: 33 additions & 0 deletions core/src/test/kotlin/cash/grammar/kotlindsl/utils/CommentsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,37 @@ internal class CommentsTest {
),
)
}

@Test fun `can find all comments in blocks`() {
val buildScript =
"""
dependencies {
// This is a
// comment
implementation(libs.lib) // Inline comments
/*
* Here's a multiline comment.
*/
implementation(deps.bar)
}
emptyBlock { }
""".trimIndent()

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

assertThat(scriptListener.commentTokens["dependencies"]?.map { it.text }).containsExactly(
"// This is a",
"// comment",
"// Inline comments",
"/*\n * Here's a multiline comment.\n */"
)
assertThat(scriptListener.commentTokens["emptyBlock"]?.map { it.text }).isEmpty()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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 @@ -24,10 +23,12 @@ internal class TestListener(
val trailingKotlinFileNewlines = Whitespace.countTerminalNewlines(tokens)
val indent = Whitespace.computeIndent(tokens, input, defaultIndent)
val dependencyExtractor = DependencyExtractor(input, tokens, indent)
val comments = Comments(tokens, indent)

val dependencyDeclarations = mutableListOf<DependencyDeclaration>()
val dependencyDeclarationsStatements = mutableListOf<String>()
val statements = mutableListOf<String>()
val commentTokens = mutableMapOf<String, List<Token>>()

override fun exitNamedBlock(ctx: KotlinParser.NamedBlockContext) {
if (ctx.isSubprojects) {
Expand All @@ -43,5 +44,7 @@ internal class TestListener(
dependencyDeclarationsStatements += dependencyContainer.getDependencyDeclarationsWithContext().map { it.statement.fullText(input)!!.trim() }
statements += dependencyContainer.getStatements().map { it.fullText(input)!!.trim() }
}

commentTokens[ctx.name().text] = comments.getCommentsInBlock(ctx)
}
}

0 comments on commit dd3445e

Please sign in to comment.