<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged openrewrite</title>
<link>https://coderlegion.com/tag/openrewrite</link>
<description>Powered by Question2Answer</description>
<item>
<title>Testing an OpenRewrite recipe</title>
<link>https://coderlegion.com/3508/testing-an-openrewrite-recipe</link>
<description>&lt;p&gt;For the last two weeks, I've kicked the tires of OpenRewrite. At first, I created a &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/1/&quot;&gt;recipe to move Kotlin source files&lt;/a&gt; as per the official recommendations with a set package name. I then improved the &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/2/&quot;&gt;recipe to compute the root automatically&lt;/a&gt;. In both versions, I thoroughly tested the recipe. However, my testing approach was wrong. In this post, I want to describe my mistakes, and how I fixed them.&lt;/p&gt;
&lt;h3 id=&quot;the-naive-approach&quot;&gt;The naive approach&lt;/h3&gt;
&lt;p&gt;I originally approached the testing of the recipe in a very naive way, to say the least. As explained in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/1/#testing-the-recipe&quot;&gt;first post&lt;/a&gt;, I used OpenRewrite's low-level APIs. Here's what I wrote:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Given
val parser = KotlinParser.builder().build()                                      //1
val cu = parser.parse(
    InMemoryExecutionContext(),                                                  //2
    sourceCode
).findFirst()                                                                    //3
 .orElseThrow { IllegalStateException(&quot;Failed to parse Kotlin file&quot;) }           //3
val originalPath = Paths.get(originalPath)
val modifiedCu = (cu as K.CompilationUnit).withSourcePath(originalPath)          //4

// When
val recipe = FlattenStructure(configuredRootPackage)
val result = recipe.visitor.visit(modifiedCu, InMemoryExecutionContext())        //5

// Then
val expectedPath = Paths.get(expectedPath)
assertEquals(expectedPath, (result as SourceFile).sourcePath)                    //6
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Build the Kotlin parser&lt;/li&gt;
&lt;li&gt;Set an execution context; I had to choose and the in-memory one was the easiest.&lt;/li&gt;
&lt;li&gt;Boilerplate to get the single compilation unit from the stream&lt;/li&gt;
&lt;li&gt;Cast to a &lt;code&gt;K.CompilationUnit&lt;/code&gt; because we know better&lt;/li&gt;
&lt;li&gt;Explicitly call the visitor to visit&lt;/li&gt;
&lt;li&gt;Assert the recipe moved the file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The above works, but requires a deep understanding of how OpenRewrite works. I didn't have that understanding, but it was good enough. It came back to bite me when I improved the recipe to compute the root.&lt;/p&gt;
&lt;p&gt;As explained in the last post, I switched from a regular recipe to a scanning recipe. I had to provide at least two source files to test the new capability. I came up with the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// When
val recipe = FlattenStructure()
val context = InMemoryExecutionContext()
val acc = AtomicReference//String?(null)
recipe.getScanner(acc).visit(modifiedCu1, context)                               //1
recipe.getScanner(acc).visit(modifiedCu2, context)                               //1
val result1 = recipe.getVisitor(acc).visit(modifiedCu1, context)                 //2
val result2 = recipe.getVisitor(acc).visit(modifiedCu2, context)                 //2
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Get the scanner and visit source files to compute the root&lt;/li&gt;
&lt;li&gt;Get the visitor and visit source files to move the file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It worked, but I admit it was a lucky guess. More involved recipes would require a deeper knowledge of how OpenRewrite works, with more potential bugs. Fortunately, OpenRewrite provides the means to keep the testing code at the right level of abstraction.&lt;/p&gt;
&lt;h3 id=&quot;the-nominal-approach&quot;&gt;The nominal approach&lt;/h3&gt;
&lt;p&gt;The nominal approach involves a couple of out-of-the-box classes; it requires a new dependency. I didn't do it before, so now is a good time: let's introduce a &lt;abbr title=&quot;Bill Of Material&quot;&gt;BOM&lt;/abbr&gt; to align all of OpenRewrite's dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.openrewrite.recipe&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;rewrite-recipe-bom&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.9.0&amp;lt;/version&amp;gt;
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
            &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's now possible to add the dependency without a version, as Maven resolves it from the above BOM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.openrewrite&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;rewrite-test&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This brings a couple of new classes to the project:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5041497665654198418&quot; alt=&quot;OpenRewrite recipes test class diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;The documentation states your test class should inherit from &lt;code&gt;RewriteTest&lt;/code&gt;, which provides &lt;code&gt;rewriteRun&lt;/code&gt;. The latter runs the recipe, without any need to know about its inner workings, &lt;em&gt;e.g.&lt;/em&gt;, the above in-memory execution context. It's the abstraction level that we want. &lt;code&gt;Assertions&lt;/code&gt; offers static methods to assert. OpenRewrite also advises using Assertion4J, which I fully endorse. Yet, I didn't do it to keep the comparison simpler.&lt;/p&gt;
&lt;p&gt;We can rewrite the previous snippet to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;rewriteRun(                                                                     //1
    kotlin(sourceCode1) { spec -&amp;gt;                                               //2-3-4
        spec.path(&quot;src/main/kotlin/ch/frankel/blog/foo/Foo.kt&quot;)                 //5
        spec.afterRecipe {                                                      //6
            assertEquals(                                                       //7
                Paths.get(&quot;src/main/kotlin/foo/Foo.kt&quot;),
                it.sourcePath
            )
        }
    },
    kotlin(sourceCode2) { spec -&amp;gt;                                               //2-3-4
        spec.path(&quot;src/main/kotlin/org/frankel/blog/bar/Bar.kt&quot;)                //5
        spec.afterRecipe {                                                      //6
            assertEquals(                                                       //7
                Paths.get(&quot;src/main/kotlin/bar/Bar.kt&quot;),
                it.sourcePath
            )
        }
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Run the recipe&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kotlin&lt;/code&gt; transform the string into a &lt;code&gt;SourceSpecs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I'm using Kotlin, but &lt;code&gt;java&lt;/code&gt; does the same for regular Java projects&lt;/li&gt;
&lt;li&gt;Allow customizing the source specification&lt;/li&gt;
&lt;li&gt;Customize the path&lt;/li&gt;
&lt;li&gt;Hook after running the recipe&lt;/li&gt;
&lt;li&gt;Assert that the recipe updated the path according to the expectations&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You may have noticed that the rewritten code doesn't specify which recipe it's testing. That's the responsibility of the &lt;code&gt;RewriteTest.defaults()&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class FlattenStructureComputeRootPackageTest : RewriteTest {

    override fun defaults(spec: RecipeSpec) {
        spec.recipe(FlattenStructure())
    }

    // Rest of the class
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;don-t-forget-cycles&quot;&gt;Don't forget cycles&lt;/h3&gt;
&lt;p&gt;If you followed the above instructions, there's a high chance your test fails with this error message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.lang.AssertionError: Expected recipe to complete in 0 cycle, but took 1 cycle. This usually indicates the recipe is making changes after it should have stabilized.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to turn to the documentation to understand this cryptic message:&lt;/p&gt;
&lt;p&gt;&amp;gt;The recipes in the execution pipeline may produce changes that in turn cause another recipe to do further work. As a result, the pipeline may perform multiple passes (or cycles) over all the recipes in the pipeline again until either no changes are made in a pass or some maximum number of passes is reached (by default 3). This allows recipes to respond to changes made by other recipes which execute after them in the pipeline.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#execution-cycles&quot;&gt;Execution Cycles&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because the recipe doesn't rely on any other and no other recipe depends on it, we can set the cycle to 1.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;override fun defaults(spec: RecipeSpec) {
    spec.recipe(FlattenStructure())
        .cycles(1)                                                              //1
        .expectedCyclesThatMakeChanges(1)                                       //2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Set how many cycles the recipe should run&lt;/li&gt;
&lt;li&gt;Set to 0 if the recipe isn't expected to make changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;criticisms&quot;&gt;Criticisms&lt;/h3&gt;
&lt;p&gt;I like what the OpenRewrite testing classes bring, but I have two criticisms.&lt;/p&gt;
&lt;p&gt;First and foremost, why does OpenRewrite assert the number of cycles by default? It bit me in the back for no good reason. I had to dig into the documentation and understand how OpenRewrite works, although the testing API is supposed to shield users from its inner workings. I also can't help but wonder about the defaults.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class RecipeSpec {

    @Nullable
    Integer cycles;

    int getCycles() {
        return cycles == null ? 2 : cycles;                                     //1
    }

    int getExpectedCyclesThatMakeChanges(int cycles) {
        return expectedCyclesThatMakeChanges == null ? cycles - 1 :             //2
                expectedCyclesThatMakeChanges;
    }

    // Rest of the class body
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Why two cycles by default? Shouldn't one be enough in most cases?&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;cycles - 1&lt;/code&gt; by default?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My second criticism is about how the provided testing classes make you structure your tests. I like to structure them into three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Given: describe the initial state&lt;/li&gt;
&lt;li&gt;When: execute the to-be-tested code&lt;/li&gt;
&lt;li&gt;Then: assert the final state conforms to what I expect.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With OpenRewrite's abstractions, the structure is widely different from the above.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this post, I migrated my &lt;em&gt;ad hoc&lt;/em&gt; test code to rely on OpenRewrite's provided classes. Even though they are not exempt from criticism, they offer a solid abstraction layer and make tests more maintainable.&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/flatten-kotlin-recipe&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/authoring-recipes/recipe-testing&quot;&gt;Recipe testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#execution-cycles&quot;&gt;Execution Cycles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bitflippers.dev/how-to-resolve-expected-recipe-cycle-mismatch&quot;&gt;How to resolve expected recipe cycle mismatch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/3/&quot;&gt;A Java Geek&lt;/a&gt; on June 22&lt;sup&gt;nd&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/3508/testing-an-openrewrite-recipe</guid>
<pubDate>Thu, 26 Jun 2025 20:18:12 +0000</pubDate>
</item>
<item>
<title>Improving my previous OpenRewrite recipe</title>
<link>https://coderlegion.com/3367/improving-my-previous-openrewrite-recipe</link>
<description>&lt;p&gt;I started discovering OpenRewrite last week by writing a Kotlin recipe that moves Kotlin files according to the official directory structure recommendation. I mentioned some future works, and here they are. In this post, I want to describe how to compute the root package instead of letting the user set it.&lt;/p&gt;
&lt;h3 id=&quot;reminder&quot;&gt;Reminder&lt;/h3&gt;
&lt;p&gt;I developed last week a recipe to follow the Kotlin recommendation regarding directory structure:&lt;/p&gt;
&lt;p&gt;&amp;gt;In pure Kotlin projects, the recommended directory structure follows the package structure with the common root package omitted. For example, if all the code in the project is in the &lt;code&gt;org.example.kotlin&lt;/code&gt; package and its subpackages, files with the &lt;code&gt;org.example.kotlin&lt;/code&gt; package should be placed directly under the source root, and files in &lt;code&gt;org.example.kotlin.network.socket&lt;/code&gt; should be in the network/socket subdirectory of the source root.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://kotlinlang.org/docs/coding-conventions.html#source-code-organization&quot;&gt;Directory structure&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a Java project, if you have packages &lt;code&gt;ch.frankel.foo&lt;/code&gt;, &lt;code&gt;ch.frankel.bar&lt;/code&gt;, and &lt;code&gt;ch.frankel.baz&lt;/code&gt;, you'll get the following structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src
 |__ main
    |__ java
       |__ ch
          |__ frankel
             |__ foo
             |__ bar
             |__ baz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Kotlin projects, many developers follow the same structure as above, but it can be flattened as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src
 |__ main
    |__ kotlin
       |__ foo
       |__ bar
       |__ baz
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-work&quot;&gt;The work&lt;/h3&gt;
&lt;p&gt;My recipe's original version mandated that you configure the root package yourself, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel&lt;/code&gt; for the above example. However, it should be possible to compute it automatically, from looking at the source files. It adds an extra step to the process: before moving the file to the root, the recipe should look at each source file, get the package, compute the longest prefix with the existing root, make it the root, and go to the next source file. The regular &lt;code&gt;Recipe&lt;/code&gt; doesn't work in this case. We need to switch to a &lt;code&gt;ScanningRecipe&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&amp;gt;If a recipe needs to generate new source files or needs to see all source files before making changes, it must be a &lt;code&gt;ScanningRecipe&lt;/code&gt;. A &lt;code&gt;ScanningRecipe&lt;/code&gt; extends the normal &lt;code&gt;Recipe&lt;/code&gt; and adds two key objects: an &lt;code&gt;accumulator&lt;/code&gt; and a &lt;code&gt;scanner&lt;/code&gt;. The &lt;code&gt;accumulator&lt;/code&gt; object is a custom data structure defined by the recipe itself to store any information the recipe needs to function. The &lt;code&gt;scanner&lt;/code&gt; object is a visitor which populates the &lt;code&gt;accumulator&lt;/code&gt; with data.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes&quot;&gt;Scanning recipes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Scanning recipes offer two steps: the first to gather data, the second to do the work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14758325757968137092&quot; alt=&quot;ScanningRecipe class diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;We must design our algorithm within the constraints of OpenRewrite, and they are the following: in the first phase, for each source file, OpenRewrite will call the &lt;code&gt;getScanner()&lt;/code&gt; method that returns a visitor of our choice. In turn, OpenRewrite calls the visitor's methods, which can access the accumulator.&lt;/p&gt;
&lt;p&gt;My first naive approach was to use a collection as the accumulator, but it's not necessary. The algorithm is much simpler if we set a mutable placeholder that holds the package root and update it if necessary during each visit. The initial value should be &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the value is &lt;code&gt;null&lt;/code&gt;, which happens on the first visitor, set the package root to the source file's package.&lt;/li&gt;
&lt;li&gt;If the value is an empty string, skip–see below.&lt;/li&gt;
&lt;li&gt;In any other case, compute the new package root by finding the longest prefix between the existing package root and the source file's package.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It might result in an empty string, indicating that packages have no common root, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel.foo&lt;/code&gt; and &lt;code&gt;org.frankel.foo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here's the updated code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class FlattenStructure(private val rootPackage: String?) : ScanningRecipe&amp;lt;AtomicReference&amp;lt;String?&amp;gt;&amp;gt;() { //1-2

  constructor() : this(null)                                                   //3

  override fun getDisplayName(): String = &quot;Flatten Kotlin package directory structure&quot;
  override fun getDescription(): String =
    &quot;Move Kotlin files to match idiomatic layout by omitting the root package according to the official recommendation.&quot;

  override fun getInitialValue(ctx: ExecutionContext) = AtomicReference&amp;lt;String?&amp;gt;(null) //4

  override fun getScanner(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
    if (rootPackage != null) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;() //5
    return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
      override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
        val packageName = cu.packageDeclaration?.packageName ?: return cu
        val computedPackage = acc.get()
        when (computedPackage) {
          null -&amp;gt; acc.set(packageName)                                         //6
          &quot;&quot; -&amp;gt; {}                                                             //7
          else -&amp;gt; {
            val commonPrefix = packageName.commonPrefixWith(computedPackage).removeSuffix(&quot;.&quot;) //8
            acc.set(commonPrefix)
          }
        }
        return cu
      }
    }
  }

  override fun getVisitor(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
    return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
      override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
        val packageName = cu.packageDeclaration?.packageName ?: return cu
        val packageToSet: String? = rootPackage ?: acc.get()                   //9
        if (packageToSet == null || packageToSet.isEmpty()) return cu
        val relativePath = packageName.removePrefix(packageToSet).removePrefix(&quot;.&quot;)
          .replace('.', '/')
        val filename = cu.sourcePath.fileName.toString()
        val newPath: Path = Paths.get(&quot;src/main/kotlin&quot;)
          .resolve(relativePath)
          .resolve(filename)
        return cu.withSourcePath(newPath)
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Inherit from &lt;code&gt;ScanningRecipe&lt;/code&gt; instead of directly from &lt;code&gt;Recipe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set the accumulator type to be an &lt;code&gt;AtomicReference&amp;lt;String?&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make the configuration easier when you don't override the package root&lt;/li&gt;
&lt;li&gt;The initial root is uninitialized&lt;/li&gt;
&lt;li&gt;Skip the computation if the root package is manually set&lt;/li&gt;
&lt;li&gt;If it's the first file visited, the accumulator holds &lt;code&gt;null&lt;/code&gt;, and we can set the (temporary) root as the current package&lt;/li&gt;
&lt;li&gt;One of the previous computations returned no common root–do nothing&lt;/li&gt;
&lt;li&gt;Find the longest common prefix between the held package root and the current package&lt;/li&gt;
&lt;li&gt;The only difference with the original code:&lt;br&gt;
we check if the root package has been set manually otherwise, we use the one computed in the first pass&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;optimizing-the-recipe&quot;&gt;Optimizing the recipe&lt;/h3&gt;
&lt;p&gt;You may have noticed that when there is no common root, &lt;em&gt;e.g.&lt;/em&gt;, &lt;code&gt;ch.frankel.foo&lt;/code&gt; and &lt;code&gt;org.frankel.foo&lt;/code&gt;, we scan all files anyway. In a small codebase, it's not a big issue, but when scanning millions of source files, that's a huge waste of CPU cycles and time. If you run the recipe in the Cloud, it directly translates to money. We should stop scanning as soon as we detect the computed package root is an empty string to optimize the recipe.&lt;/p&gt;
&lt;p&gt;Here's the updated code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;override fun getScanner(acc: AtomicReference&amp;lt;String?&amp;gt;): TreeVisitor&amp;lt;*, ExecutionContext&amp;gt; {
  if (rootPackage != null) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;()   //1
  val currentPackage = acc.get()
  if (currentPackage == &quot;&quot;) return TreeVisitor.noop&amp;lt;Tree, ExecutionContext&amp;gt;()  //2
  return object : KotlinIsoVisitor&amp;lt;ExecutionContext&amp;gt;() {
    override fun visitCompilationUnit(cu: K.CompilationUnit, ctx: ExecutionContext): K.CompilationUnit {
      val packageName = cu.packageDeclaration?.packageName ?: return cu
      // Different call than the one above!
      val currentPackage = acc.get()
      // First scanned file
      if (currentPackage == null) acc.set(packageName)                         //3
      else {
        // Find the longest common prefix between the stored package and the current one
        val commonPrefix = packageName.commonPrefixWith(currentPackage).removeSuffix(&quot;.&quot;)
        acc.set(commonPrefix)
      }
      return cu
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;If the root package has been set, skip visiting&lt;/li&gt;
&lt;li&gt;If one of the previous computations sets an empty string, there isn't any common root package: skip visiting&lt;/li&gt;
&lt;li&gt;Simplify the visitor by removing the clause when the accumulator is an empty string since it can't happen anymore&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that OpenRewrite still scans each file, but at least doesn't visit it thanks to the no-op visitor.&lt;/p&gt;
&lt;h3 id=&quot;counting-visits&quot;&gt;Counting visits&lt;/h3&gt;
&lt;p&gt;I made a couple of attempts before finding the right approach to the above. To ensure that I got it right, I wanted to display the number of visits by the scanner. We can use the accumulator to increment the visit count. Here are the changes I made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migrated from an &lt;code&gt;AtomicReference&amp;lt;String?&amp;gt;&lt;/code&gt; to an &lt;code&gt;AtomicReference&amp;lt;Pair&amp;lt;Int, String?&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;getInitialValue()&lt;/code&gt; function returns &lt;code&gt;AtomicReference&amp;lt;Pair&amp;lt;Int, String?&amp;gt;&amp;gt;(0 to null)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;During each visit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get the visits count from the accumulator&lt;/li&gt;
&lt;li&gt;Increment it&lt;/li&gt;
&lt;li&gt;Print it&lt;/li&gt;
&lt;li&gt;Store it back in the accumulator&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update the tests accordingly&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With packages &lt;code&gt;ch.frankel.blog.foo&lt;/code&gt;, &lt;code&gt;org.frankel.blog.bar&lt;/code&gt;, and &lt;code&gt;org.frankel.blog.baz&lt;/code&gt;, the log shows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] [stdout] FlattenStructure: 1 file visited
[INFO] [stdout] FlattenStructure: 2 files visited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By changing &lt;code&gt;ch.frankel.blog.foo&lt;/code&gt; to &lt;code&gt;org.frankel.blog.foo&lt;/code&gt;, the log changes to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] [stdout] FlattenStructure: 1 files visited
[INFO] [stdout] FlattenStructure: 2 files visited
[INFO] [stdout] FlattenStructure: 3 files visited
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because I made these changes just to validate my understanding of how OpenRewrite works, I put them in the &lt;code&gt;visits_count&lt;/code&gt; branch on GitHub. To see the differences, execute &lt;code&gt;git diff master visits_count&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this post, I added the automatic computation of the root package. I had to change my design and understand how scanning recipes work. Then, I skipped further visits when there wasn't any common root package to optimize performance.&lt;/p&gt;
&lt;p&gt;The recipe is still not serializable, though it's a recommendation. I also noticed that my tests didn't leverage OpenRewrite's testing API. There's still a lot of work to do!&lt;/p&gt;
&lt;p&gt;The complete source code for this post can be found on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ajavageek/flatten-kotlin-recipe&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/concepts-and-explanations/recipes#scanning-recipes&quot;&gt;Scanning Recipes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openrewrite/rewrite-testing-frameworks/blob/main/src/main/java/org/openrewrite/java/testing/mockito/AnyToNullable.java&quot;&gt;Code sample of an existing scanning recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.openrewrite.org/reference/scanning-recipes&quot;&gt;Existing scanning recipes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/openrewrite-recipes/2/&quot;&gt;A Java Geek&lt;/a&gt; on June 15&lt;sup&gt;th&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/3367/improving-my-previous-openrewrite-recipe</guid>
<pubDate>Thu, 19 Jun 2025 09:26:47 +0000</pubDate>
</item>
</channel>
</rss>