RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides - #971
Merged
timtebeek merged 4 commits intoAug 11, 2026
Conversation
An override whose body only forwards to `super` is redundant only when the declaration itself carries no contract. Two kinds that do were being removed. A `synchronized` override acquires the receiver monitor before dispatching to a super method that need not be synchronized itself, so removing it drops the lock. `@Deprecated` was explicitly allowlisted, even though it is metadata owned by the subclass declaration that drives a deprecation diagnostic at every call site. Skip declarations carrying the `Synchronized` modifier, and stop allowing `java.lang.Deprecated`, leaving `@Override` as the only permitted annotation. Annotations are now read through `AnnotationService#getAllAnnotations` rather than `getLeadingAnnotations()`, because an annotation written after a modifier keyword, as in `public @deprecated void foo()`, is held by the `J.Modifier` and the leading list missed it. That also pulls annotations on an annotated return type into the guard, a deliberate widening in the same conservative direction; parameter, type-parameter and nested type-use annotations are still not covered. The existing test `removeDeprecatedMethodOnlyCallingSuper` asserted the removal this change prevents, so it is inverted and renamed `doNotChangeDeprecatedMethod`. The recipe therefore no longer cleans up forwarding overrides whose only extra marking is `@Deprecated`.
4 tasks
mergify Bot
added a commit
to robfrank/linklift
that referenced
this pull request
Aug 20, 2026
… 2.40.0 to 2.41.0 [skip ci] Bumps [org.openrewrite.recipe:rewrite-static-analysis](https://github.com/openrewrite/rewrite-static-analysis) from 2.40.0 to 2.41.0. Release notes *Sourced from [org.openrewrite.recipe:rewrite-static-analysis's releases](https://github.com/openrewrite/rewrite-static-analysis/releases).* > 2.41.0 > ------ > > What's Changed > -------------- > > * Pre-install the JavaScript RPC npm package before running tests by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#956](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/956) > * UnnecessaryExplicitTypeArguments: retain witness for a return-only ty… by [`@neil-mushell`](https://github.com/neil-mushell) in [openrewrite/rewrite-static-analysis#958](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/958) > * Do not delete expressions that may have side effects by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#959](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/959) > * OpenRewrite recipe best practices by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#960](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/960) > * UseLambdaForFunctionalInterface: only convert when the anonymous class implements the SAM by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#962](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/962) > * ReplaceStringBuilderWithString: wrap `char[]` appends in `String.valueOf` by [`@martinfrancois`](https://github.com/martinfrancois) in [openrewrite/rewrite-static-analysis#977](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/977) > * RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides by [`@martinfrancois`](https://github.com/martinfrancois) in [openrewrite/rewrite-static-analysis#971](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/971) > * Make sure ModifierOrder doesn't alter Python's def quasi-modifier by [`@greg-at-moderne`](https://github.com/greg-at-moderne) in [openrewrite/rewrite-static-analysis#981](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/981) > * NullableOnMethodReturnType: only move annotations applicable to TYPE\_USE by [`@martinfrancois`](https://github.com/martinfrancois) in [openrewrite/rewrite-static-analysis#968](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/968) > * `FallThrough` to work only for Java files by [`@greg-at-moderne`](https://github.com/greg-at-moderne) in [openrewrite/rewrite-static-analysis#982](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/982) > * Retry a failed npx warm, and survive a machine without Node by [`@timtebeek`](https://github.com/timtebeek) in [openrewrite/rewrite-static-analysis#984](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/984) > * Make `DefaultComesLast` apply only to Java by [`@greg-at-moderne`](https://github.com/greg-at-moderne) in [openrewrite/rewrite-static-analysis#983](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/983) > > New Contributors > ---------------- > > * [`@martinfrancois`](https://github.com/martinfrancois) made their first contribution in [openrewrite/rewrite-static-analysis#977](https://redirect.github.com/openrewrite/rewrite-static-analysis/pull/977) > > **Full Changelog**: <openrewrite/rewrite-static-analysis@v2.40.0...v2.41.0> Commits * [`43b51de`](openrewrite/rewrite-static-analysis@43b51de) Make DefaultComesLast apply only to Java ([#983](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/983)) * [`5d08edd`](openrewrite/rewrite-static-analysis@5d08edd) Retry a failed npx warm, and survive a machine without Node ([#984](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/984)) * [`49ddf78`](openrewrite/rewrite-static-analysis@49ddf78) FallThrough to work only for Java files ([#982](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/982)) * [`9699a7e`](openrewrite/rewrite-static-analysis@9699a7e) NullableOnMethodReturnType: only move annotations applicable to TYPE\_USE ([#968](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/968)) * [`0c6f970`](openrewrite/rewrite-static-analysis@0c6f970) Make sure ModifierOrder doesn't alter Python's def quasi-modifier ([#981](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/981)) * [`6ef24c5`](openrewrite/rewrite-static-analysis@6ef24c5) RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides ([#971](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/971)) * [`0d8f7d7`](openrewrite/rewrite-static-analysis@0d8f7d7) ReplaceStringBuilderWithString: wrap `char[]` appends in `String.valueOf` ([#977](https://redirect.github.com/openrewrite/rewrite-static-analysis/issues/977)) * [`9a15a92`](openrewrite/rewrite-static-analysis@9a15a92) OpenRewrite recipe best practices * [`a394f54`](openrewrite/rewrite-static-analysis@a394f54) UnnecessaryExplicitTypeArguments: regression test for witness on varargs meth... * [`0ecb6d8`](openrewrite/rewrite-static-analysis@0ecb6d8) Update Gradle wrapper to 9.7.0 * Additional commits viewable in [compare view](openrewrite/rewrite-static-analysis@v2.40.0...v2.41.0) [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's changed?
RemoveMethodsOnlyCallSupernow keeps an override that is
synchronized, and keeps an override that carries any annotation otherthan
@Override, including@Deprecated. It also reads the visitedJ.MethodDeclaration'sannotations through
service(AnnotationService.class).getAllAnnotations(getCursor())instead ofJ.MethodDeclaration#getLeadingAnnotations(): an annotation written after a modifier keyword, as inpublic @Deprecated void foo(), is held by the modifier rather than by the declaration, and onlygetAllAnnotationsreports it.getAllAnnotationsitself is unchanged; only this call site is new.Input, which main deletes, leaving an empty
Child, and which this branch leaves unchanged:What's your motivation?
The recipe decides that an override is redundant from its body: one statement, a call on
superor areturnof one, the called method having the same name, and every argument an identifier naming theoverride's own parameter in the same position. But a declaration can hold meaning that its body does
not.
Deleting a
synchronizedoverride drops the acquisition of the monitor on the receiver, and nothingputs it back, because the parent method the override delegates to need not be
synchronizeditself:in
doNotChangeSynchronizedMethodthe parent declares a plainvoid foo(). Acquiring and releasingthat monitor are the lock and unlock actions that the Java memory model orders between threads
(JLS 17.4.2), so once
the override is deleted
Thread.holdsLock(this)inside the parent body isfalsewhere it wastrue, and concurrent calls are no longer serialized.Deleting a
@Deprecatedoverride removes the javac-Xlint:deprecationwarning at call sites typedas
Child, and removes the annotation fromChild's declared methods, where@Deprecatedisretained at runtime and visible to reflection. Reproduced on 2.40.0 and on current main.
Anything in particular you'd like reviewers to focus on?
This change alters an expectation that used to hold. Main asserted, in
removeDeprecatedMethodOnlyCallingSuper, that@Deprecated @Override void foo() { super.foo(); }isdeleted. That test name is gone. The same source is now the input of the new
doNotChangeDeprecatedMethod, which asserts that nothing changes, and the name is replaced byremovePlainOverrideAlongsideSynchronizedAndDeprecatedOverrides, which holds asynchronizedoverride, a
@Deprecatedoverride and a plain@Overrideside by side in one class and expects onlythe plain one to be removed.
removeMethodWithOverrideAnnotationAfterModifierchecks thatpublic @Override void foo()is stillremoved. What decides is which annotation it is,
@Overrideor anything else, not where in thedeclaration it is written.
Three limits. The first is new behaviour this change introduces; the other two are the same on main
and on this branch:
getAllAnnotationsalso reports an annotation on an annotated return type, so an override declaredas
public @Nullable String foo()is now kept as well. That goes beyond the two motivating cases,and it is intended: any annotation other than
@Override, in any position the recipe can see it,now stops the removal.
seen, so
void foo(@Nullable String s)andpublic String @Nullable [] foo()are still removed.strictfpoverride is still removed, which only matters on Java releases before 17.Have you considered any alternatives or workarounds?
An existing test asserted that a
@Deprecatedoverride is removed, so that behaviour was a choiceand not an oversight. Main lets an override be removed while it carries either of two annotation
types,
java.lang.Overrideandjava.lang.Deprecated, and this change narrows that set tojava.lang.Overridealone. To go on removing@Deprecatedoverrides, restore the second conditioninside the loop over the method's annotations,
!TypeUtils.isOfClassType(annotationType, "java.lang.Deprecated"), and drop the@Deprecatedcasesfrom the tests:
doNotChangeDeprecatedMethod,doNotChangeDeprecatedMethodWithAnnotationAfterModifier,and the
@Deprecatedoverride insideremovePlainOverrideAlongsideSynchronizedAndDeprecatedOverrides. Thesynchronizedguard isindependent of that decision and works either way.
Any additional context
This change adds 5 tests to
RemoveMethodsOnlyCallSuperTestand removes the nameremoveDeprecatedMethodOnlyCallingSuper, as described above. Without the code change in this pullrequest, 4 of the added tests fail:
doNotChangeSynchronizedMethod,doNotChangeDeprecatedMethod,doNotChangeDeprecatedMethodWithAnnotationAfterModifierandremovePlainOverrideAlongsideSynchronizedAndDeprecatedOverrides. The fifth,removeMethodWithOverrideAnnotationAfterModifier, passes either way: main sees no annotation thereat all, since the annotation follows a modifier, and an
@Overridedoes not stop the removal oneither side.
This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and
this description.
Checklist