/******************************************************************************* * Copyright (c) 2008, 2011 Institute for Software, HSR Hochschule fuer Technik * Rapperswil, University of applied sciences and others * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Institute for Software - initial API and implementation * Markus Schorn (Wind River Systems) *******************************************************************************/ package org.eclipse.cdt.internal.core.dom.rewrite.changegenerator; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import org.eclipse.cdt.core.dom.ast.ASTVisitor; import org.eclipse.cdt.core.dom.ast.IASTArrayModifier; import org.eclipse.cdt.core.dom.ast.IASTComment; import org.eclipse.cdt.core.dom.ast.IASTDeclSpecifier; import org.eclipse.cdt.core.dom.ast.IASTDeclaration; import org.eclipse.cdt.core.dom.ast.IASTDeclarator; import org.eclipse.cdt.core.dom.ast.IASTExpression; import org.eclipse.cdt.core.dom.ast.IASTFileLocation; import org.eclipse.cdt.core.dom.ast.IASTInitializer; import org.eclipse.cdt.core.dom.ast.IASTName; import org.eclipse.cdt.core.dom.ast.IASTNode; import org.eclipse.cdt.core.dom.ast.IASTParameterDeclaration; import org.eclipse.cdt.core.dom.ast.IASTStatement; import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit; import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTNamespaceDefinition; import org.eclipse.cdt.internal.core.dom.rewrite.ASTModification; import org.eclipse.cdt.internal.core.dom.rewrite.ASTModificationMap; import org.eclipse.cdt.internal.core.dom.rewrite.ASTModificationStore; import org.eclipse.cdt.internal.core.dom.rewrite.ASTRewriteAnalyzer; import org.eclipse.cdt.internal.core.dom.rewrite.astwriter.ASTWriter; import org.eclipse.cdt.internal.core.dom.rewrite.astwriter.ProblemRuntimeException; import org.eclipse.cdt.internal.core.dom.rewrite.commenthandler.NodeCommentMap; import org.eclipse.cdt.internal.core.dom.rewrite.util.FileContentHelper; import org.eclipse.cdt.internal.core.dom.rewrite.util.FileHelper; import org.eclipse.cdt.internal.core.resources.ResourceLookup; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.CompositeChange; import org.eclipse.ltk.core.refactoring.TextFileChange; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEditGroup; public class ChangeGenerator extends ASTVisitor { private final LinkedHashMap<String, Integer> sourceOffsets = new LinkedHashMap<String, Integer>(); public LinkedHashMap<IASTNode, List<ASTModification>> modificationParent = new LinkedHashMap<IASTNode, List<ASTModification>>(); private final LinkedHashMap<IFile, MultiTextEdit> changes = new LinkedHashMap<IFile, MultiTextEdit>(); private CompositeChange change; private final ASTModificationStore modificationStore; private NodeCommentMap commentMap; { shouldVisitExpressions = true; shouldVisitStatements = true; shouldVisitNames = true; shouldVisitDeclarations = true; shouldVisitDeclSpecifiers = true; shouldVisitDeclarators = true; shouldVisitArrayModifiers= true; shouldVisitInitializers = true; shouldVisitBaseSpecifiers = true; shouldVisitNamespaces = true; shouldVisitTemplateParameters = true; shouldVisitParameterDeclarations = true; shouldVisitTranslationUnit = true; } public ChangeGenerator(ASTModificationStore modificationStore, NodeCommentMap commentMap) { this.modificationStore = modificationStore; this.commentMap = commentMap; } public void generateChange(IASTNode rootNode) throws ProblemRuntimeException { generateChange(rootNode, this); } public void generateChange(IASTNode rootNode, ASTVisitor pathProvider) throws ProblemRuntimeException { change = new CompositeChange(Messages.ChangeGenerator_compositeChange); initParentModList(); rootNode.accept(pathProvider); for (IFile currentFile : changes.keySet()) { TextFileChange subchange= ASTRewriteAnalyzer.createCTextFileChange(currentFile); subchange.setEdit(changes.get(currentFile)); change.add(subchange); } } private void initParentModList() { ASTModificationMap rootModifications = modificationStore.getRootModifications(); if (rootModifications != null) { for (IASTNode modifiedNode : rootModifications.getModifiedNodes()) { List<ASTModification> modificationsForNode = rootModifications.getModificationsForNode(modifiedNode); IASTNode modifiedNodeParent = determineParentToBeRewritten(modifiedNode, modificationsForNode); List<ASTModification> list = modificationParent.get(modifiedNodeParent != null ? modifiedNodeParent : modifiedNode); if (list != null) { list.addAll(modificationsForNode); } else { List<ASTModification> modifiableList = new ArrayList<ASTModification>(modificationsForNode); modificationParent.put(modifiedNodeParent != null ? modifiedNodeParent : modifiedNode, modifiableList); } } } } private IASTNode determineParentToBeRewritten(IASTNode modifiedNode, List<ASTModification> modificationsForNode) { IASTNode modifiedNodeParent = modifiedNode; for (ASTModification currentModification : modificationsForNode) { if (currentModification.getKind() == ASTModification.ModificationKind.REPLACE) { modifiedNodeParent = modifiedNode.getParent(); break; } } modifiedNodeParent = modifiedNodeParent != null ? modifiedNodeParent : modifiedNode; return modifiedNodeParent; } @Override public int visit(IASTTranslationUnit translationUnit) { if (hasChangedChild(translationUnit)) { synthTreatment(translationUnit); } IASTFileLocation location = translationUnit.getFileLocation(); sourceOffsets.put(location.getFileName(), Integer.valueOf(location.getNodeOffset())); return super.visit(translationUnit); } @Override public int leave(IASTTranslationUnit tu) { return super.leave(tu); } private int getOffsetForNodeFile(IASTNode rootNode) { Integer offset = sourceOffsets.get(rootNode.getFileLocation().getFileName()); return offset == null ? 0 : offset.intValue(); } @Override public int visit(IASTDeclaration declaration) { if (hasChangedChild(declaration)) { synthTreatment(declaration); return ASTVisitor.PROCESS_SKIP; } return super.visit(declaration); } private void synthTreatment(IASTNode synthNode) { synthTreatment(synthNode, null); } private void synthTreatment(IASTNode synthNode, String fileScope) { String indent = getIndent(synthNode); ASTWriter synthWriter = new ASTWriter(indent); synthWriter.setModificationStore(modificationStore); String synthSource = synthWriter.write(synthNode, fileScope, commentMap); createChange(synthNode, synthSource); IASTFileLocation fileLocation = synthNode.getFileLocation(); int newOffset = fileLocation.getNodeOffset() + fileLocation.getNodeLength(); sourceOffsets.put(fileLocation.getFileName(), Integer.valueOf(newOffset)); } private void synthTreatment(IASTTranslationUnit synthTU) { ASTWriter synthWriter = new ASTWriter(); synthWriter.setModificationStore(modificationStore); for (ASTModification modification : modificationParent.get(synthTU)) { IASTFileLocation targetLocation = modification.getTargetNode().getFileLocation(); String currentFile = targetLocation.getFileName(); IPath implPath = new Path(currentFile); IFile relevantFile= ResourceLookup.selectFileForLocation(implPath, null); if (relevantFile == null || !relevantFile.exists()) { // if not in workspace or local file system throw new UnhandledASTModificationException(modification); } MultiTextEdit edit; if (changes.containsKey(relevantFile)) { edit = changes.get(relevantFile); } else { edit = new MultiTextEdit(); changes.put(relevantFile, edit); } String newNodeCode = synthWriter.write(modification.getNewNode(), null, commentMap); switch (modification.getKind()) { case REPLACE: edit.addChild(new ReplaceEdit(targetLocation.getNodeOffset(), targetLocation.getNodeLength(), newNodeCode)); break; case INSERT_BEFORE: edit.addChild(new InsertEdit(getOffsetIncludingComments(modification.getTargetNode()), newNodeCode)); break; case APPEND_CHILD: if (modification.getTargetNode() instanceof IASTTranslationUnit && ((IASTTranslationUnit)modification.getTargetNode()).getDeclarations().length > 0) { IASTTranslationUnit tu = (IASTTranslationUnit)modification.getTargetNode(); IASTDeclaration lastDecl = tu.getDeclarations()[tu.getDeclarations().length -1]; targetLocation = lastDecl.getFileLocation(); } String lineDelimiter = FileHelper.determineLineDelimiter( FileHelper.getIFilefromIASTNode(modification.getTargetNode())); edit.addChild(new InsertEdit(targetLocation.getNodeOffset() + targetLocation.getNodeLength(), lineDelimiter + lineDelimiter + newNodeCode)); break; } } } private void createChange(IASTNode synthNode, String synthSource) { IFile relevantFile = FileHelper.getIFilefromIASTNode(synthNode); String originalCode = originalCodeOfNode(synthNode); CodeComparer codeComparer = new CodeComparer(originalCode, synthSource); MultiTextEdit edit; if (changes.containsKey(relevantFile)) { edit = changes.get(relevantFile); } else { edit = new MultiTextEdit(); changes.put(relevantFile, edit); } codeComparer.createChange(edit, synthNode); } public String originalCodeOfNode(IASTNode node) { if (node.getFileLocation() != null) { IFile sourceFile = FileHelper.getIFilefromIASTNode(node); int nodeOffset = getOffsetIncludingComments(node); int nodeLength = getNodeLengthIncludingComments(node); return FileContentHelper.getContent(sourceFile, nodeOffset, nodeLength); } return null; } private int getNodeLengthIncludingComments(IASTNode node) { int nodeOffset = node.getFileLocation().getNodeOffset(); int nodeLength = node.getFileLocation().getNodeLength(); ArrayList<IASTComment> comments = commentMap.getAllCommentsForNode(node); if (!comments.isEmpty()) { int startOffset = nodeOffset; int endOffset = nodeOffset + nodeLength; for (IASTComment comment : comments) { IASTFileLocation commentLocation = comment.getFileLocation(); if (commentLocation.getNodeOffset() < startOffset) { startOffset = commentLocation.getNodeOffset(); } if (commentLocation.getNodeOffset() + commentLocation.getNodeLength() >= endOffset) { endOffset = commentLocation.getNodeOffset() + commentLocation.getNodeLength(); } } nodeLength = endOffset - startOffset; } return nodeLength; } private int getOffsetIncludingComments(IASTNode node) { int nodeOffset = node.getFileLocation().getNodeOffset(); ArrayList<IASTComment> comments = commentMap.getAllCommentsForNode(node); if (!comments.isEmpty()) { int startOffset = nodeOffset; for (IASTComment comment : comments) { IASTFileLocation commentLocation = comment.getFileLocation(); if (commentLocation.getNodeOffset() < startOffset) { startOffset = commentLocation.getNodeOffset(); } } nodeOffset = startOffset; } return nodeOffset; } private String getIndent(IASTNode nextNode) { IASTFileLocation fileLocation = nextNode.getFileLocation(); int length = fileLocation.getNodeOffset() - getOffsetForNodeFile(nextNode); String originalSource = FileContentHelper.getContent(FileHelper.getIFilefromIASTNode(nextNode), getOffsetForNodeFile(nextNode), length); StringBuilder indent = new StringBuilder(originalSource); indent.reverse(); String lastline = indent.substring(0, Math.max(indent.indexOf("\n"), 0)); //$NON-NLS-1$ if (lastline.trim().length() == 0) { return lastline; } return ""; //$NON-NLS-1$ } private boolean hasChangedChild(IASTNode parent) { return modificationParent.containsKey(parent); } @Override public int visit(IASTDeclarator declarator) { if (hasChangedChild(declarator)) { synthTreatment(declarator); return ASTVisitor.PROCESS_SKIP; } return super.visit(declarator); } @Override public int visit(IASTArrayModifier mod) { if (hasChangedChild(mod)) { synthTreatment(mod); return ASTVisitor.PROCESS_SKIP; } return super.visit(mod); } @Override public int visit(ICPPASTNamespaceDefinition namespaceDefinition) { if (hasChangedChild(namespaceDefinition)) { synthTreatment(namespaceDefinition); return ASTVisitor.PROCESS_SKIP; } return super.visit(namespaceDefinition); } @Override public int visit(IASTDeclSpecifier declSpec) { if (hasChangedChild(declSpec)) { synthTreatment(declSpec); return ASTVisitor.PROCESS_SKIP; } return super.visit(declSpec); } @Override public int visit(IASTExpression expression) { if (hasChangedChild(expression)) { synthTreatment(expression); return ASTVisitor.PROCESS_SKIP; } return super.visit(expression); } @Override public int visit(IASTInitializer initializer) { if (hasChangedChild(initializer)) { synthTreatment(initializer); return ASTVisitor.PROCESS_SKIP; } return super.visit(initializer); } @Override public int visit(IASTName name) { if (hasChangedChild(name)) { synthTreatment(name); return ASTVisitor.PROCESS_SKIP; } return super.visit(name); } @Override public int visit(IASTParameterDeclaration parameterDeclaration) { if (hasChangedChild(parameterDeclaration)) { synthTreatment(parameterDeclaration); return ASTVisitor.PROCESS_SKIP; } return super.visit(parameterDeclaration); } @Override public int visit(IASTStatement statement) { if (hasChangedChild(statement)) { synthTreatment(statement); return ASTVisitor.PROCESS_SKIP; } return super.visit(statement); } class CodeComparer { private final StringBuilder originalCode; private final StringBuilder synthCode; private int lastCommonInSynthStart; private int lastCommonInOriginalStart; private int firstCommonInSynthEnd; private int firstCommonInOriginalEnd; public CodeComparer(String originalCode, String synthCode) { this.originalCode = new StringBuilder(originalCode); this.synthCode = new StringBuilder(synthCode); calculatePositions(); } private void calculatePositions() { lastCommonInSynthStart = calcLastCommonPositionInSynthCode(); lastCommonInOriginalStart = calcLastCommonPositionInOriginalCode(); firstCommonInSynthEnd = calcFirstPositionOfCommonEndInSynthCode(lastCommonInSynthStart, lastCommonInOriginalStart); firstCommonInOriginalEnd = calcFirstPositionOfCommonEndInOriginalCode(lastCommonInOriginalStart, lastCommonInSynthStart); trimTrailingNewlines(); } private void trimTrailingNewlines() { int prevOrigEnd = firstCommonInOriginalEnd - 1; while (prevOrigEnd > lastCommonInOriginalStart && prevOrigEnd > -1 && isUninterresting(originalCode, prevOrigEnd)) { firstCommonInOriginalEnd = prevOrigEnd; prevOrigEnd--; } while (firstCommonInOriginalEnd > 0 && firstCommonInOriginalEnd + 1 < originalCode.length() && (originalCode.charAt(firstCommonInOriginalEnd) == ' ' || originalCode.charAt(firstCommonInOriginalEnd) == '\t')) { firstCommonInOriginalEnd++; } int prevSynthEnd = firstCommonInSynthEnd - 1; while (prevSynthEnd > lastCommonInSynthStart && prevSynthEnd > -1 && isUninterresting(synthCode, prevSynthEnd)) { firstCommonInSynthEnd = prevSynthEnd; prevSynthEnd--; } while (firstCommonInSynthEnd > 0 && firstCommonInSynthEnd + 1 < synthCode.length() && (synthCode.charAt(firstCommonInSynthEnd) == ' ' || synthCode.charAt(firstCommonInSynthEnd) == '\t')) { firstCommonInSynthEnd++; } } public int getLastCommonPositionInSynthCode() { return lastCommonInSynthStart; } public int getLastCommonPositionInOriginalCode() { return lastCommonInOriginalStart; } public int getFirstPositionOfCommonEndInOriginalCode() { return firstCommonInOriginalEnd; } public int getFirstPositionOfCommonEndInSynthCode() { return firstCommonInSynthEnd; } public int calcLastCommonPositionInSynthCode() { return findLastCommonPosition(synthCode, originalCode); } public int calcLastCommonPositionInOriginalCode() { return findLastCommonPosition(originalCode, synthCode); } private int calcFirstPositionOfCommonEndInOriginalCode(int originalLimit, int synthLimit) { StringBuilder reverseOriginalCode = new StringBuilder(originalCode).reverse(); StringBuilder reverseSynthCode = new StringBuilder(synthCode).reverse(); int lastCommonPosition = findLastCommonPosition(reverseOriginalCode, reverseSynthCode, reverseOriginalCode.length() - originalLimit - 1, reverseSynthCode.length() - synthLimit - 1); if (lastCommonPosition < 0 || lastCommonPosition >= originalCode.length()) { return -1; } return originalCode.length() - lastCommonPosition - 1; } private int calcFirstPositionOfCommonEndInSynthCode(int synthLimit, int originalLimit) { StringBuilder reverseOriginalCode = new StringBuilder(originalCode).reverse(); StringBuilder reverseSynthCode = new StringBuilder(synthCode).reverse(); int lastCommonPosition = findLastCommonPosition(reverseSynthCode, reverseOriginalCode, reverseSynthCode.length() - synthLimit - 1, reverseOriginalCode.length() - originalLimit - 1); if (lastCommonPosition < 0 || lastCommonPosition >= synthCode.length()) { return -1; } return synthCode.length() - lastCommonPosition - 1; } private int findLastCommonPosition(StringBuilder first, StringBuilder second) { return findLastCommonPosition(first, second, first.length(), second.length()); } private int findLastCommonPosition(StringBuilder first, StringBuilder second, int firstLimit, int secondLimit) { int firstIndex = -1; int secondIndex = -1; int lastCommonIndex = -1; do { lastCommonIndex = firstIndex; firstIndex = nextInterrestingPosition(first, firstIndex); secondIndex = nextInterrestingPosition(second, secondIndex); } while (firstIndex > -1 && firstIndex <= firstLimit && secondIndex > -1 && secondIndex <= secondLimit && first.charAt(firstIndex) == second.charAt(secondIndex)); return lastCommonIndex; } private int nextInterrestingPosition(StringBuilder code, int position) { do { position++; if (position >= code.length()) { return -1; } } while (isUninterresting(code, position)); return position; } private boolean isUninterresting(StringBuilder code, int position) { switch (code.charAt(position)) { case ' ': case '\n': case '\r': case '\t': return true; default: return false; } } protected void createChange(MultiTextEdit edit, IASTNode changedNode) { int changeOffset = getOffsetIncludingComments(changedNode); TextEditGroup editGroup = new TextEditGroup(Messages.ChangeGenerator_group); for (ASTModification currentModification : modificationParent.get(changedNode)) { if (currentModification.getAssociatedEditGroup() != null) { editGroup = currentModification.getAssociatedEditGroup(); edit.addChildren(editGroup.getTextEdits()); break; } } createChange(edit, changeOffset); } private void createChange(MultiTextEdit edit, int changeOffset) { int i = (firstCommonInSynthEnd >= 0 ? firstCommonInOriginalEnd : originalCode.length()) - lastCommonInOriginalStart; if (i <= 0) { String insertCode = synthCode.substring(lastCommonInSynthStart, firstCommonInSynthEnd); InsertEdit iEdit = new InsertEdit(changeOffset + lastCommonInOriginalStart, insertCode); edit.addChild(iEdit); } else if ((firstCommonInSynthEnd >= 0 ? firstCommonInSynthEnd : synthCode.length()) - lastCommonInSynthStart <= 0) { int correction = 0; if (lastCommonInSynthStart > firstCommonInSynthEnd) { correction = lastCommonInSynthStart - firstCommonInSynthEnd; } DeleteEdit dEdit = new DeleteEdit(changeOffset + lastCommonInOriginalStart, firstCommonInOriginalEnd - lastCommonInOriginalStart + correction); edit.addChild(dEdit); } else { String replacementCode = getReplacementCode(lastCommonInSynthStart, firstCommonInSynthEnd); ReplaceEdit rEdit = new ReplaceEdit( changeOffset + Math.max(lastCommonInOriginalStart, 0), (firstCommonInOriginalEnd >= 0 ? firstCommonInOriginalEnd : originalCode.length()) - Math.max(lastCommonInOriginalStart, 0), replacementCode); edit.addChild(rEdit); } } private String getReplacementCode(int lastCommonPositionInSynth, int firstOfCommonEndInSynth) { int replacementStart = Math.max(lastCommonPositionInSynth, 0); int replacementEnd = firstOfCommonEndInSynth >= 0 ? firstOfCommonEndInSynth : synthCode.length(); if (replacementStart < replacementEnd) { return synthCode.substring(replacementStart, replacementEnd); } return ""; //$NON-NLS-1$ } } public Change getChange() { return change; } }