Skip to content

Add super.finalize() through two-level finalizer hierarchies - #993

Draft
martinfrancois wants to merge 1 commit into
openrewrite:mainfrom
martinfrancois:repro/object-finalize-two-level-hierarchy
Draft

Add super.finalize() through two-level finalizer hierarchies#993
martinfrancois wants to merge 1 commit into
openrewrite:mainfrom
martinfrancois:repro/object-finalize-two-level-hierarchy

Conversation

@martinfrancois

@martinfrancois martinfrancois commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Suggested review order: 45 of 52 (Score: 1.5)
Review first: #987

What's changed?

Adds 1 known-failing test to ObjectFinalizeCallsSuperTest, named addsSuperFinalizeAndWidensThrowsAcrossClassHierarchy. It reproduces two defects that show up when ObjectFinalizeCallsSuper runs over a two-level finalize() hierarchy. No recipe code changes. The test is marked @ExpectedToFail so the suite stays green; removing the mark shows the failure. The class goes from 2 tests to 3.

What's your motivation?

Recipe: org.openrewrite.staticanalysis.ObjectFinalizeCallsSuper.

When a class and its subclass both override finalize() in the same run, the recipe produces output that does not compile, and it does not converge.

Before

class A {
    @Override
    protected void finalize() {
    }
}

class B extends A {
    @Override
    protected void finalize() {
    }
}

Actual after the recipe

Using current main after the first cycle.

class A {
    @Override
    protected void finalize() {
        super.finalize();
    }
}

class B extends A {
    @Override
    protected void finalize() {
        super.finalize();
    }
}

Two things are wrong here.

First, neither throws clause is widened. java.lang.Object#finalize() declares throws Throwable, so I compiled this output with javac and it fails at each super.finalize() call with error: unreported exception Throwable; must be caught or declared to be thrown.

Second, the run does not converge. The second cycle inserts a duplicate super.finalize(); into B only, so its body ends up with two consecutive super.finalize(); calls and the test harness reports Expected recipe to complete in 1 cycle, but took at least one more cycle. The reason is that B resolves its inserted call through the parse-time type attribution of A#finalize(), which does not carry the edit made to A in the same run, so the recipe does not recognize its own first insertion.

Expected after the recipe

Both overrides MUST call super.finalize() exactly once and MUST declare throws Throwable. I verified with javac that this expected result compiles.

Found while preparing #969, which fixes the throws widening for a single class and discloses this limitation in its "Two limits" section without fixing it. A full fix needs hierarchy-aware handling beyond #969, for example a ScanningRecipe that collects the finalize hierarchy before editing, because the subclass reads the parse-time attribution of A#finalize() and does not see edits made to A in the same run.

Affected code in real projects

  • NationalSecurityAgency/ghidra GhidraRandomAccessFile.java: warns about files left unclosed in a finalize() override that declares no throws clause and never calls super.finalize(). The recipe from main appends super.finalize(); without widening the throws clause, so the file stops compiling with unreported exception Throwable; must be caught or declared to be thrown.
  • Tencent/libpag PAGSurface.java: releases native resources in a finalize() override with no throws clause. The recipe from main inserts super.finalize(); into it, producing the same unreported Throwable compile error.
  • google/guava GcFinalizationTest.java: several anonymous Object subclasses override finalize() without a throws clause to count down latches. The recipe from main adds super.finalize(); to each of them, and none of the resulting overrides compile.

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

I think this is a genuine bug: the recipe turns compiling code into non-compiling code, and it duplicates a statement on the next cycle. If you agree this should change, I would gladly prepare the fix. If this behavior is intended, feel free to close this and I know it is settled.

Any additional context

Pre-existing tests changed: None.

My open #969 touches the same recipe but does not fix this; it widens the throws clause for a single class only.

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

The added reproduction tests and the existing suite together cover changed and unchanged behavior. The known-failing tests remain disabled until implementation. The formatter run was calibrated per file; untouched lines were not reformatted.

Checklist

…rarchy

Pins the correct behavior for A and B extends A where both override
finalize() without calling super: one run should add both calls and
widen both throws clauses so the hierarchy compiles. On main both
calls are added but neither clause is widened, and B gets a duplicate
call in cycle two. Marked @ExpectedToFail pending a ScanningRecipe.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Aug 11, 2026
@martinfrancois martinfrancois changed the title ObjectFinalizeCallsSuper: add failing test for two-level finalize hierarchy Add super.finalize() through two-level finalizer hierarchies Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant