Skip to content

Commit

Permalink
Remove cycles from the docs (#258)
Browse files Browse the repository at this point in the history
Co-authored-by: Mike Solomon <[email protected]>
  • Loading branch information
mike-solomon and mike-solomon authored Jan 18, 2024
1 parent 0bb4ecc commit 9e11d07
Show file tree
Hide file tree
Showing 4 changed files with 6 additions and 45 deletions.
12 changes: 3 additions & 9 deletions authoring-recipes/recipe-conventions-and-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ By reducing a recipe's scope and minimizing the number of changes a recipe makes
On the other hand, a change that unnecessarily clobbers formatting or is otherwise overly broad in scope will burden code reviewers and drastically reduce the rate at which they accept changes.

{% hint style="success" %}
[RewriteTest](recipe-testing.md#rewritetest-interface) helps you to verify that your recipe does not make unnecessary changes by running your recipe over multiple cycles. If you see your change being made many times it is likely your visitor fails to avoid making unnecessary changes. See the [cycle section](recipe-conventions-and-best-practices.md#stay-single-cycle) for more information.
[RewriteTest](recipe-testing.md#rewritetest-interface) helps you to verify that your recipe does not make unnecessary changes by running your recipe over multiple cycles. If you see your change being made many times it is likely your visitor fails to avoid making unnecessary changes.
{% endhint %}

{% hint style="info" %}
Expand Down Expand Up @@ -117,12 +117,6 @@ If your recipe is going to work with dates, please ensure that you adhere to [RS

Even very simple pieces of code have complex LST representations which are tedious and error-prone to construct by hand. Never attempt to build up LSTs by hand. Instead, you should use faculties like [JavaTemplate](../concepts-and-explanations/javatemplate.md) to turn code snippets into [LST elements](../concepts-and-explanations/lst-examples.md). For data formats like XML or JSON, it is usually more convenient to use the format's parser to turn a snippet of text into usable LST elements.

### Stay single cycle

OpenRewrite executes recipes in a loop. Each iteration of that loop through the full recipe list is called a cycle. This is so that if one recipe makes a change and another recipe would do something based on that change, it will have a chance to do so. This happens regardless of the order the recipes are executed in.

By default, only a single cycle is executed unless some recipe in the group overrides `Recipe.causesAnotherCycle()` to return `true`. For larger recipes, such as a framework migration, the performance impact of causing another cycle can be substantially detrimental. Whenever possible, a recipe should complete all of its work the first time (which avoids the need to override `Recipe.causesAnotherCycle()`).

### State conventions

You should generally avoid passing [state](https://en.wikipedia.org/wiki/State\_\(computer\_science\)) across visitors if at all possible. If you do need to pass state around, though, you should first figure out which parts of your recipe need what information.
Expand All @@ -137,9 +131,9 @@ When passing state between functions in the same visitor, there are two main opt

Cursor messaging is the preferred method. With it, you can utilize `TreeVisitor.getCursor()` to access a map that arbitrary data can be read from or written to. For instance, in the [AddDependency recipe](https://github.com/openrewrite/rewrite/blob/v7.34.3/rewrite-maven/src/main/java/org/openrewrite/maven/AddDependency.java), we pass data from the [overridden visitTag method](https://github.com/openrewrite/rewrite/blob/v7.34.3/rewrite-maven/src/main/java/org/openrewrite/maven/AddDependency.java#L182) to the [overridden visitDocument method](https://github.com/openrewrite/rewrite/blob/v7.34.3/rewrite-maven/src/main/java/org/openrewrite/maven/AddDependency.java#L196) in our `MavenVisitor`.

The `Cursor` object is a stack that keeps track of a visitor's current progress through an LST. Once everything has been visited in a particular [cycle](../concepts-and-explanations/recipes.md#execution-cycles), all of this information is then thrown away. There is no risk of this state leaking into places that you don't want.
The `Cursor` object is a stack that keeps track of a visitor's current progress through an LST. Once everything has been visited, all of this information is then thrown away. There is no risk of this state leaking into places that you don't want.

The `ExecutionContext` object, on the other hand, has a few key differences that make it less suitable for most situations. All recipes in a run will have the same `ExecutionContext` object. This object contains a map, similar to the `Cursor`, that recipes can read from or write arbitrary data to. Unlike the `Cursor` map, though, this `ExecutionContext` map sticks around between recipe run cycles. This poses some considerable danger.
The `ExecutionContext` object, on the other hand, has a key difference that make it less suitable for most situations. All recipes in a run will have the same `ExecutionContext` object. This object contains a map, similar to the `Cursor`, that recipes can read from or write arbitrary data to. This poses some considerable danger.

Imagine one recipe author decided to write a `foo` object to the `ExecutionContext` so that the `bar` and `bash` functions could both interact with it. Now, let's imagine another recipe author, unaware of the fact that the other recipe wrote a `foo` object to the `ExecutionContext`, also decides that their recipe should read and write to a `foo` object. If those recipes get chained together, both of those recipes could break.

Expand Down
4 changes: 2 additions & 2 deletions authoring-recipes/writing-a-java-refactoring-recipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public class SayHelloRecipe extends Recipe {

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// getVisitor() should always return a new instance of the visitor to avoid any state leaking between cycles
// getVisitor() should always return a new instance of the visitor
return new SayHelloVisitor();
}

Expand Down Expand Up @@ -410,7 +410,7 @@ public class SayHelloRecipe extends Recipe {

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// getVisitor() should always return a new instance of the visitor to avoid any state leaking between cycles
// getVisitor() should always return a new instance of the visitor
return new SayHelloVisitor();
}

Expand Down
27 changes: 1 addition & 26 deletions concepts-and-explanations/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class ChangeType extends Recipe {
}
```

This recipe accepts two configuration parameters via its constructor. Recipes may be linked together with many other recipes and run in a variety of orders, contexts, and number of cycles. Making your recipes immutable, as this one is, is a strongly recommended best practice. Any mutable state should be local to the visitor, a fresh instance of which should be returned from each invocation of `getVisitor()`.
This recipe accepts two configuration parameters via its constructor. Recipes may be linked together with many other recipes and run in a variety of orders, and contexts. Making your recipes immutable, as this one is, is a strongly recommended best practice. Any mutable state should be local to the visitor, a fresh instance of which should be returned from each invocation of `getVisitor()`.

### Linking Recipes Together

Expand Down Expand Up @@ -311,31 +311,6 @@ Using the same "Migrate JUnit 5" recipe as an example, the flow through the pipe

The initiation of the execution pipeline requires the creation of an execution context. There are overloaded versions of `Recipe.run()` that will implicitly create an execution context if one is not provided. The execution context is a mechanism for sharing state across recipes (and their underlying visitors). `ExecutionContext` provides the ability to add and poll messages in a thread-safe manner.

### Execution Cycles

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.

As an example, let's assume that two recipes are added to the execution pipeline. The first recipe performs whitespace formatting on an LST and the second recipe generates additional code that is added to the same LST. Those two recipes are executed in order, so the formatting recipe is applied before the second recipe adds its generated code. The execution pipeline detects that changes have been made and executes a second pass through the recipes. During the second pass, the formatting recipe will now properly format the generated code that was added as a result of the first cycle through the execution pipeline.

When large recipes are applied to large repositories, the performance impact of additional cycles can be substantial. Whenever possible recipes should complete all of their own work within a single cycle, rather than spreading it out over multiple cycles. Whenever a recipe requires more than a single cycle to complete its work, it must return `true` from `Recipe.causesAnotherCycle()` for another cycle to be added.

If a [Declarative Recipe](recipes.md#declarative-recipes) needs to cause another cycle but none of its constituent recipes return `true` from `Recipe.causesAnotherCycle()`, the declarative recipe can set the property in yaml:

```yaml
---
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.NeedsAnotherCycle
displayName: Test
causesAnotherCycle: true
recipeList:
- com.yourorg.SomeRecipeThatDoesNotCauseAnotherCycle
- com.yourorg.SomeOtherRecipeThatDoesNotCauseAnotherCycle
```

{% hint style="success" %}
Calling `Recipe.doAfterVisit()` during the execution of a recipe/visitor schedules that recipe to execute immediately after the current recipe during the current cycle. `Recipe.doAfterVisit()` does not cause an extra cycle to execute or be required.
{% endhint %}

### Result Set

The successful completion of a recipe's execution pipeline produces a collection of `Result` instances. Each result represents the changes made to a specific source file and provides access to the following information:
Expand Down
8 changes: 0 additions & 8 deletions concepts-and-explanations/refactor.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@ for (Change change : changes) {
}
```

### Refactor Cycles

The visitors in a refactoring operation may produce changes that in turn cause another visitor to do further work. As a result, `Refactor` performs multiple passes \(or cycles\) over visitors until either no changes are made in a pass or some maximum number of passes is reached \(by default 3\).

For example, suppose we add a whitespace-formatting Checkstyle and a code-generating Spring visitor to a refactor in that order. If we just run the source files through each visitor once, the Checkstyle rule runs first and doesn't change anything. Then the Spring visitor runs and maybe generates some code that doesn't match the style of the project. If we did another loop, the Checkstyle visitor would effectively see this generated code and transform it to look idiomatically consistent in the context of this project. Then the Spring visitor runs again and doesn't have anything further to do.

In this way, individual visitors can support one another without requiring any explicit dependencies between them!

### Next Steps

In [Styles](styles.md), we'll show how to configure styles that are stored with the LST that make these refactoring operations produce changes that look idiomatically consistent with the surrounding project.
Expand Down

0 comments on commit 9e11d07

Please sign in to comment.