Method-Rename: Override-Methoden generischer Interfaces werden headless nicht aktualisiert (JDT Generics-Binding) #29

Closed
opened 2026-03-08 09:25:50 +00:00 by hauschel.fred · 3 comments
Collaborator

Problem

jdt_rename_element auf Interface-Methoden mit Generics aktualisiert Implementierungen in anderen Modulen nicht.

Betrifft nur: Virtuelle Methoden (Interface/Override) mit Generics-Typ-Parametern.
Funktioniert: Felder, Klassen, nicht-virtuelle Methoden — auch cross-project.

Reproduktion (Test I4, v0.2.18)

jdt_rename_element(elementName="org.fixture.api.Processor#process", newName="execute", elementType="METHOD")

Ergebnis: STATUS=SUCCESS, aber nur 3 Dateien geändert:

  1. Processor.java (fixture-api) — Interface-Deklaration
  2. AppService.java (fixture-app) — Caller
  3. ExternalService.java (fixture-external) — Caller

Fehlend:

  • SimpleProcessor.java (fixture-core) — behält process() statt execute()
  • BatchProcessor.java (fixture-core) — behält process() statt execute()
  • BaseProcessor.java (fixture-core) — behält process() statt execute()

→ 4 Compile-Fehler

Root Cause: JDT Generics-Binding-Resolution im Headless-Modus

RenameVirtualMethodProcessor (Processor-basierter Weg)

  • Wird korrekt gewählt (isVirtual=true, RenameVirtualMethodProcessor)
  • checkFinalConditions läuft ohne Fehler durch
  • createChange produziert Changes — aber nur für Caller, nicht für Implementierungen
  • Intern nutzt RippleMethodFinder2 den MethodOverrideTester mit Binding-Vergleichen
  • Processor<T>.process(T) hat Signatur (QT;) — die Implementierung process(String) hat (QString;)
  • Der MethodOverrideTester braucht aufgelöste Bindings für die Erasure-Zuordnung, die im Headless-Mode fehlen

SearchEngine (AST-basierter Weg)

  • ALL_OCCURRENCES findet ebenfalls nur Interface-Deklaration + Caller
  • Override-Methoden in Subtypes werden nicht als Occurrences erkannt (gleiches Generics-Problem)

TypeHierarchy (funktioniert!)

  • IType.newTypeHierarchy() findet alle Subtypes korrekt
  • IType.getMethods() listet die Methoden korrekt
  • Aber: Nur die Deklarationen können so gepatcht werden, nicht die zugehörigen Call-Sites und Self-Calls

Versuchte Fix-Ansätze (alle gescheitert)

  1. Post-Check mit TypeHierarchy: Findet fehlende Implementierungen, patcht aber nur Deklarationen — Call-Sites und Self-Calls bleiben unberührt
  2. AST-Fallback mit ALL_OCCURRENCES: SearchEngine hat dasselbe Generics-Problem wie der Processor
  3. AST-Fallback mit REFERENCES: Findet nur Aufrufe, keine Override-Deklarationen

Offene Frage

Ob das Problem auch bei virtuellen Methoden ohne Generics auftritt (z.B. void doStuff(String) in Interface und Implementierung). Falls nicht, ist es rein ein Generics-Binding-Problem.

Status

  • Diagnostic Logging hinzugefügt (commit 65daec8)
  • Fix-Versuche revertiert — kein sauberer Workaround gefunden
  • Vermutlich ein Bug in Eclipse JDT im Headless-Modus (upstream melden?)

Kontext

  • Ursprünglich identifiziert bei der Analyse von #26 durch Agent jdt-vogel
  • Bestätigt im Test-Report v0.2.18 (Test I4)
  • Analyse-Details aus 3 Test-Runs mit erweitertem Logging
## Problem `jdt_rename_element` auf Interface-Methoden mit Generics aktualisiert Implementierungen in anderen Modulen nicht. **Betrifft nur:** Virtuelle Methoden (Interface/Override) mit Generics-Typ-Parametern. **Funktioniert:** Felder, Klassen, nicht-virtuelle Methoden — auch cross-project. ## Reproduktion (Test I4, v0.2.18) ``` jdt_rename_element(elementName="org.fixture.api.Processor#process", newName="execute", elementType="METHOD") ``` **Ergebnis:** STATUS=SUCCESS, aber nur 3 Dateien geändert: 1. `Processor.java` (fixture-api) — Interface-Deklaration ✅ 2. `AppService.java` (fixture-app) — Caller ✅ 3. `ExternalService.java` (fixture-external) — Caller ✅ **Fehlend:** - `SimpleProcessor.java` (fixture-core) — behält `process()` statt `execute()` ❌ - `BatchProcessor.java` (fixture-core) — behält `process()` statt `execute()` ❌ - `BaseProcessor.java` (fixture-core) — behält `process()` statt `execute()` ❌ → 4 Compile-Fehler ## Root Cause: JDT Generics-Binding-Resolution im Headless-Modus ### RenameVirtualMethodProcessor (Processor-basierter Weg) - Wird korrekt gewählt (`isVirtual=true`, `RenameVirtualMethodProcessor`) - `checkFinalConditions` läuft ohne Fehler durch - `createChange` produziert Changes — aber nur für Caller, nicht für Implementierungen - Intern nutzt `RippleMethodFinder2` den `MethodOverrideTester` mit Binding-Vergleichen - `Processor<T>.process(T)` hat Signatur `(QT;)` — die Implementierung `process(String)` hat `(QString;)` - Der `MethodOverrideTester` braucht aufgelöste Bindings für die Erasure-Zuordnung, die im Headless-Mode fehlen ### SearchEngine (AST-basierter Weg) - `ALL_OCCURRENCES` findet ebenfalls nur Interface-Deklaration + Caller - Override-Methoden in Subtypes werden **nicht** als Occurrences erkannt (gleiches Generics-Problem) ### TypeHierarchy (funktioniert!) - `IType.newTypeHierarchy()` findet alle Subtypes korrekt - `IType.getMethods()` listet die Methoden korrekt - **Aber:** Nur die Deklarationen können so gepatcht werden, nicht die zugehörigen Call-Sites und Self-Calls ## Versuchte Fix-Ansätze (alle gescheitert) 1. **Post-Check mit TypeHierarchy**: Findet fehlende Implementierungen, patcht aber nur Deklarationen — Call-Sites und Self-Calls bleiben unberührt 2. **AST-Fallback mit ALL_OCCURRENCES**: SearchEngine hat dasselbe Generics-Problem wie der Processor 3. **AST-Fallback mit REFERENCES**: Findet nur Aufrufe, keine Override-Deklarationen ## Offene Frage Ob das Problem auch bei virtuellen Methoden **ohne Generics** auftritt (z.B. `void doStuff(String)` in Interface und Implementierung). Falls nicht, ist es rein ein Generics-Binding-Problem. ## Status - Diagnostic Logging hinzugefügt (commit 65daec8) - Fix-Versuche revertiert — kein sauberer Workaround gefunden - Vermutlich ein Bug in Eclipse JDT im Headless-Modus (upstream melden?) ## Kontext - Ursprünglich identifiziert bei der Analyse von #26 durch Agent jdt-vogel - Bestätigt im Test-Report v0.2.18 (Test I4) - Analyse-Details aus 3 Test-Runs mit erweitertem Logging
Author
Collaborator

This is already implemented in RefactoringTools.java (lines 1577-1611):

  • isVirtualMethod(method) delegates to MethodChecks.isVirtual(method) (line 1607)
  • Virtual methods → RenameVirtualMethodProcessor (line 1579)
  • Non-virtual methods → RenameNonVirtualMethodProcessor (line 1583)

This was part of the PR #40 (AST-based rename fallback) merge. Can be closed.

This is already implemented in `RefactoringTools.java` (lines 1577-1611): - `isVirtualMethod(method)` delegates to `MethodChecks.isVirtual(method)` (line 1607) - Virtual methods → `RenameVirtualMethodProcessor` (line 1579) - Non-virtual methods → `RenameNonVirtualMethodProcessor` (line 1583) This was part of the PR #40 (AST-based rename fallback) merge. Can be closed.
hauschel.fred changed title from Method-Rename: Virtual vs Non-Virtual Processor fehlt to Method-Rename: Override-Methoden generischer Interfaces werden headless nicht aktualisiert (JDT Generics-Binding) 2026-09-07 16:22:36 +00:00
Author
Collaborator

Triage 2026-09-07: Titel angepasst, P2 → P3

Der ursprüngliche Titel („Virtual vs Non-Virtual Processor fehlt") ist überholt: Der Split existiert seit PR #40 in RenameRefactoring.java:274ff (isVirtualMethodRenameVirtualMethodProcessor / RenameNonVirtualMethodProcessor).

Was bleibt, ist die in der README dokumentierte JDT-Einschränkung: RenameVirtualMethodProcessor löst im Headless-Modus Processor<T>.process(T) nicht zu SimpleProcessor.process(String) auf. Caller werden aktualisiert, Override-Methoden nicht.

Herabgestuft auf P3, weil es sich um einen JDT-Bug handelt, dessen Fix auf unserer Seite nur ein AST-Fallback über die Type-Hierarchy wäre. Betrifft nur generische Interface-Methoden.

## Triage 2026-09-07: Titel angepasst, P2 → P3 Der ursprüngliche Titel („Virtual vs Non-Virtual Processor fehlt") ist überholt: Der Split existiert seit PR #40 in `RenameRefactoring.java:274ff` (`isVirtualMethod` → `RenameVirtualMethodProcessor` / `RenameNonVirtualMethodProcessor`). Was bleibt, ist die in der README dokumentierte JDT-Einschränkung: `RenameVirtualMethodProcessor` löst im Headless-Modus `Processor<T>.process(T)` nicht zu `SimpleProcessor.process(String)` auf. Caller werden aktualisiert, Override-Methoden nicht. Herabgestuft auf P3, weil es sich um einen JDT-Bug handelt, dessen Fix auf unserer Seite nur ein AST-Fallback über die Type-Hierarchy wäre. Betrifft nur generische Interface-Methoden.
Author
Collaborator

Reproduziert und gefixt in PR #97 (Branch fix/29-generics-override-rename, Basis PR #95).

Die Diagnose im Issue war halb richtig. E2E gegen das gebaute Produkt (tests/refactoring-test.sh, Fixture-Kopie) zeigt zwei getrennte Ursachen:

1. Ein zweiter, vorgelagerter Defekt — checkFinalConditions brach komplett ab. Auf dem heutigen Stand kam nicht einmal mehr STATUS=SUCCESS mit 3 Dateien, sondern {"status":"ERROR","message":"Error during rename: assertion failed: "}:

org.eclipse.core.runtime.AssertionFailedException: assertion failed:
  at org.eclipse.ltk.core.refactoring.TextFileChange.releaseDocument(TextFileChange.java:252)
  at org.eclipse.jdt.core.refactoring.CompilationUnitChange.releaseDocument(CompilationUnitChange.java:92)
  at org.eclipse.ltk.core.refactoring.TextChange.getCurrentDocument(TextChange.java:312)
  at org.eclipse.ltk.core.refactoring.TextChange.getPreviewContent(TextChange.java:405)
  at org.eclipse.jdt.internal.corext.refactoring.rename.RenameAnalyzeUtil.createNewWorkingCopy(RenameAnalyzeUtil.java:200)
  at org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.batchFindNewOccurrences(RenameMethodProcessor.java:665)
  at org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.analyzeRenameChanges(RenameMethodProcessor.java:577)
  at org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.doCheckFinalConditions(RenameMethodProcessor.java:419)

Das ist derselbe Headless-Bug wie bei performChange() (Buffer-Manager ausserhalb der IDE), hier aber in JDTs Post-Rename-Analyse. Der Bytecode von RenameMethodProcessor.doCheckFinalConditions zeigt createChanges() (bci 330) vor analyzeRenameChanges() (bci 353) — der Change-Satz steht also bereits, wenn die Analyse scheitert. Deshalb: Analyse überspringen, Change direkt vom Processor (processor.createChange(), weil ohne durchgelaufenes checkFinalConditions keine Participants geladen sind), Warnung in die Antwort.

2. Der Ripple-Defekt ist echt und generics-spezifisch. Alles, was JDT dafür braucht, funktioniert headless: jdt_find_implementations findet alle drei Subtypen, jdt_get_type_hierarchy stimmt, der projektübergreifende Scope stimmt (Caller in fixture-external werden gefunden). Nur RippleMethodFinder2/MethodOverrideTester verknüpft Processor<T>.process(T) nicht mit SimpleProcessor.process(String). Gegenprobe mit Configurable#configure (nicht-generisch, gleiche Konstellation, Override in anderem Modul): funktioniert vollständig — die offene Frage aus dem Issue ist damit beantwortet, es ist rein ein Generics-Problem.

Warum der Fix diesmal trägt. Die früher revertierten Versuche patchten nur Deklarationen und verfehlten Call-Sites und Self-Calls. Stattdessen wird jeder übriggebliebene Override über die Typhierarchie gesucht und mit einem eigenen Rename-Lauf umbenannt: nach dem Umbenennen der Deklaration ist er nicht mehr virtuell, und diesen Fall macht JDT korrekt — inklusive Self-Call in processAndFormat() und Testklassen. Einzige Hürde dort: JDT meldet "will be shadowed by a renamed declaration" als ERROR — das beschreibt genau die gewollte Override-Beziehung und wird nur in diesem Nachlauf toleriert.

Ergebnis: tests/refactoring-test.sh 4 von 4 grün (neu Test 0 generisch und Test 0b nicht-generisch als Schutz gegen Fehlalarme), tests/smoke-test.sh 9 von 9. Der README-Eintrag unter „Bekannte Einschränkungen" ist entfernt; ein Rest, der nicht umbenannt werden kann, wird als status: WARNING mit unrenamedOverrides gemeldet statt still durchgelassen.

Ein Upstream-Report an Eclipse bleibt sinnvoll (zwei Punkte: RenameAnalyzeUtil headless und der Generics-Ripple) — dieser PR umgeht beides, behebt es aber nicht in JDT.

Reproduziert und gefixt in PR #97 (Branch `fix/29-generics-override-rename`, Basis PR #95). **Die Diagnose im Issue war halb richtig.** E2E gegen das gebaute Produkt (`tests/refactoring-test.sh`, Fixture-Kopie) zeigt zwei getrennte Ursachen: **1. Ein zweiter, vorgelagerter Defekt — `checkFinalConditions` brach komplett ab.** Auf dem heutigen Stand kam nicht einmal mehr `STATUS=SUCCESS` mit 3 Dateien, sondern `{"status":"ERROR","message":"Error during rename: assertion failed: "}`: ``` org.eclipse.core.runtime.AssertionFailedException: assertion failed: at org.eclipse.ltk.core.refactoring.TextFileChange.releaseDocument(TextFileChange.java:252) at org.eclipse.jdt.core.refactoring.CompilationUnitChange.releaseDocument(CompilationUnitChange.java:92) at org.eclipse.ltk.core.refactoring.TextChange.getCurrentDocument(TextChange.java:312) at org.eclipse.ltk.core.refactoring.TextChange.getPreviewContent(TextChange.java:405) at org.eclipse.jdt.internal.corext.refactoring.rename.RenameAnalyzeUtil.createNewWorkingCopy(RenameAnalyzeUtil.java:200) at org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.batchFindNewOccurrences(RenameMethodProcessor.java:665) at org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.analyzeRenameChanges(RenameMethodProcessor.java:577) at org.eclipse.jdt.internal.corext.refactoring.rename.RenameMethodProcessor.doCheckFinalConditions(RenameMethodProcessor.java:419) ``` Das ist derselbe Headless-Bug wie bei `performChange()` (Buffer-Manager ausserhalb der IDE), hier aber in JDTs Post-Rename-*Analyse*. Der Bytecode von `RenameMethodProcessor.doCheckFinalConditions` zeigt `createChanges()` (bci 330) **vor** `analyzeRenameChanges()` (bci 353) — der Change-Satz steht also bereits, wenn die Analyse scheitert. Deshalb: Analyse überspringen, Change direkt vom Processor (`processor.createChange()`, weil ohne durchgelaufenes `checkFinalConditions` keine Participants geladen sind), Warnung in die Antwort. **2. Der Ripple-Defekt ist echt und generics-spezifisch.** Alles, was JDT dafür braucht, funktioniert headless: `jdt_find_implementations` findet alle drei Subtypen, `jdt_get_type_hierarchy` stimmt, der projektübergreifende Scope stimmt (Caller in `fixture-external` werden gefunden). Nur `RippleMethodFinder2`/`MethodOverrideTester` verknüpft `Processor<T>.process(T)` nicht mit `SimpleProcessor.process(String)`. Gegenprobe mit `Configurable#configure` (nicht-generisch, gleiche Konstellation, Override in anderem Modul): **funktioniert vollständig** — die offene Frage aus dem Issue ist damit beantwortet, es ist rein ein Generics-Problem. **Warum der Fix diesmal trägt.** Die früher revertierten Versuche patchten nur Deklarationen und verfehlten Call-Sites und Self-Calls. Stattdessen wird jeder übriggebliebene Override über die Typhierarchie gesucht und mit einem *eigenen* Rename-Lauf umbenannt: nach dem Umbenennen der Deklaration ist er nicht mehr virtuell, und diesen Fall macht JDT korrekt — inklusive Self-Call in `processAndFormat()` und Testklassen. Einzige Hürde dort: JDT meldet `"will be shadowed by a renamed declaration"` als ERROR — das beschreibt genau die gewollte Override-Beziehung und wird nur in diesem Nachlauf toleriert. Ergebnis: `tests/refactoring-test.sh` 4 von 4 grün (neu Test 0 generisch und Test 0b nicht-generisch als Schutz gegen Fehlalarme), `tests/smoke-test.sh` 9 von 9. Der README-Eintrag unter „Bekannte Einschränkungen" ist entfernt; ein Rest, der nicht umbenannt werden kann, wird als `status: WARNING` mit `unrenamedOverrides` gemeldet statt still durchgelassen. Ein Upstream-Report an Eclipse bleibt sinnvoll (zwei Punkte: `RenameAnalyzeUtil` headless und der Generics-Ripple) — dieser PR umgeht beides, behebt es aber nicht in JDT.
Commenting is not possible because the repository is archived.
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ai-tools/jdt-mcp-server#29
No description provided.