Skip to content

Conversation

Jenson3210
Copy link
Contributor

@Jenson3210 Jenson3210 commented Aug 12, 2025

Fix cross-recipe state tracking for sequential dependency transformations

Summary

This PR enhances the ChangeDependencyGroupIdAndArtifactId recipe to correctly handle sequential transformations of the same dependency within a single recipe run. The fix uses the ExecutionContext to pass state between different recipe scanning phases, enabling later recipe executions to detect when a dependency will already have been modified by an earlier recipe in the chain by the time this visitor receives it.

Problem

When multiple ChangeDependencyGroupIdAndArtifactId recipes are chained together in a single recipe run (e.g., transforming A→B then B→C), the second recipe would fail to find and transform the dependency because it was still looking for the original coordinates rather than the updated ones from the first transformation.

Solution

The implementation now leverages the ExecutionContext to store transformation state between recipe scanning phases:

  1. Scanning Phase: During the initial scanning phase, each recipe records which dependencies it will transform in the ExecutionContext
  2. State Tracking: This context is passed between different recipe runs, allowing subsequent recipes to check if a dependency tag will be modified by an earlier recipe

Changes

ChangeDependencyGroupIdAndArtifactId.java

  • Added ExecutionContext state management to track transformations across recipe executions
  • Implemented logic to detect and handle dependencies that have been modified by earlier recipes in the chain
  • Ensures proper coordination between scanning and visitation phases
  • Cleaned up the Accumulator a bit to use the same object.

ChangeDependencyGroupIdAndArtifactIdTest.java

  • Added comprehensive test canChangeSameDependencyMultipleTimesInASingleRecipeRun() that validates a sequential transformation of bouncycastle dependencies: bcprov-jdk15onbcprov-jdk15to18bcprov-jdk18on

Technical Details

The ExecutionContext serves as a communication channel between recipe phases, storing metadata about planned transformations. This allows the recipe framework to maintain awareness of in-flight changes, ensuring that subsequent recipes in a chain operate on the most current state rather than the original document state. Note that this is not the change that I originally intended / am very happy with, but other attempts to use cursor messages etc have all proved not to be working.

Testing

  • New test validates complex transformation chains work correctly
  • Ensures recipes can handle both single transformations and sequential multi-step transformations
  • All existing tests continue to pass, confirming backward compatibility

An example

In the example where we run A -> B then B -> C our scanners will run sequentially.

A -> B : the xml code does contain A in the code and calculates new dependency
B-> C: the xml code does not contain B in the code and therefor does not calculate the new dependency.

By introducing the context/Map, the code will now:

A -> B: the xml code does contain A in the code and calculates new dependency + puts A -> B mapping in the context
B -> C: the xml code does not contain B, but the context knows that at the time this recipe will run, B will be present so we calculate the new situation to migrate to based on the expected outcome of the previous run that bumps to B.

This logic is "safe" to bump multiple levels, but might give unexpected results when we have a recipe:

  • bump A to C
  • bump B to C
  • bump C to D

where A and B are present in the same POM as the new dependency will already be present and this accumulator will also not detect this.
As this change is already quite big, I believe a separate PR for this behavior fix is needed (also the logic of checkIfNewDependencyPresents needs a fix in that one as it won't work for latest.patch etc...)

@Jenson3210 Jenson3210 self-assigned this Aug 12, 2025
@Jenson3210 Jenson3210 added the bug Something isn't working label Aug 12, 2025
@github-project-automation github-project-automation bot moved this to In Progress in OpenRewrite Aug 12, 2025
github-actions[bot]

This comment was marked as off-topic.

changedDependency = changedDependency.withVersion(resolvedNewVersion);
} catch (MavenDownloadingException e) {
return e.warn(tag);
throw new RuntimeException("Failed to resolve version for " + changedDependency, e);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change -> I adapted this as the e.warn modifies the lst and then I had tests fail with a warning indicating that scanners cannot modify the lst

Optional.ofNullable(newArtifactId).orElse(oldArtifactId),
newVersion, versionPattern).getVisitor());
}
ctx.pollMessage(CHANGED_DEPENDENCIES);
Copy link
Contributor Author

@Jenson3210 Jenson3210 Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is polling the message to clear the context necessary? Is there a better way to clear this message than to run it in every visitor? We can safely remove it after the all-recipe-scanning phase.

github-actions[bot]

This comment was marked as off-topic.

Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx);
boolean isOldDependencyTag = isDependencyTag(oldGroupId, oldArtifactId);

Map<GroupArtifactVersion, GroupArtifactVersion> changedDependencies = ctx.getMessage(CHANGED_DEPENDENCIES);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the context not shared between visiting different POM files? What I was meaning was that if we have a record of a dependency having been changed, we may need distinguish that between different POM files in the same repo so that we don't mistakenly skip changing a dependency in a sibling module's PO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to see how that is different from before.
the accumulator is also shared between different POM files.
I assumed that if this recipe runs, it will bump on all impacted modules making the same changes there. Or am I misunderstanding?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jenson3210 I think what I was thinking was that getInitialValue(..) was called once when the recipe was invoked, whereas the visitor was instantiated per file being analyzed, and so if you were checking whether you needed to make a change based on the presence of the message for a particular dependency then it might skip changing when it actually needed to do so still in the current file, but I realize now I had misread the checks, and that they should still be safe, other than what you mentioned in the comment below.

Map<GroupArtifactVersion, GroupArtifactVersion> changedDependencies = ctx.getMessage(CHANGED_DEPENDENCIES);

GroupArtifactVersion oldDependency = new GroupArtifactVersion(oldGroupId, oldArtifactId, null);
for (Map.Entry<GroupArtifactVersion, GroupArtifactVersion> entry : changedDependencies.entrySet()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steve-aom-elliott Your comment makes me realize that there might be multiple bumps earlier on so that I will have to change the loop logic still a bit. (luckily the revert gives us this time)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants