package edu.ucsd.arcum.interpreter.transformation;
import static edu.ucsd.arcum.ArcumPlugin.DEBUG;
import static edu.ucsd.arcum.util.StringUtil.debugDisplay;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import edu.ucsd.arcum.exceptions.ArcumError;
import edu.ucsd.arcum.interpreter.ast.ASTUtil;
import edu.ucsd.arcum.interpreter.ast.ASTUtil.ASTPath;
import edu.ucsd.arcum.interpreter.fragments.ResolvedType;
import edu.ucsd.arcum.interpreter.parser.ASTVisitorAdaptor;
import edu.ucsd.arcum.interpreter.query.ASTTraverseTable;
import edu.ucsd.arcum.interpreter.query.Entity;
import edu.ucsd.arcum.interpreter.query.EntityTuple;
public class CodeRewriter
{
private static final String TRACKING_ID = "edu.ucsd.arcum.interpreter.transformation.CodeRewriter.TRACKING_ID";
final private List<Change> additionalChanges;
final private Map<String, ASTRewrite> rewriteLookup;
final private Map<String, ImportRewrite> importRewriteLookup;
final private Map<String, CompilationUnit> unitLookup;
final private TextEditGroup group;
// to keep track of the first doomed node per each type declaration, so that
// any new nodes inserted can go to the same place (that way, if the user has
// a preference for locations this should be the least surprising)
final private Map<TypeDeclaration, ASTNode> typeDeclarationMap;
final private Set<ASTNode> deletedNodes;
private List<ASTNode> trackedNodes;
final private static ASTTraverseTable traverser;
static {
traverser = new ASTTraverseTable();
}
public CodeRewriter(List<Change> additionalChanges, String editMessage) {
this.additionalChanges = additionalChanges;
this.rewriteLookup = Maps.newHashMap();
this.importRewriteLookup = Maps.newHashMap();
this.unitLookup = Maps.newHashMap();
this.group = new TextEditGroup(editMessage);
this.typeDeclarationMap = Maps.newIdentityHashMap();
this.deletedNodes = Sets.newHashSet();
}
public Change[] getChanges() throws IllegalArgumentException, CoreException {
List<Change> result = new ArrayList<Change>(additionalChanges);
for (String unit : rewriteLookup.keySet()) {
ASTRewrite rewrite = rewriteLookup.get(unit);
ImportRewrite importRewrite = importRewriteLookup.get(unit);
CompilationUnit cu = unitLookup.get(unit);
MultiTextEdit edits = new MultiTextEdit();
TextEdit astEdits = rewrite.rewriteAST();
edits.addChild(astEdits);
if (importRewrite != null) {
TextEdit importEdits = importRewrite.rewriteImports(null);
edits.addChild(importEdits);
}
IFile file = (IFile)cu.getJavaElement().getResource();
TextFileChange change = new TextFileChange(unit, file);
change.setTextType("java");
change.setEdit(edits);
result.add(change);
}
return result.toArray(new Change[0]);
}
public void insertNode(ASTNode parentNode, ASTNode newChildNode) {
ASTRewrite rewrite = getRewriter(ASTUtil.findCompilationUnit(parentNode));
ImportRewrite imports = getImportRewriter(ASTUtil.findCompilationUnit(parentNode));
if (parentNode instanceof TypeDeclaration) {
TypeDeclaration declaration = (TypeDeclaration)parentNode;
ListRewrite listRewrite;
listRewrite = rewrite.getListRewrite(parentNode,
TypeDeclaration.BODY_DECLARATIONS_PROPERTY);
ASTNode firstRemoved = typeDeclarationMap.get(declaration);
newChildNode = ensugarNode(newChildNode, imports);
if (firstRemoved != null) {
listRewrite.insertAfter(newChildNode, firstRemoved, group);
}
else {
listRewrite.insertLast(newChildNode, group);
}
}
}
public void removeNode(ASTNode doomed) {
deletedNodes.add(doomed);
if (true||DEBUG) {
System.out.printf("%nI'm gonna delete: [%s]%n", doomed.toString().trim());
}
ASTNode parent = doomed.getParent();
ASTRewrite rewrite = getRewriter(ASTUtil.findCompilationUnit(parent));
rewrite.remove(doomed, group);
if (parent instanceof TypeDeclaration) {
TypeDeclaration declaration = (TypeDeclaration)parent;
if (typeDeclarationMap.get(declaration) == null) {
typeDeclarationMap.put(declaration, doomed);
}
}
}
// Assumed to be called in a top-down order. That is, nodes higher up in the tree
// get replaced first. Also assumes all original nodes being replaced are being
// tracked.
public void replaceNode(ASTNode original, EntityTuple replacementTuple) {
final CompilationUnit cu = ASTUtil.findCompilationUnit(original);
final ASTRewrite rewriter = getRewriter(cu);
final ImportRewrite imports = getImportRewriter(cu);
final int trackingID = (Integer)original.getProperty(TRACKING_ID);
ASTNode replacement = replacementTuple.getReplacementNode(original);
replacement = ensugarNode(replacement, imports);
original = trackedNodes.get(trackingID);
if (original == null){
ArcumError.fatalError("Cannot replace node that wasn't tracked!");
}
if (true || DEBUG) {
System.out.printf("Replacing %s (%d)%n with: %s%n", debugDisplay(original),
trackingID, replacement);
}
// GETDONE: WAS HERE LAST -- If original is a sugared guy, we need to replace
// the list (e.g., of field declarations) using the list rewritter
rewriter.replace(original, replacement, group);
List<ASTNode> updatedChildren = findAllTrackedNodes(replacement);
for (ASTNode updatedChild : updatedChildren) {
int id = (Integer)getTrackingID(updatedChild);
trackedNodes.set(id, updatedChild);
}
}
private ASTNode searchForTrackingIdentifier(ASTNode node, final int trackingID) {
final ASTNode[] result = new ASTNode[1];
traverser.traverseAST(node, new ASTVisitorAdaptor() {
@Override
public boolean visitASTNode(ASTNode node, StructuralPropertyDescriptor edge) {
if (node == null) {
return false;
}
Integer foundID = (Integer)node.getProperty(TRACKING_ID);
if (foundID != null && foundID == trackingID) {
result[0] = node;
return false;
}
return true;
}
});
return result[0];
}
private ASTNode removeObsoleteNodes(final ASTNode original) {
final ASTNode result = Entity.copySubtree(original.getAST(), original);
traverser.traverseAST(original, new ASTVisitorAdaptor() {
@Override
public boolean visitASTNode(ASTNode node, StructuralPropertyDescriptor edge) {
if (node == null) {
return false;
}
ASTPath path = ASTUtil.getPathToRoot(node, original);
ASTNode element = path.getASTNodeFrom(result);
ASTUtil.recordUpdatedNode(node, element);
if (deletedNodes.contains(node)) {
ASTUtil.removeNodeFromParent(element);
return false;
}
else {
return true;
}
}
});
return result;
}
// Remove qualifiers from the given node, if possible. E.g., fully qualified
// type names become simple type names when an import can be done.
private ASTNode ensugarNode(ASTNode node, ImportRewrite imports) {
ASTNode newNode = doEnsugarNode(node, imports, null);
if (newNode != null) {
if (true || DEBUG) {
System.out.printf("Ensugaring %s into %s%n", debugDisplay(node),
debugDisplay(newNode));
}
return newNode;
}
else {
return node;
}
}
// track changes based on if we return null or not
private ASTNode doEnsugarNode(ASTNode node, ImportRewrite imports,
StructuralPropertyDescriptor parentEdge)
{
if (node instanceof QualifiedName) {
return ensugarQualifiedName((QualifiedName)node, imports, parentEdge);
}
else {
StructuralPropertyDescriptor[] spds = ASTTraverseTable.getProperties(node);
boolean madeChanges = false;
edges: for (StructuralPropertyDescriptor spd : spds) {
Object property = node.getStructuralProperty(spd);
if (property == null) {
continue edges;
}
if (spd.isChildProperty()) {
ASTNode child = (ASTNode)property;
// Don't ensugar tracked nodes: They will be replaced anyway
if (!isTracked(child)) {
ASTNode n = doEnsugarNode(child, imports, spd);
if (n != null) {
node.setStructuralProperty(spd, n);
madeChanges = true;
}
}
}
else if (spd.isChildListProperty()) {
List children = (List)property;
for (int i = 0; i < children.size(); ++i) {
ASTNode child = (ASTNode)children.get(i);
// Ditto
if (!isTracked(child)) {
ASTNode n = doEnsugarNode(child, imports, spd);
if (n != null) {
children.set(i, n);
madeChanges = true;
}
}
}
}
}
if (madeChanges) {
ASTNode newNode = Entity.copySubtree(node.getAST(), node);
return newNode;
}
else {
return null;
}
}
}
// if we can't change it, return null
private ASTNode ensugarQualifiedName(QualifiedName name, ImportRewrite imports,
StructuralPropertyDescriptor parentEdge)
{
if (representsAType(name, parentEdge)) {
String fullyQualifiedName = name.getFullyQualifiedName();
String toUse = imports.addImport(fullyQualifiedName);
if (toUse.contains(".")) {
return null;
}
else {
if (true || DEBUG) {
System.out.printf("The name [%s] can be [%s].%n", name, toUse);
}
AST ast = name.getAST();
Name newName = ast.newName(toUse);
return newName;
}
}
return null;
}
private boolean representsAType(QualifiedName name, StructuralPropertyDescriptor parentEdge) {
if (parentEdge == SimpleType.NAME_PROPERTY) {
return true;
}
String fullyQualifiedName = name.getFullyQualifiedName();
if (ResolvedType.isKnownType(fullyQualifiedName)) {
return true;
}
return false;
}
private ASTRewrite getRewriter(CompilationUnit rootNode) {
String cuName = rootNode.getJavaElement().getPath().toString();
ASTRewrite rewrite = rewriteLookup.get(cuName);
if (rewrite == null) {
AST ast = rootNode.getAST();
rewrite = ASTRewrite.create(ast);
rewriteLookup.put(cuName, rewrite);
if (!unitLookup.containsKey(cuName)) {
unitLookup.put(cuName, rootNode);
}
}
return rewrite;
}
private ImportRewrite getImportRewriter(CompilationUnit rootNode) {
String cuName = rootNode.getJavaElement().getPath().toString();
ImportRewrite importRewrite = importRewriteLookup.get(cuName);
if (importRewrite == null) {
importRewrite = ImportRewrite.create(rootNode, true);
importRewriteLookup.put(cuName, importRewrite);
if (!unitLookup.containsKey(cuName)) {
unitLookup.put(cuName, rootNode);
}
}
return importRewrite;
}
public void trackNodes(List<ASTNode> nodesToTrack) {
int trackingID = 0;
for (ASTNode node : nodesToTrack) {
node.setProperty(TRACKING_ID, trackingID++);
}
this.trackedNodes = Lists.newArrayList(nodesToTrack);
}
public void resetTracking() {
for (ASTNode node : trackedNodes) {
node.setProperty(TRACKING_ID, null);
}
this.trackedNodes = null;
}
public static boolean isTracked(ASTNode node) {
return node.getProperty(TRACKING_ID) != null;
}
public static Object getTrackingID(ASTNode node) {
return node.getProperty(TRACKING_ID);
}
public static void setTrackingID(ASTNode node, Object id) {
node.setProperty(TRACKING_ID, id);
}
public static List<ASTNode> findAllTrackedNodes(ASTNode node) {
final List<ASTNode> result = Lists.newArrayList();
traverser.traverseAST(node, new ASTVisitorAdaptor() {
@Override
public boolean visitASTNode(ASTNode node, StructuralPropertyDescriptor edge) {
if (node == null) {
return false;
}
if (isTracked(node)) {
System.out.printf("Tracking %s (%d)%n", debugDisplay(node), getTrackingID(node));
result.add(node);
}
return true;
}
});
return result;
}
}