Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.

Bug 105014

Summary: [ast rewrite] code lost applying changes after AST modification
Product: [Eclipse Project] JDT Reporter: Randall Theobald <rstheo>
Component: CoreAssignee: Martin Aeschlimann <martinae>
Status: CLOSED WORKSFORME QA Contact:
Severity: normal    
Priority: P3    
Version: 3.0.2   
Target Milestone: ---   
Hardware: PC   
OS: Windows XP   
Whiteboard:

Description Randall Theobald CLA 2005-07-25 10:09:00 EDT
My eclipse build is 200503110845, as part of IBM Websphere Integration Developer
6.0.

Basically, the problem is that if you apply the right combination of changes
to the AST before applying the changes, things can get lost. If the same
changes are made individually and applied in a serial fashion, there is no
problem. So, it appears that the mechanism that takes the changes to the AST
and writes them back to the document gets confused for certain change combinations.

I wrote a Java file that illustrates this by removing a CastExpression and a
ParenthesizedExpression from a line of code. The problem is that the deepest
expression somehow gets lost.

If you see a problem with the way I am doing things, please let me know. This
problem has caused me days of wasted effort and I didn't want somebody else
to have to go through it.

Here is the output of running the code:

******************************************************
original code: String myString = ((String)"myString");

Single removal of cast
  [cast removal]
code: String myString = ("myString");

Piggyback removal of parens
  [paren removal]
code: String myString = "myString";

Removal of both cast and parens
  [cast removal]
  [paren removal]
  [code from AST: String myString="myString";]
code after applying changes: String myString = ;

******************************************************

Here is the entire Java code:

******************************************************

import java.util.List;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jface.text.Document;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.UndoEdit;

/**
 * This file demonstrates a defect in the applying of changes made in the AST
 * back to the document. When making each change individually (applying in between),
 * there is no problem. However, when making both changes before applying, there
 * is a BIG problem (the expression disappears).
 *
 * Here, I use the example of removing a Cast and Parentheses from an expression.
 *
 * @author randallt
 */
public class ASTChangeTest {

    private static final String CU_START = "public class DUMMYCLASS { public
void X(){"; //$NON-NLS-1$
    private static final String CU_END = "\n}}//DUMMYCLASS"; //$NON-NLS-1$
    private static String NL = System.getProperty("line.separator"); //$NON-NLS-1$
    //     instance variables
    private static CompilationUnit cu = null;
    private static Document doc = null;
    
    /**
     * Creates the parser and parses the snippet
     * @param snippet the snippet
     * @return the Block object representing the body of the snippet
     */
    private static Block getBodyFromSnippet(String snippet) {
        ASTParser parser = ASTParser.newParser(AST.JLS2);
        StringBuffer buf = new
StringBuffer(CU_START).append(snippet).append(CU_END);
        doc = new Document(buf.toString());
        parser.setSource(doc.get().toCharArray());
        cu = (CompilationUnit)parser.createAST(null);
        cu.recordModifications();
        List cuTypes = cu.types();
        TypeDeclaration type = (TypeDeclaration)cuTypes.get(0);
        MethodDeclaration method = type.getMethods()[0];
        Block body = method.getBody();
        return body;
    }
    
    /**
     * Applies the changes and returns the changed code
     */
    private static String applyChanges() throws Exception {
        // Apply the rewrite, and get the output
        TextEdit edits = cu.rewrite(doc, null);
        UndoEdit undo = edits.apply(doc);
        String code = doc.get();
        code = code.substring(CU_START.length(), code.length() - CU_END.length());
        return code;
    }
    
    static String CODE = "String myString = ((String)\"myString\");";
    
    public static void main(String[] args) throws Exception {
        System.out.println("original code: " + CODE);
               
        // remove cast
        System.out.println(NL + "Single removal of cast");
        Block body = getBodyFromSnippet(CODE);
        ChangeVisitor visitor = new ChangeVisitor();
        visitor.state = 1;
        body.accept(visitor);
        String newCode = applyChanges();
        System.out.println("code: " + newCode);
       
        // remove paren from result of cast removal
        System.out.println(NL + "Piggyback removal of parens");
        body = getBodyFromSnippet(newCode);
        visitor.state = 2;
        body.accept(visitor);
        newCode = applyChanges();
        System.out.println("code: " + newCode);
       
        // start over and remove both before applying changes
        System.out.println(NL + "Removal of both cast and parens");
        body = getBodyFromSnippet(CODE);
        visitor.state = 3;
        body.accept(visitor);
        newCode = applyChanges();
        System.out.println("code after applying changes: " + newCode);
    }
    
}
    
class ChangeVisitor extends ASTVisitor {
    public int state = -1;
    
    public void endVisit(CastExpression cast) {
        if (state == 1 || state == 3) {
            System.out.println("  [cast removal]");
            Expression expression = cast.getExpression();
            Util.replaceNode(cast,expression);
        }
        super.endVisit(cast);
    }
    
    public void endVisit(ParenthesizedExpression paren) {
        if (state == 2 || state == 3) {
            ASTNode parent = paren.getParent().getParent();
            System.out.println("  [paren removal]");
            Expression expression = paren.getExpression();
            Util.replaceNode(paren,expression);
            if (state == 3) {
                System.out.println("  [code from AST: " + parent + "]");
            }
        }
        super.endVisit(paren);
    }
}


class Util {
    /**
     * Replaces the given oldNode with the newNode in the AST of the oldNode
     * @param oldNode the node to be replaced
     * @param newNode the node to replace with
     */
    public static void replaceNode(ASTNode oldNode, ASTNode newNode) {
        ASTNode copiedNode = ASTNode.copySubtree(oldNode.getAST(),newNode);
        ASTNode parent = oldNode.getParent();
        StructuralPropertyDescriptor location = oldNode.getLocationInParent();
        if (location.isChildProperty()) {
            parent.setStructuralProperty(location,copiedNode);
        } else if (location.isChildListProperty()) {
            List list = (List)parent.getStructuralProperty(location);
            int index = list.indexOf(oldNode);
            list.set(index,copiedNode);
        }
    }
}


******************************************************




import java.util.List;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jface.text.Document;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.UndoEdit;
/**
 * This file demonstrates a defect in the applying of changes made in the AST
 * back to the document. When making each change individually (applying in between),
 * there is no problem. However, when making both changes before applying, there
 * is a BIG problem (the expression disappears).
 * 
 * Here, I use the example of removing a Cast and Parentheses from an expression.
 * 
 * @author randallt
 */
public class ASTChangeTest {

	private static final String CU_START = "public class DUMMYCLASS { public void
X(){"; //$NON-NLS-1$
	private static final String CU_END = "\n}}//DUMMYCLASS"; //$NON-NLS-1$
	private static String NL = System.getProperty("line.separator"); //$NON-NLS-1$
	//	 instance variables
	private static CompilationUnit cu = null;
	private static Document doc = null;
	
	/**
	 * Creates the parser and parses the snippet
	 * @param snippet the snippet
	 * @return the Block object representing the body of the snippet
	 */
	private static Block getBodyFromSnippet(String snippet) {
	    ASTParser parser = ASTParser.newParser(AST.JLS2);
        StringBuffer buf = new
StringBuffer(CU_START).append(snippet).append(CU_END);
        doc = new Document(buf.toString());
        parser.setSource(doc.get().toCharArray());
        cu = (CompilationUnit)parser.createAST(null);
        cu.recordModifications();
        List cuTypes = cu.types();
        TypeDeclaration type = (TypeDeclaration)cuTypes.get(0);
        MethodDeclaration method = type.getMethods()[0];
        Block body = method.getBody();
        return body;
	}
	
	/**
	 * Applies the changes and returns the changed code
	 */
	private static String applyChanges() throws Exception {
	    // Apply the rewrite, and get the output
		TextEdit edits = cu.rewrite(doc, null);
		UndoEdit undo = edits.apply(doc);
		String code = doc.get();
		code = code.substring(CU_START.length(), code.length() - CU_END.length());
		return code;
	}
	
	static String CODE = "String myString = ((String)\"myString\");";
	
	public static void main(String[] args) throws Exception {
		System.out.println("original code: " + CODE);
				
		// remove cast
		System.out.println(NL + "Single removal of cast");
		Block body = getBodyFromSnippet(CODE);
		ChangeVisitor visitor = new ChangeVisitor();
		visitor.state = 1;
		body.accept(visitor);
		String newCode = applyChanges();
		System.out.println("code: " + newCode);
		
		// remove paren from result of cast removal
		System.out.println(NL + "Piggyback removal of parens");
		body = getBodyFromSnippet(newCode);
		visitor.state = 2;
		body.accept(visitor);
		newCode = applyChanges();
		System.out.println("code: " + newCode);
		
		// start over and remove both before applying changes
		System.out.println(NL + "Removal of both cast and parens");
		body = getBodyFromSnippet(CODE);
		visitor.state = 3;
		body.accept(visitor);
		newCode = applyChanges();
		System.out.println("code after applying changes: " + newCode);
	}
	
}
	
class ChangeVisitor extends ASTVisitor {
	public int state = -1;
	
	public void endVisit(CastExpression cast) {
		if (state == 1 || state == 3) {
			System.out.println("  [cast removal]");
			Expression expression = cast.getExpression();
			Util.replaceNode(cast,expression);
		}
		super.endVisit(cast);
	}
	
	public void endVisit(ParenthesizedExpression paren) {
		if (state == 2 || state == 3) {
			ASTNode parent = paren.getParent().getParent();
			System.out.println("  [paren removal]");
			Expression expression = paren.getExpression();
			Util.replaceNode(paren,expression);
			if (state == 3) {
				System.out.println("  [code from AST: " + parent + "]");
			}
		}
		super.endVisit(paren);
	}
}


class Util {
	/**
	 * Replaces the given oldNode with the newNode in the AST of the oldNode
	 * @param oldNode the node to be replaced
	 * @param newNode the node to replace with
	 */
	public static void replaceNode(ASTNode oldNode, ASTNode newNode) {
		ASTNode copiedNode = ASTNode.copySubtree(oldNode.getAST(),newNode);
	    ASTNode parent = oldNode.getParent();
	    StructuralPropertyDescriptor location = oldNode.getLocationInParent();
	    if (location.isChildProperty()) {
	        parent.setStructuralProperty(location,copiedNode);
	    } else if (location.isChildListProperty()) {
	        List list = (List)parent.getStructuralProperty(location);
	        int index = list.indexOf(oldNode);
	        list.set(index,copiedNode);
	    }
	}
}
Comment 1 Martin Aeschlimann CLA 2005-07-25 12:53:59 EDT
In 3.1, this is the output of your example:

original code: String myString = ((String)"myString");

Single removal of cast
  [cast removal]
code: String myString = ("myString");

Piggyback removal of parens
  [paren removal]
code: String myString = "myString";

Removal of both cast and parens
  [cast removal]
  [paren removal]
  [code from AST: String myString="myString";
]
code after applying changes: String myString = "myString";

That's the intended result, no?
Comment 2 Randall Theobald CLA 2005-07-25 12:58:58 EDT
Yes, it looks like it works fine in 3.1. Looks like this is a duplicate of a
previous defect. I didn't have the chance to investigate further, so thanks.
Comment 3 Martin Aeschlimann CLA 2005-07-26 05:39:17 EDT
closing
Comment 4 Randall Theobald CLA 2008-12-16 10:43:16 EST
No longer an issue.