Repository navigation
Description
We triaged all recipes in this repo against our recipe review checklist: 216 Java classes and 5 YAML files, the same way as openrewrite/rewrite-spring#1201, openrewrite/rewrite-testing-frameworks#1211 and openrewrite/rewrite-migrate-java#1330. This issue tracks behavioural bugs, wiring and design questions. Low-risk mechanical polish is tracked separately in #1115.
We reproduced 36 of the bugs with a throwaway RewriteTest, and marked those PROBED. Two suspected bugs did not reproduce and were left out. CommonStaticAnalysis is the flagship composite here, so sections B and C cover only recipes it runs. Sections D and E in the first comment cover the rest.
Each item is self-contained, so you can pick one up independently or point an agent at it ("work on item C3 of this issue"). Please reference the item ID in your PR title or description. As soon as you open the PR, tick the item's box and add a link to the PR after it; there's no need to wait for the merge.
How to pick up an item
- Line numbers are against
mainat3b091745and may have drifted, so search by symbol if needed. - Java files are under
src/main/java/org/openrewrite/staticanalysis/and YAML undersrc/main/resources/META-INF/rewrite/. - Wiring shorthand:
- CSA =
CommonStaticAnalysis(common-static-analysis.yml) - CC =
CodeCleanup(static-analysis.yml) - JBP =
org.openrewrite.java.migrate.JavaBestPracticesin rewrite-migrate-java
- CSA =
- PROBED means a throwaway test reproduced the behaviour; the probe input is given and can become the regression test. Confirmed means someone traced the code path and the behaviour is certain. Suspected means it was inferred from reading. Write the probe test first; if it does not reproduce, close the item with a note.
- Prefer bailing out (making no change) over guessing when a recipe can't preserve semantics.
- Recipes should work across Java, Kotlin, Groovy and C#. Where a recipe breaks another language, fix it for that language where reasonably possible, and add a test in that language. Bail out only for constructs that have no equivalent there.
- Every fix comes with a test. Expected output must be correct, not just a record of today's behaviour; several tests below assert broken output.
- Use one concern per commit, with an imperative headline that names APIs in backticks.
- Items in A need a maintainer decision first. Comment on the issue before starting.
- Already tracked elsewhere, so left out: DefaultComesLast recipe creates duplicate case #289, org.openrewrite.staticanalysis.AvoidBoxedBooleanExpressions does not understand @NonNull annotations #349, org.openrewrite.staticanalysis.ReplaceDuplicateStringLiterals creates unnecessary duplicates #351, InstanceOfPatternMatch changes code meaning with multiple reassignments #480, CombineSemanticallyEqualCatchBlocks expects too much when comparing body clauses #506, CombineSemanticallyEqualCatchBlocks indexes J.Case#getCaseLabels() unguarded, and only ever compares the first label #985, EmptyBlock and RemoveMethodsOnlyCallSuper delete blocks and methods whose only content is a comment #1050, AddSerialVersionUidToSerializable skips generic classes implementing Serializable #1055, and open PRs RemoveUnusedLabels: preserve comments and Kotlin-referenced labels #966, ObjectFinalizeCallsSuper: declare the overridden finalize() exceptions when adding super.finalize() #969–ReplaceDeprecatedRuntimeExecMethods: preserve the argument vector #976, UnnecessaryCloseInTryWithResources: only remove a trailing close of a Closeable #978–UseLambdaForFunctionalInterface: keep the receiver of implicit getClass() #980, Avoid non-compiling collisions when renaming near-miss
Objectmethods #987, Preserve comments and Groovy conversions when replacing wrapper constructors #991, Widenfinalize()throws clauses across class hierarchies inObjectFinalizeCallsSuper#993–Place@Nullablecorrectly on qualified nested return types #996, Preserve escaped backslash-newline text inUsePortableNewlines#1006, Skip declaration-site variance for invariant storage #1007, AllBranchesIdentical to hoist side-effect statements #1032, Java: Fix AddSerialVersionUidToSerializable for generic classes #1060, Preserve casts after reassignment in InstanceOfPatternMatch #1074, ReplaceLambdaWithCallableReference for Kotlin #1092.
A. Wiring & placement (decide first)
-
A1 Take
IndexOfChecksShouldUseAStartPositionout ofCommonStaticAnalysis(PROBED)Details
- Where:
IndexOfChecksShouldUseAStartPosition.java:54-69; the@DocumentExample(changeLhsWithLiteral) asserts the broken output. - Problem:
s.indexOf("x") > 2("the first match is after index 2") becomess.indexOf("x", 2) > -1("some match is at index 2 or later"). That is off by one, and also true when an early match is followed by a later one. There is no equivalent single call. - Options: remove it from CSA and make it search-only, or delete it.
- Where:
-
A2
HideUtilityClassConstructor: active inCodeCleanup, commented out ofCommonStaticAnalysis, and breaks callers (PROBED)Details
- Where:
HideUtilityClassConstructorVisitor.java:84-98,246-262;static-analysis.yml:38;common-static-analysis.yml:46(no reason given). - Problem: a class counts as a utility class from its own members only. With
class MoreConstants extends Constants {}andnew Constants()elsewhere,private Constants() {}is still added, and both stop compiling. - Groovy (Suspected): the context-sensitive Java constructor template runs inside Groovy classes; add a Groovy test.
- Fix: skip classes that are instantiated or extended in the source set (scan per
JavaProject). Then decide whether it belongs in CSA, CC, both or neither, and comment the decision.
- Where:
-
A3
ReplaceThreadRunWithThreadStartrewritessuper.run()(PROBED)Details
- Where:
static-analysis.yml:51-62: a bareChangeMethodNameonjava.lang.Thread run(), wired into CC. - Problem:
super.run()inside aThreadsubclass'srun()(the standard delegation to theRunnabletarget) becomessuper.start(), which throwsIllegalThreadStateExceptionat runtime.thread::runpassed as aRunnablebecomesthread::startand spawns an extra thread. S1217 flags neither. - Fix: a Java recipe that skips
super.receivers, calls inside aThreadsubclass, and member references. Decide whether it belongs in CC (a "cleanup") or next to its RSPEC siblings in CSA. - Related:
Thread.run()should not be called directly.RSPEC-1217#43 (the rule this implements).
- Where:
-
A4
NoFinalizerdeletes finalizer logic and makesObjectFinalizeCallsSuperdead inCommonStaticAnalysisDetails
- Where:
NoFinalizer.java:52-64;common-static-analysis.yml:64,69. - Problem: the cleanup code in a
finalize()body is lost. Callers of apublic finalize()from other packages stop compiling, because the call now resolves to the protectedObject#finalize. CSA runsNoFinalizerfirst, soObjectFinalizeCallsSuper(D9) never acts. - Decision: which of the two should CSA run?
NoFinalizercould become search-only. - Related: ObjectFinalizeCallsSuper: declare the overridden finalize() exceptions when adding super.finalize() #969 and Widen
finalize()throws clauses across class hierarchies inObjectFinalizeCallsSuper#993 (openObjectFinalizeCallsSuperfixes).
- Where:
-
A5
CommonDeclarationSiteTypeVariances: theexcludeBoundskey is silently ignored (PROBED); key fixed in UseexcludedBoundsinCommonDeclarationSiteTypeVariances#1131, overrides part still openDetails
- Where:
static-analysis.yml:90saysexcludeBounds:, but the option isexcludedBounds(DeclarationSiteTypeVariance.java:51). - Problem:
java.lang.*is never excluded, soFunction<String, Integer>becomesFunction<? super String, ? extends Integer>.validateAll()reports the recipe as valid: unknown YAML option keys aren't flagged anywhere. That is worth raising in openrewrite/rewrite. - Also (Confirmed):
DeclarationSiteTypeVariancechanges an interface or base method signature while its@Overrideimplementations are skipped, so they stop compiling. TheoverriddenMethodstest (DeclarationSiteTypeVarianceTest:249) hides this withSourceSpec::skip. Not covered by Skip declaration-site variance for invariant storage #1007. - Fix: rename the key, skip overridable methods that have overrides in the source set, and remove the
skipfrom the test.
- Where:
-
A6 Decide on
CommonStaticAnalysisrecipes that change runtime values by designDetails
PreferSystemGetPropertyOverGetenv(:37-45):getenv("HOME")anduser.homediffer in containers, CI and withsudo -H.getenv("TMPDIR")is usuallynullon Linux whilejava.io.tmpdirnever is, soif (System.getenv("TMPDIR") != null)flips. There is no RSPEC rule behind it.BigDecimalDoubleConstructor(:24-47):BigDecimal.valueOf(1.0)has scale 1 wherenew BigDecimal(1.0)has scale 0, soequals,hashCodeandtoStringchange. The description says the value is preserved. Related: BigDecimalDoubleConstructorRecipe should also convert BigDecimal.valueOf(doubleLiteral) -> new BigDecimal(stringLiteral) #255.CaseInsensitiveComparisonsDoNotChangeCase(:56-67):"straße".toUpperCase().equalsIgnoreCase("STRASSE")istrue; the rewrite isfalse.UpperCaseLiteralSuffixes(:79-81): upper-casing every hex literal (0xff→0xFF) is beyond S818 and causes high-volume churn.WriteOctalValuesAsDecimal(:53-65,71-105): Goos.FileModeis exempt, but Java permission masks (TarArchiveEntry#setMode(0644)) become420.- Decision per recipe: keep and document, restrict, or drop from CSA.
-
A7 Scope follow-up visitors to what the recipe changed (PROBED for
InlineVariable) A7 + B6: scope follow-up visitors to what each recipe changed #1122 A7 + B6 follow-up: side effects, comments and formatting scope #1123Details
InlineVariable.java:82runsRemoveUnusedLocalVariablesover the whole file whenever it inlines one variable. So a recipe that is deliberately commented out of CSA (common-static-analysis.yml:79) runs anyway. Probe:void g() { int unused = 42; } int h(int a) { int r; r = a * 2; return r; }emptiesg(). Combined with B6,int unused = counter++;is removed with its increment.- The same pattern elsewhere (Confirmed): one match makes an unrelated recipe rewrite the whole file or class.
EmptyBlock:RemoveCallsToSystemGc:62,SimplifyConstantIfBranchExecution:59-69(alsoRemoveUnneededBlock)RemoveUnusedImports:RemoveMethodsOnlyCallSuper:122,UseLambdaForFunctionalInterface:147,RemoveUnusedPrivateMethods:105RemoveUnneededBlock:TernaryOperatorsShouldNotBeNested:84,97,AllBranchesIdentical:93SimplifyBooleanExpression:MoveConditionsToWhile:82; alsoSimplifyConstantIfBranchExecutioninAnnotateRequiredParameters:96-99EqualsAvoidsNull:StringLiteralEquality:63- Auto-format of the enclosing block or method:
UnwrapElseAfterReturn:88,UnnecessaryPrimitiveAnnotations:63-67,TypecastParenPad:61-66(CC)
- Fix: apply the follow-up to the changed subtree only (
maybeRemoveImportfor specific types, a cursor-bounded visitor,autoFormatof the new element). ForInlineVariable, remove only the declaration it emptied. - Related: EmptyBlock and RemoveMethodsOnlyCallSuper delete blocks and methods whose only content is a comment #1050 (
EmptyBlockdeleting comment-only blocks, which these file-wide runs spread).
-
A8
CovariantEqualsis@Incubating, runs inCommonStaticAnalysis, and crashes on abstract methods (PROBED) Add a delegatingequals(Object)inCovariantEquals(A8 of #1116) #1134Details
- Where:
CovariantEquals.java:88-128. - Problem:
abstract class Shape { public abstract boolean equals(Shape other); }throws anAssertionErrorat:115(an NPE without-ea).- The generated
getClass()check makes subclass instances unequal. - A subclass's
@Override equals(Point)in another file stops compiling. - A value-based
equalswithouthashCodeviolates the hash contract.
- Fix: bail out on a null body, use
instanceof, skip overridden hierarchies (or keep a delegating overload), and decide whether it stays in CSA.
- Where:
-
A9 Reachability: 115 of 205 recipes are reached by no composite in this repo
Details
- Wire after a scale run (small, typed, SAST-style, no consumer anywhere):
RemoveUnneededBlock,UnnecessaryFinalInTryWithResources,SillyEqualsCheck,RemoveUnnecessaryBitwiseOperation,SimplifyRedundantLogicalExpression(fix E4 first),RemoveDuplicateConditions,RemoveSelfAssignment,RemoveUnreachableMultiCatchAlternative,UseStringCaseInsensitiveOrderRecipe,SortedSetStreamToLinkedHashSet,StaticAccessViaInstance,CollapsibleIfStatements,UseMapEntrySetIteration,InterruptedExceptionHandling(E7), andUseMapContainsKey(fold the single-entryJavaApiBestPracticesinto CSA). - Bug-prevention rules consumed only by JBP that arguably belong in CSA:
ReplaceWeekYearWithYear,RemoveHashCodeCallsFromArrayInstances,RemoveToStringCallsFromArrayInstances,UseObjectNotifyAll,URLEqualsHashCodeRecipes,CompareEnumsWithEqualityOperator,MissingOverrideAnnotation. - Opt-in by design (style policies, opposing pairs, downgrades): say so in their descriptions, and don't wire them. Examples:
ExplicitThis,FinalizeLocalVariablesvsNoFinalizedLocalVariables,ReplaceTextBlockWithStringandRemoveInstanceOfPatternMatch(downgrades),ReorderAnnotationAttributes,SingleLineCommentSpacing,HiddenField,LowercasePackage,ReferentialEqualityToObjectEquals. - Search recipes with no composite:
FindMainWithThrowsClause,FindMissingJavadocOnPublicMethods,FindNewExceptionWithoutCause,FindNonCaseLabelsInSwitch,FindSystemAndRuntimeExitCalls,FindThreadGroupUsages,FindWaitWithMultipleLocksHeld. Group them in aFindStaticAnalysisIssuescomposite. ModernizeCollectionsis unreferenced; of its members onlyReplaceStackWithDequeis wired anywhere (JBP), and that one has the worst bugs (D1).- Related: AllBranchesIdentical to hoist side-effect statements #1032 and Preserve observable array accesses and casts in identical branches #995 (
AllBranchesIdentical), Java: Fix AddSerialVersionUidToSerializable for generic classes #1060 and AddSerialVersionUidToSerializable skips generic classes implementing Serializable #1055 (AddSerialVersionUidToSerializable); wiring those waits on these.
- Wire after a scale run (small, typed, SAST-style, no consumer anywhere):
-
A10
CommonStaticAnalysis,CodeCleanupandJavaBestPracticesoverlapDetails
- 11 recipes are listed in both CSA and CC, and the lists have drifted (
HideUtilityClassConstructor, A2). - JBP re-lists 7 CSA entries instead of including a shared composite, so runs that chain CSA and JBP visit
UnnecessaryParentheses,EqualsAvoidsNulland others twice. - Decision: have CC include a shared composite or document that CSA and CC are alternatives, and coordinate with rewrite-migrate-java.
- 11 recipes are listed in both CSA and CC, and the lists have drifted (
-
A11
ReplaceValidateNotNullHavingSingleArgWithObjectsRequireNonNullmatches every overload; move the Commons Lang recipes to rewrite-apacheDetails
- Where:
java-best-practices.yml:50-62. - Problem (Confirmed): the pattern is
notNull(..), so run on its own the recipe turnsValidate.notNull(o, "%s is null", n)intoObjects.requireNonNull(o, "%s is null", n), which doesn't exist. The parent composite only works because it lists the varargs recipe first. - Also (Confirmed): the varargs recipe moves message arguments into a lambda, so reassigned locals no longer compile (
ReplaceValidateNotNullHavingVarargsWithObjectsRequireNonNull:57-69). - Fix: use
notNull(java.lang.Object), eagerString.format, and move the trio to rewrite-apache'sPreferJavaStandardLibrary.
- Where:
B. CommonStaticAnalysis: statements and control flow
-
B1 Five recipes return
nullfor a statement in a brace-less body (PROBED) Don’t leave a null body when removing the only statement of a brace-less if or loop #1149Details
- Where:
RemoveCallsToSystemGc:61-65,RemoveSystemOutPrintln:70-77,RemoveCallsToObjectFinalize:62-66,RemoveSelfAssignment:49-55,RemoveUnneededAssertion:103-106. The first and third are in JBP; the others are unwired. - Problem: returning
nullonly works inside aJ.Block. As the body of a brace-lessif,elseor loop, the tree is left with a null body.if (low) System.gc(); c.clear();makes the follow-upEmptyBlockVisitor.visitIf(:108) throw an NPE. The test harness hides the cause, because its error handler NPEs onJ.If.toString()too.if (d) System.out.println(x); else log(x);becomesif (d) else log(x);.
- Same family:
SimplifyCompoundVisitor:33-36,52-55(return b &= true;→return ;), andLambdaBlockToExpression:56-61(() -> { return; }, CSA). - Fix: return an empty
J.Blockwhen the parent isn't a block, asRemoveUnnecessaryBitwiseOperation:128-130does. Bail out in expression position.
- Where:
-
B2
CatchClauseOnlyRethrows: inlining an emptiedtry(PROBED)Details
- Where:
CatchClauseOnlyRethrows.java:52-64,67-85. - Problem:
- Two sibling
try { String s = r.readLine(); … } catch (IOException e) { throw e; }blocks give duplicateString sdeclarations. - Under
case 1:the result is a baretry { r.close(); }. - Kotlin (PROBED):
val text = try { load() } catch (e: IOException) { throw e }becomesval text = try { load() }, which doesn't compile, and the removed import leaves a blank line. - Comments in the
try's prefix are dropped.
- Two sibling
- Fix: inline only when the body declares nothing and the parent is a block; otherwise keep a plain
{ … }block. For a Kotlintryexpression, replace it with its body when that is a single expression, and otherwise leave it.UnnecessaryCatch:66-78has the same inlining code (D6).
- Where:
-
B3
EmptyBlockdeletestry {} finally {…},synchronizedand switch selectors (PROBED) B3:EmptyBlockkeepsfinallystatements andswitchselector side effects, and leavessynchronizedalone #1117Details
- Where:
EmptyBlockVisitor.java:86-89,183-186,192-195; also in CC. - Problem:
try { } finally { r.run(); }is removed whole, sor.run()is lost.synchronized (lock) {}andswitch (sel()) {}are removed with their lock and selector side effects.
- Also (Confirmed,
:112-152,218-230): theif-inversion has theNaNflaw from C3, and side-effect extraction turnsif (m.find() && m.group(1) != null) {}intom.find(); m.group(1);, which throws when there's no match. - Fix: keep the
finallystatements, extract side effects as theifpath does, skip floating-point inversion, and keep&&/||intact. - Related: EmptyBlock and RemoveMethodsOnlyCallSuper delete blocks and methods whose only content is a comment #1050.
- Where:
-
B4
FallThroughadds unreachablebreaks, andbreakinside switch expressions (PROBED)Details
- Where:
FallThroughVisitor.java:183-225(noJ.Synchronized,J.DoWhileLooporJ.Yield),:279-293,:72-77(firstEnclosing(J.Switch.class)finds the outer switch); also in CC. - Problem:
case 1: synchronized (lock) { return 1; }gets an unreachablebreak;.- A colon-style switch expression nested in a switch statement gets
break;after eachyield(illegal) and afterreturn r;. - The recipe needs a second cycle; cycle 2 adds a second
break;at each place, becauseFindLastLineBreakscomparescase_ == scopeagainst the already-modified case. do { … } while (true)is also missed (Confirmed).
- Also (Confirmed, skip-only): an
if/elsecounts as breaking when only thethenbranch breaks (:188-191). - Related:
FallThroughshould not add break whenswitchis last statement in a method requiring areturn#229.
- Where:
-
B5
RedundantFileCreation:new FileInputStream(new File(uri))→new FileInputStream(uri)(PROBED) B5:RedundantFileCreationonly unwrapsnew File(String)#1124Details
- Where:
RedundantFileCreation.java:42-53. - Problem: any one-argument
new File(..)is unwrapped, includingFile(URI), butFileInputStreamhas noURIconstructor. Type validation doesn't catch it. - Fix: match
java.io.File <constructor>(java.lang.String)with aMethodMatcher.
- Where:
-
B6
RemoveUnusedLocalVariablesdrops++/--/compound-assignment side effects (PROBED) A7 + B6: scope follow-up visitors to what each recipe changed #1122 A7 + B6 follow-up: side effects, comments and formatting scope #1123Details
- Where:
RemoveUnusedLocalVariables.java:216-241(mightSideEffectonly flags invocations,newand assignments),:176-184. Reached from CSA throughInlineVariable(A7), though it is commented out there. - Probe:
int next() { int unused = counter++; return counter; }, in a file whereInlineVariablefires: the increment is gone. - Fix: use
SideEffects.mayHaveSideEffectsfor initializers and assignment references. - Also (Confirmed):
VariableReferences:88-142matches by simple name, socount = 5; int count = 0;with a fieldcountremoves the field write. Fix: comparegetFieldType(), and checkisModifying()invisitUnary. - Also (Suspected,
:192-212): removed comments are parked on the enclosing class and land on the next member. Without a class (a Groovy script)dropParentUntilthrows.
- Where:
-
B7
RemoveMethodsOnlyCallSuperremoves visibility-widening overrides (PROBED)Details
- Where:
RemoveMethodsOnlyCallSuper.java:196-220; in CC. - Problem:
widensVisibilityonly looks at the direct supertype's declared methods.class Sub extends Base { @Override public Object clone() … { return super.clone(); } }loses itspublic clone(), and callers ofs.clone()stop compiling. - Fix: compare against
TypeUtils.findOverriddenMethod(..). - Related: EmptyBlock and RemoveMethodsOnlyCallSuper delete blocks and methods whose only content is a comment #1050.
- Where:
-
B8
MinimumSwitchCasesevaluates the selector twice and retargets nestedbreaks (PROBED)Details
- Where:
MinimumSwitchCases.java:124,135,156and:95-102,286-301. - Problem:
switch (it.next()) { case 1: … case 2: … }becomesif (it.next() == 1) … else if (it.next() == 2) ….default: if (skip) break; b();inside aforloop keeps thebreak, which now exits the loop and skips the code after the switch.
- Also (Confirmed):
- The enum branch builds comparisons without parentheses.
- The default-only unwrap (
:59-73) drops the selector's side effects and can clash with later declarations. redeclareAssignments(:316-344) redeclares on every top-level assignment, sox = 2; x = 3;becomes twoint xdeclarations.
- Suspected: arrow cases with
defaultfirst (:80,116-157) passdefaultto the template. Groovycase String:(isCasesemantics) becomes==; only C# is excluded. - Fix: bail on selectors with side effects and on any non-trailing
break. For Groovy, only convert cases whose labels are literals, whereisCaseand==agree.
- Where:
-
B9
NoRedundantJumpStatementsremovescontinue outer;(PROBED)Details
- Where:
NoRedundantJumpStatements.java:56-80. - Problem: labels are ignored.
if (stop) { continue outer; } else { work(); }becomesif (!stop) { work(); }, so the inner loop keeps going. - Also (Confirmed,
:93-94): removing a trailingreturn;drops its comments, andUnnecessaryReturnAsLastStatement:59-62,75-76does the same. - Fix: require
getLabel() == null, and move comments to the block end.
- Where:
C. CommonStaticAnalysis: expressions and declarations
-
C1
NoValueOfOnStringType:String.valueOf(a) + String.valueOf(b)→a + b(PROBED) C1:NoValueOfOnStringTypekeeps oneString.valueOfwhen both concatenation operands arevalueOfcalls #1127Details
- Where:
NoValueOfOnStringType.java:110-120. - Problem: each operand sees the original parent binary, in which the other operand is still a
String-typedvalueOf, so both are removed. The string concatenation becomes int addition. - Fix: don't remove when the other operand is itself a removable
String.valueOf.
- Where:
-
C2
NoEqualityInForConditiondoesn't flip the operator when the variable is on the right (PROBED) C2:NoEqualityInForConditionflips the operator when the loop variable is on the right #1128Details
- Where:
NoEqualityInForCondition.java:61-70,90-101. - Problem:
for (int i = 0; 10 != i; i++)becomes10 < i, so the loop never runs. Matching is by simple name. - Fix: flip the operator or skip the case, and use
SemanticallyEqual.
- Where:
-
C3
BooleanChecksNotInvertedbreaks theNaNcheck (PROBED) Preserve NaN semantics when simplifying negated comparisons #1142Details
- Where:
BooleanChecksNotInverted.java:61-69; the same inversion is inEmptyBlockVisitor:131-152. - Problem:
if (!(f > 0)) throw …with adouble fbecomesif (f <= 0), which no longer rejectsNaN. That idiom is often deliberate. - Fix: skip relational operators with a floating-point or unknown operand.
- Where:
-
C4
EqualsAvoidsNullremoves the null check beforecontentEquals(PROBED) Keep the null check beforecontentEqualsinEqualsAvoidsNull#1133Details
- Where:
EqualsAvoidsNull.java:88-101with:48,140; also in CC and JBP. - Problem:
s != null && s.contentEquals("abc")becomes"abc".contentEquals(s), which throws an NPE fornull, unlikeequals. - Fix: keep the null check for
CONTENT_EQUALS.
- Where:
-
C5
NoDoubleBraceInitializationadds the collection as receiver to calls on the enclosing class (PROBED)Details
- Where:
NoDoubleBraceInitialization.java:142-170. - Problem:
new HashMap<>() {{ put(key(), "b"); }}becomesm.put(m.key(), "b"). The recipe also reformats the unrelatedkey()method. - Also (Confirmed,
:130-134,211-219): for method locals, the statements are spliced into the enclosing block, so a local declared inside the braces clashes with a later one. - Fix: add the select only when the declaring type is the collection supertype, and wrap or bail when the braces declare variables.
- Kotlin (Suspected):
object : HashMap<String, String>() { init { put(..) } }may match too; add a Kotlin test.
- Where:
-
C6
NoEmptyCollectionWithRawTypedoesn't compile under a cast (PROBED)Details
- Where:
NoEmptyCollectionWithRawType.java:80-107. - Problem:
(List<String>) Collections.EMPTY_LISTbecomes(List<String>) Collections.emptyList(). javac reportsList<Object>cannot be converted toList<String>, because a cast isn't a poly context. Type validation doesn't catch it. - Fix: use a type witness and drop the cast, or skip cast contexts.
- Where:
-
C7
NoPrimitiveWrappersForToStringOrCompareTo:new Float(d).toString()→Float.toString(d)(PROBED) Preserve Float double constructor conversions (1116 C7) #1143 (Float constructor fix; lambda concerns remain open)Details
- Where:
NoPrimitiveWrappersForToStringOrCompareTo.java:77-107. - Problem: a
doublepassed wherefloatis expected is a lossy-conversion error. The same applies tocompareTo→Float.compare. - Also (Confirmed,
LambdaBlockToExpression:51-82): trailing comments in a lambda block are dropped, andnew Task(() -> { compute(); })can switch to aCallableconstructor overload, because only method invocations are guarded. - Fix: bail on a narrowing argument, or insert a cast.
- Where:
-
C8
ExplicitInitializationtreats0x100000000Las zero (PROBED)Details
- Where:
ExplicitInitializationVisitor.java:106-111(intValue() == 0); also in CC. - Problem:
private long mask = 0x100000000L;becomesprivate long mask;. - Also (Confirmed): Lombok
@FieldDefaults(makeFinal = true)classes lose initializers that the generatedfinalfields need (:79-82,90). - Also (Suspected): C#
const int Zero = 0loses its required initializer. - Fix:
longValue() == 0, skip@FieldDefaults(makeFinal = true), and skipconst.
- Where:
-
C9
FinalizePrivateFieldsfinalizes fields read by initializers, and breaks Kotlin (PROBED) FixFinalizePrivateFieldsfor initializer reads, Kotlinvarand Groovy (C9) #1139Details
- Where:
FinalizePrivateFields.java:91-101,170-219(reads at depth 0 aren't tracked),:50(only JavaScript is excluded),:117-122,146; also in CC. - Java:
private int x; private int y = x + 1; A() { x = 5; }makes both fieldsfinal, soy's initializer reads an unassigned final. In CSAExplicitInitializationruns first and strips= 0, which makes this more likely. - Kotlin:
private var count = 0becomesprivate var val count = 0, becausevaris aLanguageExtensionmodifier andvalprints for anyFinal. - Groovy (Suspected): private fields aren't enforced, so outside writes hit a
ReadOnlyPropertyException. - Fix: exclude fields read directly in instance initializers. For Kotlin, replace the
varmodifier withvalinstead of appending aFinalone, and add a Kotlin test. For Groovy, skip fields that may be written dynamically, or skip Groovy if that can't be told apart.
- Where:
-
C10
UseDiamondOperatoradds a diamond to an anonymous class assigned to a wildcard type (PROBED) Keep type arguments of an anonymous class assigned to a wildcard type inUseDiamondOperator(C10 of #1116) #1161Details
- Where:
UseDiamondOperator.java:94-104,227-248. - Problem: on Java 9+,
c = new Consumer<String>() { public void accept(String s) {} }withConsumer<?> cbecomesnew Consumer<>() {…}, which infersObjectand no longer overrides. The argument path guards against wildcards (:130-143); the assignment path doesn't. - Fix: bail when a target type argument is a wildcard or type variable.
- Where:
-
C11
UseJavaStyleArrayDeclarationsgives every variable the array type (PROBED) C11:UseJavaStyleArrayDeclarationsonly rewrites declarations with matching dimensions #1132Details
- Where:
UseJavaStyleArrayDeclarations.java:49-81. - Problem:
int a, b[];becomesint[] a, b;, andfor (int i = 0, a[] = {1, 2, 3}; …)becomesfor (int[] i = 0, a = …).MultipleVariableDeclarationsonly splits block statements, soforinitializers reach this recipe unsplit in CSA. - Fix: only rewrite when every variable has the same number of dimensions.
- Where:
-
C12
ReplaceClassIsInstanceWithInstanceofproducesinstanceofthat doesn't compile (PROBED) SkipReplaceClassIsInstanceWithInstanceofon incompatible types or primitives (C12 of #1116) #1164Details
- Where:
ReplaceClassIsInstanceWithInstanceof.java:64-73,90-98; tests at:34-60(@DocumentExample) and:120-135. - Problem:
Integer.class.isInstance(s)withString sbecomess instanceof Integer(incompatible types), andString.class.isInstance(o.hashCode())becomeso.hashCode() instanceof String. The tests assert both. - Fix: skip primitives and provably unrelated types, and fix the tests.
- Where:
-
C13
ReplaceLambdaWithMethodReferencequalifiesthiswith a superclass inside nested lambdas (PROBED) Fix qualifiedthisresolution inReplaceLambdaWithMethodReference#1153Details
- Where:
ReplaceLambdaWithMethodReference.java:235-248; also in JBP. - Problem: the nearest
J.Lambdais treated as the owner ofthis.ls.forEach(l -> l.forEach(s -> log(s))), withloginherited fromBase, becomesl.forEach(Base.this::log), which doesn't compile. Own methods get a needlessA.this::m. - Fix: don't stop at lambdas; use
thiswhen the innermost class is assignable to the declaring type.
- Where:
-
C14
ReplaceStringBuilderWithStringdrops aCharSequenceseed and copies literal source text (PROBED) FixReplaceStringBuilderWithStringliteral conversion,CharSequenceseeds and Kotlin output #1150Details
- Where:
ReplaceStringBuilderWithString.java:173-183,95-118,127,168; also in CC and JBP. - Problem:
new StringBuilder(prefix).append("x")with aCharSequence prefixbecomes"x"..append('"').append(n)becomes""" + n, which doesn't compile;append(10L)becomes"10L".- Kotlin (PROBED):
StringBuilder().append(count).append(" items").toString()becomesString.valueOf(count) + " items";kotlin.Stringhas novalueOf.
- Fix: bail on non-
String, non-intconstructor arguments, escape converted literals or skip them, and emitcount.toString()(or a string template) in Kotlin. Preserve argument comments and ranged append behavior in string-builder conversion #994 covers argument comments and rangedappend, not these.
- Where:
-
C15
RenameMethodsNamedHashcodeEqualOrToStringgives weaker access, hides withstatic, and skips other files (PROBED)Details
- Where:
RenameMethodsNamedHashcodeEqualOrToString.java:62-72. - Problem: package-private
int hashcode()becomesint hashCode()(weaker access thanObject's), andstatic String tostring()becomesstatic String toString(). Both fail to compile. The rename runs throughdoAfterVisit, so callers in other files keep the old name (Confirmed). - Fix: only rename
publicinstance methods, and rename callers across files or bail. Avoid existing methods when renaming near-missObjectmethods #974 and Avoid non-compiling collisions when renaming near-missObjectmethods #987 cover name collisions, not these.
- Where:
-
C16
SimplifyArraysAsListunwraps primitive arrays (PROBED) SimplifyArraysAsList: do not unwrap primitive arrays (C16 from #1116) #1125Details
- Where:
SimplifyArraysAsList.java:60-68. - Problem:
Arrays.asList(new int[]{1, 2})is a one-elementList<int[]>. After the rewrite it isArrays.asList(1, 2), a two-elementList<Integer>. - Fix: require a reference component type.
- Where:
-
C17
SimplifyBooleanExpressionon KotlinBoolean?(PROBED) C17:SimplifyBooleanExpressionkeeps== trueon Kotlin calls returningBoolean?#1126Details
- Where:
SimplifyBooleanExpression.java:59-65. - Problem:
if (flag() == true)withfun flag(): Boolean?becomesif (flag()), a type mismatch. The method-invocation override returns before the nullable-type check from SimplifyBooleanExpression: skip nullable boolean comparisons in Kotlin #839. - Fix:
return !isNullSafe && super.shouldSimplifyEqualsOn(j);. - Related: SimplifyBooleanExpression ignore excessively deep boolean expressions #387.
- Where:
-
C18
SimplifyBooleanReturnreturns a non-boolean in Python (PROBED)Details
- Where:
SimplifyBooleanReturn.java:64-97;SimplifyBooleanReturnTest:341-378expectsreturn re.match(...). - Problem:
if items: return True/return Falsebecomesreturn items, which returns the list. Groovydefmethods and JS/TS have the same risk (Suspected). - Also (Confirmed,
:81,208-209): an emptyelse {}throwsIndexOutOfBoundsException. - Fix: require a
booleancondition type, and guard the empty block.
- Where:
-
C19
MethodNameCasingproduces duplicate methods and renamesnativemethods (PROBED)Details
- Where:
MethodNameCasing.java:99-127. - Problem: in main sources,
void Get_Value(){} void GetValue(){}gives twogetValue()methods, andprivate native void native_init();becomesnativeInit, which fails withUnsatisfiedLinkErrorat runtime. - Also (Confirmed): there is no override check, so a protected base method is renamed while its public override isn't.
- Not reproduced: a Kotlin
fun Get_Name()was not renamed with default options. - Fix: track claimed names per type, skip
nativemethods and overrides, and treat each override family all-or-nothing.
- Where:
-
C20
InlineVariablechanges semantics (Confirmed)Details
- Where:
InlineVariable.java:64-84; also in rewrite-rewrite's best practices. - Problem:
long v = 1; return v;in anObjectmethod becomesreturn 1;, which returns anInteger.Runnable r = () -> {}; return r;in anObjectmethod becomesreturn () -> {};, which doesn't compile.res = compute(); return res;withfinally { log(res); }now logs0.
- Fix: bail when the declared type differs from the initializer's, on lambdas and method references, and when the variable is read elsewhere.
- Related:
InlineVariableto handle more complex expressions too #803.
- Where:
-
C21 Smaller
CommonStaticAnalysisitems (Confirmed unless noted)Details
ForLoopIncrementInUpdate:84-88: inserts the moved increment in alphabetical order, sofor (…; count = i) { …; i++; }becomescount = i, i++; the@DocumentExampleasserts the sorting. It also drops the comment above the increment.FixStringFormatExpressions:97-103: dropping "unused" arguments drops side effects (n[0]++) and can change varargs binding for a trailing array.ExternalizableHasNoArgsConstructor:65,118-141: addspublic X() {}when the parent's no-arg constructor is private, to enums, and to records. In a Kotlin class with only secondary constructors it inserts a Java constructor (Suspected).FinalClassVisitor:73,136-137:final non-sealed class.CollectionToArrayShouldHaveProperType:139-141: NPE on an implicitthis(toArray()in anArrayListsubclass).ChainStringBuilderAppendCalls:110-115,182: comments on operands are dropped, and achar[]operand is now appended by content.MultipleVariableDeclarationsVisitor:52,62: comments after the commas are dropped.RemoveExtraSemicolons:73-83,113-123: comments on a trailing empty statement are dropped.UsePortableNewlines:75-89: nested format calls in the arguments are never converted.UnnecessaryCloseInTryWithResources:67-88: NPE on a field-access resource (try (this.in)); check against UnnecessaryCloseInTryWithResources: only remove a trailing close of a Closeable #978.ReplaceStringConcatenationWithStringValueOf:62-71: unwraps every parenthesizedString.valueOf, not only its own.StaticMethodNotFinal:51-53(Suspected): a double space whenfinalwas the first modifier.- Related: Keep
char[]and Groovy array operands out ofString.valueOfconversion #975 (ReplaceStringConcatenationWithStringValueOf), Preserve escaped backslash-newline text inUsePortableNewlines#1006 (UsePortableNewlines).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsBacklog
Activity
timtebeek commentedon Oct 25, 2026
Continued from the issue body.
D. Recipes reached from other composites
These run in
CodeCleanup, rewrite-migrate-java (JavaBestPractices,UpgradeToJava17/25, the JSpecify recipes), rewrite-rewrite or rewrite-java-security, so their bugs reach users even thoughCommonStaticAnalysisdoesn't include them.D1
ReplaceStackWithDeque: no escape, field or API guardDetails
ReplaceStackWithDeque.java:67-98; inModernizeCollectionsand JBP (java-best-practices.yml:89in rewrite-migrate-java).J.Return".while (!stack.empty())andconsume(stack)(withconsume(Stack<String>)) stop compiling;Dequehas noempty(),search()orListmethods.push(null)throws,peek()on an empty stack returnsnullinstead of throwing.Stack<String> s = new Stack<>(); s.push("a"); h.put("a", s.pop());throws aStackOverflowErrorin rewrite-analysis'sForwardFlow.visitBlocksRecursive, called fromFindLocalFlowPathsat:92. Raise it in openrewrite/rewrite-analysis, and catch it here so the file is skipped.return s.pop();counts as the stack escaping, because any node inside aJ.Returnis a sink, so that local isn't converted.ReplaceLegacyCollection(locals only, real escape checks, incompatible-method list), and decide on the order andpeekdifferences. Consider pulling it from JBP until then.Vector,Hashtable,StackandStringBuffershould not be used.RSPEC-1149#37.D2
RemoveRedundantTypeCast: ternary operands andvar(JBP)Details
RemoveRedundantTypeCast.java:76-82,142-143,224-236. JBP runs it afterUseVarForTypeCast.Number n = f ? (Number) i : dbecomesf ? i : d, which switches to numeric promotion (1 becomes 1.0) and unboxing (NPE fornull).var n = (Number) ibecomesvar n = i, narrowing the variable's type.var. Preserve raw cast on non-parameterized subtype argument inRemoveRedundantTypeCast#1109 covered a different shape.D3
RemoveCallsToObjectFinalizeis a no-op for almost every real call (JBP)Details
RemoveCallsToObjectFinalize.java:36,56,62-63; the three no-change tests inRemoveCallsToObjectFinalizeTest:72-166lock it in.UsesMethodwithoutmatchOverridesonly matchessuper.finalize(), and the visitor requires the declaring type's direct supertype to beObject.a.finalize()is only removed when the same file also has asuper.finalize().new MethodMatcher("java.lang.Object finalize()", true), excludingsuper.finalize()receivers, whichObjectFinalizeCallsSuperadds on purpose. Also fix B1 here.D4
OperatorWrapduplicates comments when re-wrapping (CC)Details
OperatorWrap.java:97-119, and the same pattern for bounds,instanceof, ternaries, member references, assignments and initializers.withWhitespace(" ")keeps the comments on the side the space was moved from, soa &&\n // why\n bprints the comment twice.OperatorWrapandWrappingAndBracescausing wrong result for Kotlin #302 looks fixed for Kotlin by 5545efa and could be closed.D5
InstanceOfPatternMatchreuses a name that is declared later (UpgradeToJava17)Details
InstanceOfPatternMatch.java:381-384skips the duplicate check for the EXACT strategy.if (o instanceof String) { if (f) { String s = (String) o; use(s); } String s = "other"; }gives "variable s is already defined". A field with the same name used in the condition is silently rebound. Not covered by InstanceOfPatternMatch changes code meaning with multiple reassignments #480 or Preserve casts after reassignment in InstanceOfPatternMatch #1074.D6
UnnecessaryThrowsandUnnecessaryCatchbreak callers and inlinetrybodies unsafelyDetails
UnnecessaryThrows.java:211-305(commented out of CSA; used by rewrite-migrate-java's Java 25 recipes);UnnecessaryCatch.java:66-78(rewrite-java-security).UnnecessaryCatchinlines a catch-lesstrythe same way as B2:case 1: try { helper(); } …leaves a baretry, and a declaration in the body can clash.UnnecessaryThrowstoprivatemethods, or document it as API-breaking. Share the fixed inlining with B2.D7
UseCollectionInterfacesbreaks overrides and calls to implementation-only methodsDetails
UseCollectionInterfaces.java:158-211,281-301,97-100; commented out of CSA, used by rewrite-migrate-java.@Overridewhose parameter type is fixed by a supertype in another file stops overriding.clone(),ConcurrentHashMap#mappingCount, the navigable methods ofConcurrentSkipListMap,CopyOnWriteArrayList#addIfAbsent,comparator()on sorted types, andVectormethods called on aStack.D8
UseTryWithResources: close exceptions now propagate, and the outercatchwidensDetails
UseTryWithResources.java:224-234,318-328,93-107,130-146; used by rewrite-migrate-java.try { in.close(); } catch (IOException e) {}infinallyis dropped, so the implicitclose()now throwsIOException, which the method may not declare.close()exceptions under the outertry'scatchclauses.tryCatchWrappedCloseandpreservesCatchBlocksmask both (the latter's input doesn't compile).tryhas no catches that could catch those exceptions.try-with-resourcesstatement #591.D9
ObjectFinalizeCallsSuper: unreachablesuper.finalize()and an NPE on abstract finalizersDetails
ObjectFinalizeCallsSuper.java:58-82; in CSA, but dead behindNoFinalizer(A4).return/throw, which is an unreachable statement.finalize()throws an NPE.x.finalize()call counts as asupercall.finalize()throws clauses across class hierarchies inObjectFinalizeCallsSuper#993.D10
UnwrapElseAfterReturn: a middleelse ifthat falls through now also runs the finalelse(rewrite-rewrite)Details
UnwrapElseAfterReturn.java:58-71; commented out of CSA; the test at:431-466encodes the bug with input that doesn't compile.if (a) {return;} else if (b) {x();} else if (c) {return;} else {y();},y()now also runs whenbis true.return/throw. UnwrapElseAfterReturn: only unwrap when else declarations do not collide #979 covers declaration collisions, not this.D11
SimplifyConstantIfBranchExecution: class-wide cleanups, and ternaries lose their parenthesesDetails
SimplifyConstantIfBranchExecution.java:59-69,149-152; used byAnnotateRequiredParametersand other repos.EmptyBlockandRemoveUnneededBlockrun over the whole class (this also reaches EmptyBlock and RemoveMethodsOnlyCallSuper delete blocks and methods whose only content is a comment #1050).visitTernaryalways returns the cleaned-up result, so(c) ? 1 : 2becomesc ? 1 : 2even when nothing is constant.RemoveUnreachableCodeVisitor:48-61): only a directreturn/throw/break/continuecounts as a jump, soif (true) { if (c) return 1; else return 2; } return 3;likely leaves an unreachablereturn 3;.D12 Smaller items in recipes that JBP and other repos consume (Confirmed unless noted)
Details
UseStandardCharset:74-80:Charset.forName("UTF 8")throwsIllegalCharsetNameExceptionout of the recipe; onlyUnsupportedCharsetExceptionis caught.return methodat:79discards the visited changes.UseStringReplace:94-98: the new value is fully re-escaped, so"é"becomes"é"and text blocks become plain strings. Decide whether to wire it (Use lambdas where possibleleads to ambiguity in WireMockXml.java#10, cited in CSA, is closed).URLEqualsHashCode:41-49,59-67:URI.create(..)throws for URLs that aren't valid URIs (spaces), anda.equals(null)now throws an NPE.UseSystemLineSeparator:51-80:lineSeparator()ignores a runtimeSystem.setProperty("line.separator", ..); document it.ReplaceWeekYearWithYear:76-78: the literal is rebuilt from its unescaped value, so"YYYY\\MM"becomes"yyyy\MM".RemoveSystemOutPrintln:70-75:System.out.println(q.poll())is removed with its side effect.RemoveDuplicateAnnotations:64-103(Suspected): duplicates split across the leading annotations and the return type (@Nullable public @Nullable String m()) aren't found, which is the JSpecify migration case.CompareEnumsWithEqualityOperator:53-57,79-85:!both(c.equals(RED), f)becomesboth(c != RED, f), ande.equals("RED")becomese == "RED".EqualsToContentEquals:67-68: the renamed call keeps theequalsmethod type.AddSerialAnnotationToSerialVersionUID:58-63: a non-Serializableouter class skips its nestedSerializableclasses.AnnotateRequiredParameters:222-235,353-372: conditionalrequireNonNullcalls (in a lambda orcase) mark parameters required, and then become a baresor() -> s. The description doesn't say that runtime null guards are removed.AnnotateNullableMethods:271-279: propagation only recognisesJ.AnnotatedType, so declaration annotations likeCheckForNulldon't propagate.AnnotateNullable*(Suspected): Kotlin value parameters and varargs get the annotation in the wrong position.java/MoveFieldAnnotationToType:52-59,142-145: annotations are moved whatever their@Target; the defaultorg.openrewrite..*moves@Optioninto type position, which doesn't compile.RemoveUnusedPrivateFields:38,64-67,80-83,115-117(rewrite-rewrite):@lombok.*matches one package segment, so@lombok.experimental.SuperBuilderclasses lose fields; side-effecting initializers are deleted;maybeRemoveImportgets a generic signature.RemoveUnusedPrivateMethods:56-69:@SuppressWarnings({"unchecked", "unused"})andvalue = "unused"aren't recognised.OnlyCatchDeclaredExceptions:65-142(rewrite-java-security): collects thrown types from nested lambdas andtrys, ignoresthrow, and emitscatch (FileNotFoundException | IOException e), which doesn't compile.BufferedWriterCreation:66-98(rewrite-java-security):FileWriter(f, false)maps toCREATE, which doesn't truncate;FileWriter(f, true)maps toAPPENDwithoutCREATE; the charset changes to UTF-8; the description's leak claim is wrong, and it cites CWE-755 rather than CWE-775.MaskCreditCardNumbers:38-54(rewrite-java-security): masks any 16-digit literal (no Luhn check) and re-delimits text blocks.CombineSemanticallyEqualCatchBlocks:54(Suspected): generates multi-catch in Kotlin and C#, which have none.:290-1816is a roughly 1500-line copy ofSemanticallyEqualthat only adds comment comparison.URLEqualsHashCode), CombineSemanticallyEqualCatchBlocks expects too much when comparing body clauses #506 and CombineSemanticallyEqualCatchBlocks indexes J.Case#getCaseLabels() unguarded, and only ever compares the first label #985 (CombineSemanticallyEqualCatchBlocks).E. Standalone recipes: fix before wiring
None of these are in a composite (see A9), so they only hurt users who run them on purpose. Fix them before wiring any into CSA.
E1
TernaryOperatorsShouldNotBeNested: the switch conversion loses null safety and emits invalid switchesDetails
TernaryOperatorsShouldNotBeNested.java:207-216,328-400. The@DocumentExample(test:955) shows the null problem."a".equals(s) ? …returnsfalsefornull, butswitch (s)throws an NPE.longselectors produce invalid switches.i == f()becomescase f() ->.isConstantat:399is inverted, so non-constant labels are accepted.case Color.REDis illegal before Java 21.var.equals(CONST)form only, and unqualified enum constants.E2
RemoveInstanceOfPatternMatchremoves bindings that are still usedDetails
RemoveInstanceOfPatternMatch.java:93,443-463; the@Disabledtests atRemoveInstanceOfPatternMatchTest:525,564expect wrong output.if's or ternary's scope.if (!(o instanceof String s)) return 0; return s.length();leavessundeclared.return o instanceof String s && s.isEmpty();.if, the cast is hoisted unconditionally and throwsClassCastException.instanceofalone. Skip deconstruction patterns.E3 Arithmetic and equality rewrites that change results
Details
PreferIncrementOperator:97-133(Confirmed):x = x + a + bbecomesx += a + b, which regroups the operands.s = s + 1 + 2gives"s3"instead of"s12", and alongaccumulator overflows inint. Restrict it to same-typed integral operands.PreferEqualityComparisonOverDifferenceCheck:47-50,87-92(Confirmed):a - b == 0becomesa == bonIntegers, which compares references.ReferentialEqualityToObjectEquals:60-68,106-111(Confirmed):a + b == cbecomesa + b.equals(c); there is no parenthesization. In Kotlin,==already callsequals, so the rewrite should leave Kotlin alone rather than producea.equals(b), which doesn't compile for nullable types.SimplifyConsecutiveAssignments:69-74,181-200(Confirmed):x = y; x += x;becomesx = y + x;int i = 10; i *= 1.5;becomesint i = 10 * 1.5;, which doesn't compile.E4
SimplifyRedundantLogicalExpressiondrops parentheses that are neededDetails
SimplifyRedundantLogicalExpression.java:60.(x || y) && (x || y) && zbecomesx || y && z.ParenthesizeVisitor.E5 Statement-removing recipes that ignore reads and side effects
Details
RemoveUnconditionalValueOverwrite:81-111(Confirmed):map.put("k", 1); map.put("k", map.get("k") + 1);loses the firstput, so the second throws an NPE.a[i] = 1; a[i] = a[i] + 1;has the same problem.UseForEachRemoveInsteadOfSetRemoveAll:42-65(Confirmed):changed |= s.removeAll(c),check(s.removeAll(c))andx -> x.removeAll(c)becomevoidforEachcalls, which don't compile.s.removeAll(s)would throwConcurrentModificationException.RemoveSelfAssignment:49-55(Suspected):arr[i++] = arr[i++]is removed with its side effects; a Python class-bodyx = xbinds from the enclosing scope.RemoveDuplicateConditions:89-133(Confirmed): two duplicates in one chain need two cycles.E6
UseLambdaForFunctionalInterface: self-recursion and default-method calls breakDetails
UseLambdaForFunctionalInterface.java:489-571; commented out of CSA (cite UseLambdaForFunctionalInterface: keep the receiver of implicit getClass() #980, notUse lambdas where possibleleads to ambiguity in WireMockXml.java#10).n * of(n - 1)) or to an interface default method resolves against the enclosing class once inside a lambda, so the result doesn't compile or calls something else.RemoveUnusedImports(A7);isAcceptableadmits Kotlin and Groovy; the"stop"cursor message at:690-703is a no-op, so variables declared after the anonymous class block the conversion.E7 Smaller standalone items (Confirmed unless noted)
Details
StaticAccessViaInstance:62-67,81-86: qualifies with a package-private base type from another package; nested types are imported asOuter$Inner(Suspected).UseAsBuilder:164-171,86-145,78: theimmutablecheck is inverted, statements between builder calls are hoisted above the declaration, andmax(Integer::sum)is not a comparator.SingleLineCommentSpacing:52-66:/// Returns x.becomes// / Returns x., and//go:build linuxbecomes// go:build linux, which disables the build constraint.MergeIdenticalBranches:74-84:a ? b : c || dwithout parentheses, and pattern bindings that go out of scope.CollapsibleIfStatements:77-90:if (a && b ? c : d).AllBranchesIdentical:93: comments are dropped, and Kotlin if-expressions are suspect.ReplaceTextBlockWithString:56-84:"""…""".formatted(a, b)becomes"…" + "…".formatted(a, b). Kotlin raw strings are converted too, with the same precedence bug around.trimIndent(); the description and the S5663 tag talk about single-line blocks, but every block is converted.ReplaceStringBufferWithStringBuilder/ReplaceLegacyCollection:190-226:return sb.append("x")and array stores escape undetected.ReplaceVectorWithArrayList:32-35: two-argumentindexOf/lastIndexOfdon't exist onArrayList.ReplaceCollectionToArrayArgWithEmptyArray:60-91:List/Setreceivers never match (nomatchOverrides),new String[10]becomes[0], and a boxed dimension throwsClassCastException.ReplaceDuplicateStringLiterals:213-231: reuses a nested class's constant in the outer class, where it isn't in scope.ReplaceOptionalIsPresentWithIfPresent:153-160: constructors that throw checked exceptions are moved into the lambda.SillyEqualsCheck:78-86:Arrays.equals(int[], long[])doesn't exist, andx.equals(null)→x == nullturns an NPE intotrue.ReorderAnnotationAttributes:68-72: the last attribute's trailing newline moves into the middle of the list.RenamePrivateFieldsToCamelCase:115-145: silently addsstatic, which breaks blank finals in multi-variable declarations and changes the defaultserialVersionUID.NoFinalizedLocalVariables:44-83andFinalizeLocalVariables:43,82-85: Kotlinval x = 1becomesx = 1, andvar xbecomesvar val x; removingfinalfrom a constant used as acaselabel breaks Java.ExplicitLambdaArgumentTypes:62-160: mixes implicit and explicit parameters, doesn't import parameterized types, and mangles Kotlin lambdas.HiddenFieldVisitor:85-96,194-256: renames inner-class fields, not just locals and parameters, which breaks references from other files.FindMissingJavadocOnPublicMethods:48-52,102-122: reports every documented Kotlin or Groovy method, because neither parser createsJavadoc.DocComment.MoveConditionsToWhile:82,LowercasePackage:74-93(other packages' imports aren't updated),InterruptedExceptionHandling:60(re-interrupts before a rethrow),MemberNameCaseInsensitiveDuplicates,UseMapEntrySetIteration:119-179(a supertype-typed loop variable gets the narrower key type, which changes overload resolution),maven/MavenJavadocNonAsciiRecipe:56-73(deletes CJK, Cyrillic and Greek text outright),SimplifyDurationCreationUnits:112-140(folds an overflowingintproduct),SimplifyForLoopBoundaryComparison:94-95(c <= 'z'→c < 'z' + 1),UnwrapRepeatableAnnotations:102-130,SimplifyElseBranch:51-71(end-of-block comments),ExplicitThis:226-235(Suspected:1Local.this),FindNewExceptionWithoutCause:122,210-230(new X(e.getMessage())counts as preserving the cause),ExplicitCharsetOnStringGetBytes:37-42("UTF-8"givesStandardCharsets.UTF-8),AddSerialVersionUidToSerializable:98-118(replaces an existing field's modifiers wholesale),RemoveEmptyJavaDocParameters:203-208.MergeIdenticalBranches,AllBranchesIdentical), org.openrewrite.staticanalysis.ReplaceDuplicateStringLiterals creates unnecessary duplicates #351 (ReplaceDuplicateStringLiterals),RenamePrivateFieldsToCamelCasemisses cases when method parameters with target name exist #186, CommonStaticAnalysis recipe does not change calls to getter/setters on camel casing changes. #189,RenamePrivateFieldsToCamelCaseshould not rename annotated fields, similar toFinalizePrivateFields#254 (RenamePrivateFieldsToCamelCase),SimplifyElseBranchputs comment in not the best location #878 (SimplifyElseBranch), Synchronized classesVector,Hashtable,StackandStringBuffershould not be used.RSPEC-1149#37 (legacy collections), Recipe:staticmember should be accessed viaclassreference instead ofinstancereference #74 (StaticAccessViaInstance), prefermap.entrySet()over manual iteration of thekeySetand manual access to the values #77 (UseMapEntrySetIteration), AddSerialVersionUidToSerializable skips generic classes implementing Serializable #1055, Java: Fix AddSerialVersionUidToSerializable for generic classes #1060 (AddSerialVersionUidToSerializable).F. Open questions & coverage
F1 Tests that assert wrong output
Details
IndexOfChecksShouldUseAStartPositionTest:31-50(A1),ReplaceClassIsInstanceWithInstanceofTest:34-60,120-135(C12),ForLoopIncrementInUpdateTest:32-58(C21),SimplifyBooleanReturnTest:341-378(C18),UnwrapElseAfterReturnTest:431-466(D10),UseTryWithResourcesTest:145-181(D8),RemoveCallsToObjectFinalizeTest:72-166(D3),DeclarationSiteTypeVarianceTest:249(A5),LambdaBlockToExpressionTest:139-140,173-174(odd() ->\n foo())wrapping).CommonStaticAnalysisIssuesPerformanceTest, see M12 in Recipe quality triage: mechanical polish (M1–M16) #1115). 48 recipes inCommonStaticAnalysisandCodeCleanuphave no Kotlin or Groovy test, which is how B2, C9, C14 and C17 went unnoticed.RedundantFileCreationTest(one test),RemoveSystemOutPrintlnTest,PreferSystemGetPropertyOverGetenvTest,ExplicitCharsetOnStringGetBytesTest,StaticMethodNotFinalTest,UseAsBuilderTest,IsEmptyCallOnCollectionsTest.overrideandcompanionkeywords #151,OperatorWrapandWrappingAndBracescausing wrong result for Kotlin #302, CodeCleanup: Compilation failure usingas-statement SimpleAssociation<*, *> #446 (earlier Kotlin breakages fromCommonStaticAnalysisandCodeCleanup).F2 Escalate upstream
Details
validateAll()(A5). Raised in Report unknown declarative recipe options as validation failures rewrite#9393.J.Ifwith a null body, the test harness's error handler NPEs onJ.If.toString(), which hides the real exception (B1).new FileInputStream(uri),Float.toString(d), the cast in C6), so "tests pass" says little about whether the output compiles. Consider compiling the after-sources inRewriteTestwhere the classpath allows it.F3 Validate at scale before rewiring
Details
CommonStaticAnalysison a platform organisation before and after the A decisions. Measure churn fromfix.patchor theSourcesFileResultsdata table, not fromgit diff, sincemod rundoesn't change the working tree.Match `VariableReferences` with `SemanticallyEqual`
B3: `EmptyBlock` keeps `finally` statements and `switch` selector sid…
Tell fields from locals in `VariableReferences` so a shadowed field i…
Remove only the declaration `InlineVariable` leaves unused instead of…
Remove only the imports of the removed code in `RemoveMethodsOnlyCall…
Inline only the block `AllBranchesIdentical` and `TernaryOperatorsSho…
Simplify only the new loop condition in `MoveConditionsToWhile`
Put the literal first when `StringLiteralEquality` builds the `equals…
Stop `TypecastParenPad` formatting after the cast parentheses, leavin…
Remove only the blocks and `if`s that `RemoveCallsToSystemGc` empties
Simplify only the statements whose null checks `AnnotateRequiredParam…
Match `VariableReferences` with `SemanticallyEqual`
7 remaining items
tashen247 commentedon Oct 26, 2026
I’ll take B9 (labelled continue in NoRedundantJumpStatements, plus the dropped comments in it and UnnecessaryReturnAsLastStatement); PR coming today
Fix `FinalizePrivateFields` for initializer reads, Kotlin `var` and G…
Add a delegating `equals(Object)` in `CovariantEquals` instead of rew…
Keep labelled `continue` and the comments of a removed `return;` (B9 …
Retain raw empty collections used as cast operands (#1155)
Preserve value comparison for boxed subtraction operands (#1157)
Preserve casts nested in variable initializers (#1156)
Niloyyy commentedon Oct 28, 2026
I've opened PRs for two items, in case you'd like to tick them:
ReplaceLambdaWithMethodReference: Fix qualifiedthisresolution inReplaceLambdaWithMethodReference#1153UseDiamondOperator: Keep type arguments of an anonymous class assigned to a wildcard type inUseDiamondOperator(C10 of #1116) #1161Niloyyy commentedon Oct 28, 2026
ReplaceClassIsInstanceWithInstanceof: SkipReplaceClassIsInstanceWithInstanceofon incompatible types or primitives (C12 of #1116) #1164Skip `ReplaceClassIsInstanceWithInstanceof` on incompatible types or …
Keep type arguments of an anonymous class assigned to a wildcard type…
Niloyyy commentedon Oct 31, 2026
SimplifyRedundantLogicalExpression: Keep parentheses needed for precedence inSimplifyRedundantLogicalExpression(E4 of #1116) #1178Niloyyy commentedon Oct 31, 2026
CollectionToArrayShouldHaveProperType: Handle implicitthisinCollectionToArrayShouldHaveProperType(part of C21 of #1116) #1179Niloyyy commentedon Nov 1, 2026
InlineVariable: Avoid changing semantics inInlineVariable(C20 of #1116) #1180