Skip to content

Add Gradle recipe RemoveEmptyBuildscriptBlock#8329

Open
sullis wants to merge 5 commits into
openrewrite:mainfrom
sullis:ss-remove-buildscript
Open

Add Gradle recipe RemoveEmptyBuildscriptBlock#8329
sullis wants to merge 5 commits into
openrewrite:mainfrom
sullis:ss-remove-buildscript

Conversation

@sullis

@sullis sullis commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Adds a new recipe org.openrewrite.gradle.RemoveEmptyBuildscriptBlock that removes buildscript blocks which contribute nothing to the build, and wires it into org.openrewrite.gradle.GradleBestPractices.

Behavior

  • Applies to build.gradle(.kts) and settings.gradle(.kts) (Groovy and Kotlin DSL).
  • Removes a buildscript block whose closure body is empty, or contains only other empty blocks (e.g. an empty dependencies or repositories block).
  • Leaves the block alone when it contains a comment anywhere inside, so no comment is silently deleted.
  • Removal is done at the statement-list level rather than by returning null from the method invocation visit, so the following statement inherits the removed block's prefix — otherwise removing the first statement in a file leaves its leading blank lines behind.

Tests

RemoveEmptyBuildscriptBlockTest covers the empty block, a block containing only empty nested blocks, the Kotlin DSL, a block between other statements, and the negative cases (real dependencies, a comment in the block, a comment in a nested block). GradleBestPracticesTest gains a case verifying the recipe runs as part of the composite recipe.

@sullis

sullis commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

LGTM

…sing `ListUtils.map`

Both `G.CompilationUnit` and `K.CompilationUnit` dispatch through `adapt()`, so a
single `JavaIsoVisitor` traverses Groovy and Kotlin scripts and the two visitors
collapse into one. The top-level statement list still needs a per-language cast,
since `JavaSourceFile` exposes `getClasses()` rather than `getStatements()`.

`ListUtils.filter` cannot express the removal, because the removed block's prefix
has to transfer to the statement taking its place. `ListUtils.map` can, following
the `AtomicReference<Space>` idiom in `ChangeMethodAccessLevelVisitor`. The first
removed prefix wins, so consecutive empty blocks do not leave the successor with a
separator prefix at the start of the file; a new test covers that.
…ldscript` blocks

A Kotlin script's `K.CompilationUnit` always holds its body in a single block, for
`build.gradle.kts` and `settings.gradle.kts` alike, so that branch could never see a
`buildscript` invocation and never fired. Kotlin's top level is reached through
`visitBlock` instead; only Groovy keeps its statements on the compilation unit.

Both traversals are load-bearing, which no test previously showed: without the
recursion in `visitBlock` an empty `buildscript` nested in another block survives in
Kotlin, and without visiting a Groovy compilation unit's children it survives in
Groovy. Add a test per language so neither can regress unnoticed.
Groovy gives the last statement of a closure an implicit `return`, so a `buildscript`
block written there is a `J.Return` rather than a `J.MethodInvocation` and the name
check never matched it. The emptiness check already unwrapped that, making the two
disagree: the same block counted as empty when nested inside another block, yet was
never removed when it sat last. Kotlin has no such wrapper, so it removed the block
either way.

Share the unwrapping between both checks so the two agree, and take the prefix from
the outer statement so the `return` keeps supplying the whitespace it owns.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants