Skip to content

Conversation

@MBoegers
Copy link
Contributor

What's changed?

This PR introduces a comprehensive suite of search recipes to identify ThreadLocal variables that could be migrated to ScopedValue in Java 25+ (JEP 506):

What's your motivation?

Java 25 introduces ScopedValue as a modern alternative to ThreadLocal for immutable thread-local values, offering:

  • Better performance (no need for weak references or cleanup)
  • Safer scoped value passing (values cannot be mutated)
  • Clearer lifecycle management

Unfortunately not all ThreadLocal instances qualify equally.

These recipes help development teams:

  1. Assess their codebase's readiness for ScopedValue migration
  2. Identify which ThreadLocals can be safely migrated
  3. Understand the scope of (manual) refactoring required

Anything in particular you'd like reviewers to focus on?

  1. Cross-file analysis implementation - The AbstractFindThreadLocals base class uses scanning to detect mutations across multiple compilation units. Please verify this correctly handles all edge cases.
  2. Mutation detection completeness - Review that we're catching all ways a ThreadLocal can be mutated (set, remove, initialValue override, etc.)

Anyone you would like to review specifically?

Have you considered any alternatives or workarounds?

  • Dataflow analysis: Considered but opted for AST-based visitor pattern to maintain consistency with OpenRewrite patterns and avoid complexity
  • Single-file analysis only: Rejected as it would miss many cross-file mutations, giving false positives
  • More granular recipes: Could have created more specific recipes but the current three categories cover the main use cases effectively

Any additional context

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've used the IntelliJ IDEA auto-formatter on affected files

@MBoegers MBoegers self-assigned this Oct 16, 2025
@MBoegers MBoegers added enhancement New feature or request recipe Recipe requested java 25+ labels Oct 16, 2025
@github-project-automation github-project-automation bot moved this to In Progress in OpenRewrite Oct 16, 2025
@MBoegers
Copy link
Contributor Author

needs another round of personal review, but open for comments.

@openrewrite openrewrite deleted a comment from github-actions bot Oct 17, 2025
@MBoegers MBoegers marked this pull request as ready for review October 17, 2025 09:27
- Moved ThreadLocalTable from search/ to table/ package to align with repository conventions
- Added data table verification tests to all four ThreadLocal recipe test files
- Updated import in AbstractFindThreadLocals to use new package location
}

public static class ThreadLocalInfo {
private @Nullable Path declarationPath;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private @Nullable Path declarationPath;
private @Nullable Path declarationPath;

Comment on lines +427 to +429
assertThat(rows.get(0).getClassName()).isEqualTo("com.example.Holder");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("com.example.Holder");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
assertThat(rows.getFirst().getClassName()).isEqualTo("com.example.Holder");
assertThat(rows.getFirst().getFieldName()).isEqualTo("TL");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Never mutated");

Comment on lines +358 to +362
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getModifiers()).isEqualTo("static final");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getModifiers()).isEqualTo("static final");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
assertThat(rows.getFirst().getClassName()).isEqualTo("Example");
assertThat(rows.getFirst().getFieldName()).isEqualTo("TL");
assertThat(rows.getFirst().getAccessModifier()).isEqualTo("private");
assertThat(rows.getFirst().getModifiers()).isEqualTo("static final");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Never mutated");

Comment on lines +262 to +265
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("PUBLIC_TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("public");
assertThat(rows.get(0).getMutationType()).isEqualTo("Potentially mutable");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("PUBLIC_TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("public");
assertThat(rows.get(0).getMutationType()).isEqualTo("Potentially mutable");
assertThat(rows.getFirst().getClassName()).isEqualTo("Example");
assertThat(rows.getFirst().getFieldName()).isEqualTo("PUBLIC_TL");
assertThat(rows.getFirst().getAccessModifier()).isEqualTo("public");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Potentially mutable");

Comment on lines +262 to +265
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getMutationType()).isEqualTo("Mutated only in initialization");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getMutationType()).isEqualTo("Mutated only in initialization");
assertThat(rows.getFirst().getClassName()).isEqualTo("Example");
assertThat(rows.getFirst().getFieldName()).isEqualTo("TL");
assertThat(rows.getFirst().getAccessModifier()).isEqualTo("private");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Mutated only in initialization");

}

public static class ThreadLocalInfo {
private @Nullable Path declarationPath;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private @Nullable Path declarationPath;
private @Nullable Path declarationPath;

Comment on lines +427 to +429
assertThat(rows.get(0).getClassName()).isEqualTo("com.example.Holder");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("com.example.Holder");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
assertThat(rows.getFirst().getClassName()).isEqualTo("com.example.Holder");
assertThat(rows.getFirst().getFieldName()).isEqualTo("TL");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Never mutated");

Comment on lines +358 to +362
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getModifiers()).isEqualTo("static final");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getModifiers()).isEqualTo("static final");
assertThat(rows.get(0).getMutationType()).isEqualTo("Never mutated");
assertThat(rows.getFirst().getClassName()).isEqualTo("Example");
assertThat(rows.getFirst().getFieldName()).isEqualTo("TL");
assertThat(rows.getFirst().getAccessModifier()).isEqualTo("private");
assertThat(rows.getFirst().getModifiers()).isEqualTo("static final");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Never mutated");

Comment on lines +262 to +265
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("PUBLIC_TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("public");
assertThat(rows.get(0).getMutationType()).isEqualTo("Potentially mutable");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("PUBLIC_TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("public");
assertThat(rows.get(0).getMutationType()).isEqualTo("Potentially mutable");
assertThat(rows.getFirst().getClassName()).isEqualTo("Example");
assertThat(rows.getFirst().getFieldName()).isEqualTo("PUBLIC_TL");
assertThat(rows.getFirst().getAccessModifier()).isEqualTo("public");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Potentially mutable");

Comment on lines +262 to +265
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getMutationType()).isEqualTo("Mutated only in initialization");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(rows.get(0).getClassName()).isEqualTo("Example");
assertThat(rows.get(0).getFieldName()).isEqualTo("TL");
assertThat(rows.get(0).getAccessModifier()).isEqualTo("private");
assertThat(rows.get(0).getMutationType()).isEqualTo("Mutated only in initialization");
assertThat(rows.getFirst().getClassName()).isEqualTo("Example");
assertThat(rows.getFirst().getFieldName()).isEqualTo("TL");
assertThat(rows.getFirst().getAccessModifier()).isEqualTo("private");
assertThat(rows.getFirst().getMutationType()).isEqualTo("Mutated only in initialization");

@MBoegers MBoegers marked this pull request as draft January 15, 2026 09:16
@MBoegers
Copy link
Contributor Author

@timtebeek, I will close this PR and move it over to my personal GitHub. I feel it's more of a demo than a recipe we want in the official catalog.

@MBoegers MBoegers closed this Jan 15, 2026
@github-project-automation github-project-automation bot moved this from In Progress to Done in OpenRewrite Jan 15, 2026
@timtebeek timtebeek deleted the boeg/find-thread-local branch January 15, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request java 25+ recipe Recipe requested

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants