Skip to content

RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides - #971

Merged
timtebeek merged 4 commits into
openrewrite:mainfrom
martinfrancois:fix/remove-methods-only-call-super-contracts
Aug 11, 2026
Merged

RemoveMethodsOnlyCallSuper: keep synchronized and deprecated overrides#971
timtebeek merged 4 commits into
openrewrite:mainfrom
martinfrancois:fix/remove-methods-only-call-super-contracts

Conversation

@martinfrancois

Copy link
Copy Markdown
Contributor

What's changed?

RemoveMethodsOnlyCallSuper
now keeps an override that is synchronized, and keeps an override that carries any annotation other
than @Override, including @Deprecated. It also reads the visited J.MethodDeclaration's
annotations through service(AnnotationService.class).getAllAnnotations(getCursor()) instead of
J.MethodDeclaration#getLeadingAnnotations(): an annotation written after a modifier keyword, as in
public @Deprecated void foo(), is held by the modifier rather than by the declaration, and only
getAllAnnotations reports it. getAllAnnotations itself is unchanged; only this call site is new.

Input, which main deletes, leaving an empty Child, and which this branch leaves unchanged:

class Child extends Parent {
    @Override
    synchronized void foo() {
        super.foo();
    }
}

What's your motivation?

The recipe decides that an override is redundant from its body: one statement, a call on super or a
return of one, the called method having the same name, and every argument an identifier naming the
override's own parameter in the same position. But a declaration can hold meaning that its body does
not.

Deleting a synchronized override drops the acquisition of the monitor on the receiver, and nothing
puts it back, because the parent method the override delegates to need not be synchronized itself:
in doNotChangeSynchronizedMethod the parent declares a plain void foo(). Acquiring and releasing
that 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 is false where it was
true, and concurrent calls are no longer serialized.

Deleting a @Deprecated override removes the javac -Xlint:deprecation warning at call sites typed
as Child, and removes the annotation from Child's declared methods, where @Deprecated is
retained 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(); } is
deleted. 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 by
removePlainOverrideAlongsideSynchronizedAndDeprecatedOverrides, which holds a synchronized
override, a @Deprecated override and a plain @Override side by side in one class and expects only
the plain one to be removed.

removeMethodWithOverrideAnnotationAfterModifier checks that public @Override void foo() is still
removed. What decides is which annotation it is, @Override or anything else, not where in the
declaration 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:

  • getAllAnnotations also reports an annotation on an annotated return type, so an override declared
    as 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.
  • Annotations on a parameter, on a type parameter, and nested type-use annotations are still not
    seen, so void foo(@Nullable String s) and public String @Nullable [] foo() are still removed.
  • A strictfp override is still removed, which only matters on Java releases before 17.

Have you considered any alternatives or workarounds?

An existing test asserted that a @Deprecated override is removed, so that behaviour was a choice
and not an oversight. Main lets an override be removed while it carries either of two annotation
types, java.lang.Override and java.lang.Deprecated, and this change narrows that set to
java.lang.Override alone. To go on removing @Deprecated overrides, restore the second condition
inside the loop over the method's annotations,
!TypeUtils.isOfClassType(annotationType, "java.lang.Deprecated"), and drop the @Deprecated cases
from the tests: doNotChangeDeprecatedMethod, doNotChangeDeprecatedMethodWithAnnotationAfterModifier,
and the @Deprecated override inside
removePlainOverrideAlongsideSynchronizedAndDeprecatedOverrides. The synchronized guard is
independent of that decision and works either way.

Any additional context

This change adds 5 tests to RemoveMethodsOnlyCallSuperTest and removes the name
removeDeprecatedMethodOnlyCallingSuper, as described above. Without the code change in this pull
request, 4 of the added tests fail: doNotChangeSynchronizedMethod, doNotChangeDeprecatedMethod,
doNotChangeDeprecatedMethodWithAnnotationAfterModifier and
removePlainOverrideAlongsideSynchronizedAndDeprecatedOverrides. The fifth,
removeMethodWithOverrideAnnotationAfterModifier, passes either way: main sees no annotation there
at all, since the annotation follows a modifier, and an @Override does not stop the removal on
either side.

This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and
this description.

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

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`.
@timtebeek
timtebeek merged commit 6ef24c5 into openrewrite:main Aug 11, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Aug 11, 2026
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)
  
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility\_score?dependency-name=org.openrewrite.recipe:rewrite-static-analysis&package-manager=maven&previous-version=2.40.0&new-version=2.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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants