Skip to content

Commit f333497

Browse files
vlsiclaude
andcommitted
build: add per-recipe disable switch and a rewriteDiagnose task
Two follow-ups to the in-house OpenRewrite runner. disabledRecipes lets a single recipe be switched off by name, e.g. openrewrite { disabledRecipes.add("org.openrewrite.java.testing.junit5.AssertThrowsOnLastStatement") } The compiled composites (JUnit5BestPractices and friends) expose an immutable recipeList, so the runner wraps the recipe tree in a FilteringRecipe that hides the disabled names instead of mutating the list. This removes the need to fork a recipe module or drop a whole composite; the JUnit 5 recipes are now enabled for every test module and only AssertThrowsOnLastStatement is switched off, pending openrewrite/rewrite-testing-frameworks#1048 rewriteDiagnose runs each active leaf recipe on its own and reports whether it fails, would change files, or does nothing, so the problematic recipes can be found without reading a full run. On jorphan: 183 leaf recipes, 0 fail in Java mode, 21 would make changes. Note that a recipe which only fails as part of an ordered composite (AssertThrowsOnLastStatement on Kotlin) passes in isolation, so the per-leaf report is a lower bound; the composite run surfaces the rest as non-fatal "Error while rewriting" warnings thanks to the lenient ExecutionContext. -PopenrewriteKotlin=true flips Kotlin processing on from the command line for experimenting with rewrite-kotlin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7bb2e7b commit f333497

5 files changed

Lines changed: 166 additions & 27 deletions

File tree

build-logic/jvm/src/main/kotlin/build-logic.openrewrite.gradle.kts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ openrewrite {
4444
activeRecipes.add("org.apache.jmeter.staticanalysis.CodeCleanup")
4545
activeRecipes.add("org.apache.jmeter.staticanalysis.CommonStaticAnalysis")
4646

47-
// The JUnit 5 recipes are Java-only: their JavaTemplate rewrites throw on Kotlin test
48-
// sources, so enable them only for modules whose tests are pure Java.
49-
if (!file("src/test/kotlin").isDirectory) {
50-
plugins.withId("build-logic.test-junit5") {
51-
activeRecipes.add("org.openrewrite.java.testing.junit5.JUnit5BestPractices")
52-
activeRecipes.add("org.openrewrite.java.testing.junit5.CleanupAssertions")
53-
}
47+
plugins.withId("build-logic.test-junit5") {
48+
activeRecipes.add("org.openrewrite.java.testing.junit5.JUnit5BestPractices")
49+
activeRecipes.add("org.openrewrite.java.testing.junit5.CleanupAssertions")
5450
}
51+
52+
// Recipes that crash or corrupt output are disabled by name here instead of dropping the
53+
// whole composite. AssertThrowsOnLastStatement throws on Kotlin sources; see the fix in
54+
// https://github.com/openrewrite/rewrite-testing-frameworks/pull/1048
55+
disabledRecipes.add("org.openrewrite.java.testing.junit5.AssertThrowsOnLastStatement")
5556
}

build-logic/openrewrite/src/main/kotlin/build-logic.openrewrite-base.gradle.kts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import org.apache.jmeter.buildtools.openrewrite.OpenRewriteBaseTask
19+
import org.apache.jmeter.buildtools.openrewrite.OpenRewriteDiagnoseTask
1920
import org.apache.jmeter.buildtools.openrewrite.OpenRewriteDryRunTask
2021
import org.apache.jmeter.buildtools.openrewrite.OpenRewriteExtension
2122
import org.apache.jmeter.buildtools.openrewrite.OpenRewriteRunTask
@@ -54,12 +55,18 @@ val openrewriteExtension = extensions.create<OpenRewriteExtension>(OpenRewriteEx
5455
fun configureCommon(task: OpenRewriteBaseTask) {
5556
task.group = "openrewrite"
5657
task.activeRecipes.set(openrewriteExtension.activeRecipes)
58+
task.disabledRecipes.set(openrewriteExtension.disabledRecipes)
5759
task.activeStyles.set(openrewriteExtension.activeStyles)
5860
task.rewriteClasspath.from(openrewriteClasspath)
5961
task.configFile.set(openrewriteExtension.configFile)
6062
task.projectRootDir.set(layout.projectDirectory)
6163
task.logCompilationWarningsAndErrors.convention(false)
62-
task.processKotlin.set(openrewriteExtension.processKotlin)
64+
// Allow flipping Kotlin processing from the command line, e.g. -PopenrewriteKotlin=true
65+
task.processKotlin.set(
66+
providers.gradleProperty("openrewriteKotlin")
67+
.map { it.toBoolean() }
68+
.orElse(openrewriteExtension.processKotlin)
69+
)
6370
}
6471

6572
tasks.register<OpenRewriteRunTask>("rewriteRun") {
@@ -74,6 +81,12 @@ tasks.register<OpenRewriteDryRunTask>("rewriteDryRun") {
7481
failOnDryRunResults.set(openrewriteExtension.failOnDryRunResults)
7582
}
7683

84+
tasks.register<OpenRewriteDiagnoseTask>("rewriteDiagnose") {
85+
description = "Runs each active leaf recipe on its own and reports failures and no-ops"
86+
configureCommon(this)
87+
reportDir.set(layout.buildDirectory.dir("reports/openrewrite"))
88+
}
89+
7790
plugins.withId("java") {
7891
the<JavaPluginExtension>().sourceSets.all {
7992
val sourceSet: SourceSet = this

build-logic/openrewrite/src/main/kotlin/org/apache/jmeter/buildtools/openrewrite/OpenRewriteExtension.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ open class OpenRewriteExtension @Inject constructor(objects: ObjectFactory) {
3333

3434
val activeRecipes = objects.setProperty<String>()
3535

36+
/** Fully qualified recipe names to prune from the active recipe tree, e.g. ones that crash. */
37+
val disabledRecipes = objects.setProperty<String>()
38+
3639
val failOnDryRunResults = objects.property<Boolean>().convention(true)
3740

3841
/**

build-logic/openrewrite/src/main/kotlin/org/apache/jmeter/buildtools/openrewrite/OpenRewriteTasks.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ abstract class OpenRewriteBaseTask @Inject constructor(
4949
@get:Input
5050
abstract val activeRecipes: SetProperty<String>
5151

52+
@get:Input
53+
abstract val disabledRecipes: SetProperty<String>
54+
5255
@get:Input
5356
abstract val activeStyles: SetProperty<String>
5457

@@ -72,18 +75,20 @@ abstract class OpenRewriteBaseTask @Inject constructor(
7275
val sourceSets: NamedDomainObjectContainer<SourceSetConfig> =
7376
objects.domainObjectContainer(SourceSetConfig::class.java)
7477

75-
protected fun runRewrite(applyChanges: Boolean, patch: Provider<RegularFile>?) {
78+
protected fun runRewrite(applyChanges: Boolean, patch: Provider<RegularFile>?, diagnose: Boolean = false) {
7679
val snapshots = sourceSets.map { it.toSnapshot() }
7780
val rewriteCp = rewriteClasspath.files
7881
val queue = executor.processIsolation {
7982
classpath.from(rewriteClasspath)
8083
}
8184
queue.submit(OpenRewriteWork::class) {
8285
this.applyChanges.set(applyChanges)
86+
this.diagnose.set(diagnose)
8387
this.projectRoot.set(projectRootDir)
8488
patch?.let { this.patchFile.set(it) }
8589
this.configFile.set(this@OpenRewriteBaseTask.configFile)
8690
this.activeRecipes.set(this@OpenRewriteBaseTask.activeRecipes)
91+
this.disabledRecipes.set(this@OpenRewriteBaseTask.disabledRecipes)
8792
this.activeStyles.set(this@OpenRewriteBaseTask.activeStyles)
8893
this.sourceSets.set(snapshots)
8994
this.rewriteClasspath.set(rewriteCp)
@@ -135,3 +140,18 @@ abstract class OpenRewriteDryRunTask @Inject constructor(
135140
const val PATCH_NAME = "rewrite.patch"
136141
}
137142
}
143+
144+
/** Runs each active leaf recipe on its own and reports which fail, change, or do nothing. */
145+
abstract class OpenRewriteDiagnoseTask @Inject constructor(
146+
objects: ObjectFactory,
147+
executor: WorkerExecutor,
148+
) : OpenRewriteBaseTask(objects, executor) {
149+
@get:OutputDirectory
150+
abstract val reportDir: DirectoryProperty
151+
152+
@TaskAction
153+
fun run() {
154+
runRewrite(applyChanges = false, patch = reportDir.file("diagnose.txt"), diagnose = true)
155+
logger.lifecycle("OpenRewrite diagnose report: {}", reportDir.file("diagnose.txt").get().asFile)
156+
}
157+
}

build-logic/openrewrite/src/main/kotlin/org/apache/jmeter/buildtools/openrewrite/OpenRewriteWork.kt

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,15 @@ interface OpenRewriteParameters : WorkParameters {
110110
/** Unified diff of the proposed changes, written in dry-run mode. */
111111
val patchFile: RegularFileProperty
112112

113+
/** When true, run each leaf recipe on its own and report which ones fail or make changes. */
114+
val diagnose: Property<Boolean>
115+
113116
val configFile: RegularFileProperty
114117
val activeRecipes: SetProperty<String>
118+
119+
/** Fully qualified recipe names to prune from the active recipe tree before running. */
120+
val disabledRecipes: SetProperty<String>
121+
115122
val activeStyles: SetProperty<String>
116123
val sourceSets: SetProperty<SourceSetSnapshot>
117124
val rewriteClasspath: SetProperty<File>
@@ -125,26 +132,18 @@ abstract class OpenRewriteWork : WorkAction<OpenRewriteParameters> {
125132
}
126133

127134
override fun execute() {
128-
val ctx: ExecutionContext = InMemoryExecutionContext { t -> logger.warn("Error while rewriting", t) }
129-
val results = listResults(ctx)
130-
if (parameters.applyChanges.get()) {
131-
applyResults(results)
132-
} else {
133-
writePatch(results)
134-
}
135-
}
136-
137-
private fun listResults(ctx: ExecutionContext): List<Result> {
138135
val env = createEnvironment()
139-
val recipe = env.activateRecipes(parameters.activeRecipes.get())
140-
if (recipe.recipeList.isEmpty()) {
141-
logger.warn(
142-
"No recipes were activated. Add a recipe with openrewrite.activeRecipes in the build file."
143-
)
144-
return emptyList()
136+
val activated = env.activateRecipes(parameters.activeRecipes.get())
137+
val disabled = parameters.disabledRecipes.get()
138+
val diagnose = parameters.diagnose.get()
139+
140+
if (activated.recipeList.isEmpty()) {
141+
logger.warn("No recipes were activated. Add a recipe with openrewrite.activeRecipes in the build file.")
142+
return
145143
}
146144
val styles = env.activateStyles(parameters.activeStyles.get())
147145

146+
val ctx: ExecutionContext = InMemoryExecutionContext { t -> logger.warn("Error while rewriting", t) }
148147
val sourceFiles = parse(ctx)
149148
.map { source ->
150149
if (styles.isEmpty()) {
@@ -155,10 +154,88 @@ abstract class OpenRewriteWork : WorkAction<OpenRewriteParameters> {
155154
}
156155
.toList()
157156

158-
val recipeRun = recipe.run(InMemoryLargeSourceSet(sourceFiles), ctx)
159-
return recipeRun.changeset.allResults
157+
// In diagnose we test everything (disabled recipes are still reported, annotated) so the
158+
// report is a complete picture. In normal runs we prune the disabled recipes from the tree.
159+
if (diagnose) {
160+
diagnose(activated, sourceFiles, disabled)
161+
return
162+
}
163+
164+
val recipe = if (disabled.isEmpty()) {
165+
activated
166+
} else {
167+
val present = collectNames(activated).intersect(disabled)
168+
logger.lifecycle("OpenRewrite disabled {} recipe(s): {}", present.size, present.sorted())
169+
FilteringRecipe(activated, disabled)
170+
}
171+
172+
val results = recipe.run(InMemoryLargeSourceSet(sourceFiles), ctx).changeset.allResults
173+
if (parameters.applyChanges.get()) {
174+
applyResults(results)
175+
} else {
176+
writePatch(results)
177+
}
178+
}
179+
180+
private fun collectNames(recipe: org.openrewrite.Recipe): Set<String> {
181+
val names = mutableSetOf<String>()
182+
fun visit(r: org.openrewrite.Recipe) {
183+
names.add(r.name)
184+
r.recipeList.forEach { visit(it) }
185+
}
186+
visit(recipe)
187+
return names
188+
}
189+
190+
/** Runs each leaf recipe individually so a single failure does not hide the others. */
191+
private fun diagnose(recipe: org.openrewrite.Recipe, sourceFiles: List<SourceFile>, disabled: Set<String>) {
192+
fun leaves(r: org.openrewrite.Recipe): List<org.openrewrite.Recipe> =
193+
if (r.recipeList.isEmpty()) listOf(r) else r.recipeList.flatMap { leaves(it) }
194+
195+
val leaves = leaves(recipe).distinctBy { it.name }
196+
val report = StringBuilder()
197+
var failed = 0
198+
var changed = 0
199+
var clean = 0
200+
for (leaf in leaves) {
201+
val tag = if (leaf.name in disabled) " [disabled]" else ""
202+
val errors = mutableListOf<Throwable>()
203+
val ctx: ExecutionContext = InMemoryExecutionContext { errors.add(it) }
204+
val line = try {
205+
val results = leaf.run(InMemoryLargeSourceSet(sourceFiles), ctx).changeset.allResults
206+
when {
207+
errors.isNotEmpty() -> {
208+
failed++
209+
"FAILED ${leaf.name}$tag -> ${errors.first().firstMessageLine()}"
210+
}
211+
results.isNotEmpty() -> {
212+
changed++
213+
"changes:${results.size.toString().padStart(3)} ${leaf.name}$tag"
214+
}
215+
else -> {
216+
clean++
217+
"ok ${leaf.name}$tag"
218+
}
219+
}
220+
} catch (t: Throwable) {
221+
failed++
222+
"FAILED ${leaf.name}$tag -> ${t.firstMessageLine()}"
223+
}
224+
report.appendLine(line)
225+
logger.lifecycle(line)
226+
}
227+
val summary = "OpenRewrite diagnose: ${leaves.size} recipes -> " +
228+
"$failed failed, $changed would change, $clean no-op"
229+
report.appendLine().appendLine(summary)
230+
logger.lifecycle(summary)
231+
val out = parameters.patchFile.get().asFile
232+
out.parentFile.mkdirs()
233+
out.writeText(report.toString())
160234
}
161235

236+
private fun Throwable.firstMessageLine(): String =
237+
(message ?: this::class.java.simpleName).lineSequence().first().take(200)
238+
162239
private fun applyResults(results: List<Result>) {
163240
val root = parameters.projectRoot.get().asFile.toPath()
164241
var changes = 0
@@ -326,3 +403,28 @@ abstract class OpenRewriteWork : WorkAction<OpenRewriteParameters> {
326403
return res
327404
}
328405
}
406+
407+
/**
408+
* Wraps a recipe tree and hides recipes whose name is in [disabled], so a crashing or unwanted
409+
* recipe can be switched off by name without editing the third-party composite that declares it.
410+
* The compiled composites expose an immutable recipeList, so filtering by wrapping is the reliable
411+
* way to remove a recipe from the run.
412+
*/
413+
private class FilteringRecipe(
414+
private val delegate: org.openrewrite.Recipe,
415+
private val disabled: Set<String>,
416+
) : org.openrewrite.Recipe() {
417+
private val filtered: List<org.openrewrite.Recipe> by lazy {
418+
delegate.recipeList
419+
.filter { it.name !in disabled }
420+
.map { FilteringRecipe(it, disabled) }
421+
}
422+
423+
override fun getName(): String = delegate.name
424+
override fun getDisplayName(): String = delegate.displayName
425+
override fun getDescription(): String =
426+
delegate.description?.takeIf { it.isNotBlank() } ?: delegate.displayName
427+
override fun getVisitor(): org.openrewrite.TreeVisitor<*, org.openrewrite.ExecutionContext> =
428+
delegate.visitor
429+
override fun getRecipeList(): List<org.openrewrite.Recipe> = filtered
430+
}

0 commit comments

Comments
 (0)