Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.
Bug 406347 - [extract local] Extract to local variable not replacing multiple occurrences in same statement (MalformedTreeException: No target edit )
Summary: [extract local] Extract to local variable not replacing multiple occurrences ...
Status: RESOLVED FIXED
Alias: None
Product: JDT
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 3.3   Edit
Hardware: All All
: P3 normal (vote)
Target Milestone: 4.4 M3   Edit
Assignee: Nikolay Metchev CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2013-04-23 12:31 EDT by Andrew Eisenberg CLA
Modified: 2013-10-08 13:16 EDT (History)
4 users (show)

See Also:
noopur_gupta: review+


Attachments
proposed patch (18.97 KB, patch)
2013-10-03 09:00 EDT, Nikolay Metchev CLA
no flags Details | Diff
Code review changes, bug fix + unit test (17.25 KB, patch)
2013-10-04 09:57 EDT, Nikolay Metchev CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Andrew Eisenberg CLA 2013-04-23 12:31:42 EDT
In the following class:

public class BadExtract {
    void meth() {
        int x = 9;
        int y = 10;
        int j = x + y + x + y;
    }
}

Select the first 'x + y'.  And execute the extract local variable refactoring.  Only one occurrence is replaced.  The following exception is thrown:

java.lang.reflect.InvocationTargetException
	at org.eclipse.jface.operation.ModalContext.run(ModalContext.java:421)
	at org.eclipse.ltk.internal.ui.refactoring.RefactoringWizardDialog2.run(RefactoringWizardDialog2.java:331)
	at org.eclipse.ltk.ui.refactoring.RefactoringWizard.internalPerformFinish(RefactoringWizard.java:637)
	at org.eclipse.ltk.ui.refactoring.UserInputWizardPage.performFinish(UserInputWizardPage.java:153)
	at org.eclipse.ltk.ui.refactoring.RefactoringWizard.performFinish(RefactoringWizard.java:710)
	at org.eclipse.ltk.internal.ui.refactoring.RefactoringWizardDialog2.okPressed(RefactoringWizardDialog2.java:455)
	at org.eclipse.jface.dialogs.Dialog.buttonPressed(Dialog.java:472)
	at org.eclipse.jface.dialogs.Dialog$2.widgetSelected(Dialog.java:624)
	at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:248)
	at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:84)
	at org.eclipse.swt.widgets.Display.sendEvent(Display.java:4136)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1458)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1481)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1466)
	at org.eclipse.swt.widgets.Widget.notifyListeners(Widget.java:1271)
	at org.eclipse.swt.widgets.Display.runDeferredEvents(Display.java:3982)
	at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:3621)
	at org.eclipse.jface.window.Window.runEventLoop(Window.java:825)
	at org.eclipse.jface.window.Window.open(Window.java:801)
	at org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation$1.run(RefactoringWizardOpenOperation.java:187)
	at org.eclipse.swt.custom.BusyIndicator.showWhile(BusyIndicator.java:70)
	at org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation.run(RefactoringWizardOpenOperation.java:202)
	at org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation.run(RefactoringWizardOpenOperation.java:122)
	at org.eclipse.jdt.internal.ui.refactoring.actions.RefactoringStarter.activate(RefactoringStarter.java:38)
	at org.eclipse.jdt.ui.actions.ExtractTempAction.run(ExtractTempAction.java:88)
	at org.eclipse.jdt.ui.actions.SelectionDispatchAction.dispatchRun(SelectionDispatchAction.java:279)
	at org.eclipse.jdt.ui.actions.SelectionDispatchAction.run(SelectionDispatchAction.java:251)
	at org.eclipse.jface.action.Action.runWithEvent(Action.java:498)
	at org.eclipse.jface.action.ActionContributionItem.handleWidgetSelection(ActionContributionItem.java:584)
	at org.eclipse.jface.action.ActionContributionItem.access$2(ActionContributionItem.java:501)
	at org.eclipse.jface.action.ActionContributionItem$5.handleEvent(ActionContributionItem.java:411)
	at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:84)
	at org.eclipse.swt.widgets.Display.sendEvent(Display.java:4136)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1458)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1481)
	at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1466)
	at org.eclipse.swt.widgets.Widget.notifyListeners(Widget.java:1271)
	at org.eclipse.swt.widgets.Display.runDeferredEvents(Display.java:3982)
	at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:3621)
	at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$9.run(PartRenderingEngine.java:1053)
	at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:332)
	at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:942)
	at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:86)
	at org.eclipse.ui.internal.Workbench$5.run(Workbench.java:588)
	at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:332)
	at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:543)
	at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:149)
	at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:124)
	at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:196)
	at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:110)
	at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:79)
	at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:353)
	at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:180)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:601)
	at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:629)
	at org.eclipse.equinox.launcher.Main.basicRun(Main.java:584)
	at org.eclipse.equinox.launcher.Main.run(Main.java:1438)
	at org.eclipse.equinox.launcher.Main.main(Main.java:1414)
Caused by: org.eclipse.text.edits.MalformedTreeException: No target edit provided.
	at org.eclipse.text.edits.CopySourceEdit.performConsistencyCheck(CopySourceEdit.java:239)
	at org.eclipse.text.edits.TextEdit.traverseConsistencyCheck(TextEdit.java:873)
	at org.eclipse.text.edits.CopySourceEdit.traverseConsistencyCheck(CopySourceEdit.java:214)
	at org.eclipse.text.edits.TextEdit.traverseConsistencyCheck(TextEdit.java:869)
	at org.eclipse.text.edits.TextEdit.traverseConsistencyCheck(TextEdit.java:869)
	at org.eclipse.text.edits.TextEdit.traverseConsistencyCheck(TextEdit.java:869)
	at org.eclipse.text.edits.TextEditProcessor.checkIntegrityDo(TextEditProcessor.java:176)
	at org.eclipse.text.edits.TextEdit.dispatchCheckIntegrity(TextEdit.java:743)
	at org.eclipse.text.edits.TextEditProcessor.performEdits(TextEditProcessor.java:151)
	at org.eclipse.ltk.core.refactoring.TextChange.getPreviewDocument(TextChange.java:534)
	at org.eclipse.ltk.core.refactoring.TextChange.getPreviewDocument(TextChange.java:403)
	at org.eclipse.ltk.core.refactoring.TextChange.getPreviewContent(TextChange.java:411)
	at org.eclipse.jdt.internal.corext.refactoring.code.ExtractTempRefactoring.checkNewSource(ExtractTempRefactoring.java:559)
	at org.eclipse.jdt.internal.corext.refactoring.code.ExtractTempRefactoring.checkFinalConditions(ExtractTempRefactoring.java:504)
	at org.eclipse.ltk.core.refactoring.CheckConditionsOperation.run(CheckConditionsOperation.java:85)
	at org.eclipse.ltk.core.refactoring.CreateChangeOperation.run(CreateChangeOperation.java:121)
	at org.eclipse.ltk.core.refactoring.PerformChangeOperation.run(PerformChangeOperation.java:209)
	at org.eclipse.core.internal.resources.Workspace.run(Workspace.java:2344)
	at org.eclipse.ltk.internal.ui.refactoring.WorkbenchRunnableAdapter.run(WorkbenchRunnableAdapter.java:87)
	at org.eclipse.jface.operation.ModalContext$ModalContextThread.run(ModalContext.java:121)
Comment 1 Dani Megert CLA 2013-04-24 05:40:05 EDT
Used to work in 3.2 and older.
Comment 2 Nikolay Metchev CLA 2013-10-03 09:00:36 EDT
Created attachment 236070 [details]
proposed patch

This contribution complies with http://www.eclipse.org/legal/CoO.php

This was very tough for me to figure out. Hopefully the approach I have come up with is the correct one. 
This patch also fixes similar problems with Extract Constant Refactoring.
Comment 3 Noopur Gupta CLA 2013-10-04 07:28:04 EDT
Comment on attachment 236070 [details]
proposed patch

- Fix is required only in AssociativeInfixExpressionFragment and hence the changes in IASTFragment and SimpleFragment are not relevant.
- In AssociativeInfixExpressionFragment, 'matchingFragments' can be obtained in #replace(...) with "getMatchingFragmentsWithNode(groupNode)". 
- The processed fragments can be ignored in ExtractTempRefactoring#addReplaceExpressionWithTemp(...) by checking the associated node of the matching fragments to replace.
- The logic in AssociativeInfixExpressionFragment#replace(...) is fine. The code can be improved by refactoring and looking for minimal changes in the existing code.
- ExtractConstantRefactoring can be updated similar to ExtractTempRefactoring and tests can be added in ExtractConstantTests.
Comment 4 Nikolay Metchev CLA 2013-10-04 09:57:15 EDT
Created attachment 236113 [details]
Code review changes, bug fix + unit test

This contribution complies with http://www.eclipse.org/legal/CoO.php
Comment 5 Noopur Gupta CLA 2013-10-07 05:29:49 EDT
Thanks for the patch Nikolay.
Released after some minor changes:
http://git.eclipse.org/c/jdt/eclipse.jdt.ui.git/commit/?id=a34e4c1c1a8d27c68cfc055933a580c756e7fd10
Comment 6 Markus Keller CLA 2013-10-08 13:16:54 EDT
The approach looked correct, until I realized that there are still cases where we produce exceptions, e.g. if you change j to "int j = x + y + (x + y)".

The first underlying problem is a design flaw in InfixExpression: It doesn't handle all operands uniformly. Filed and fixed bug 418924.

To work around that, the code always created a new InfixExpression and then copied the other operands to the new expression. Your fix addressed the case where all matching fragments appeared on the same level in an InfixExpression, but still failed for cases where they are more deeply nested.

Then there's another caveat: There are many ways how to represent a longer infix expression in an AST. The old code tried to ensure it works no matter whether a chain of operands was parsed into a single InfixExpression or into a nested hierarchy of InfixExpressions. Since the ASTParser doesn't guarantee a certain structure, we shouldn't rely on a patterns we often see.

The fix also missed to consider the "replace all occurrences" flag: ExtractConstantTests#test45() should have failed, since the test implementation has replaceAll==false, but the expected output had all occurrences replaced.


Released a simpler implementation based on bug 418924 and updated tests:
http://git.eclipse.org/c/jdt/eclipse.jdt.ui.git/commit/?id=3f132f86580b01fb9661823f29ef36276d6265c9