/******************************************************************************* * Copyright (c) 2000, 2010 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.wst.jsdt.internal.corext.refactoring.rename; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.RefactoringStatusContext; import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry; import org.eclipse.ltk.core.refactoring.TextChange; import org.eclipse.ltk.core.refactoring.TextEditChangeGroup; import org.eclipse.text.edits.TextEdit; import org.eclipse.wst.jsdt.core.IJavaScriptUnit; import org.eclipse.wst.jsdt.core.IJavaScriptElement; import org.eclipse.wst.jsdt.core.ISourceRange; import org.eclipse.wst.jsdt.core.ISourceReference; import org.eclipse.wst.jsdt.core.JavaScriptModelException; import org.eclipse.wst.jsdt.core.WorkingCopyOwner; import org.eclipse.wst.jsdt.core.compiler.IProblem; import org.eclipse.wst.jsdt.core.dom.AST; import org.eclipse.wst.jsdt.core.dom.ASTNode; import org.eclipse.wst.jsdt.core.dom.ASTVisitor; import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit; import org.eclipse.wst.jsdt.core.dom.IBinding; import org.eclipse.wst.jsdt.core.dom.IVariableBinding; import org.eclipse.wst.jsdt.core.dom.Name; import org.eclipse.wst.jsdt.core.dom.SimpleName; import org.eclipse.wst.jsdt.core.dom.VariableDeclaration; import org.eclipse.wst.jsdt.core.search.FieldDeclarationMatch; import org.eclipse.wst.jsdt.core.search.MethodDeclarationMatch; import org.eclipse.wst.jsdt.core.search.SearchMatch; import org.eclipse.wst.jsdt.internal.corext.SourceRange; import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodes; import org.eclipse.wst.jsdt.internal.corext.dom.NodeFinder; import org.eclipse.wst.jsdt.internal.corext.refactoring.RefactoringCoreMessages; import org.eclipse.wst.jsdt.internal.corext.refactoring.SearchResultGroup; import org.eclipse.wst.jsdt.internal.corext.refactoring.base.JavaStatusContext; import org.eclipse.wst.jsdt.internal.corext.refactoring.base.JavaStringStatusContext; import org.eclipse.wst.jsdt.internal.corext.refactoring.util.RefactoringASTParser; import org.eclipse.wst.jsdt.internal.corext.refactoring.util.TextChangeManager; import org.eclipse.wst.jsdt.internal.corext.util.Messages; import org.eclipse.wst.jsdt.internal.corext.util.SearchUtils; class RenameAnalyzeUtil { private static class ProblemNodeFinder { private ProblemNodeFinder() { //static } public static SimpleName[] getProblemNodes(ASTNode methodNode, VariableDeclaration variableNode, TextEdit[] edits, TextChange change) { String key= variableNode.resolveBinding().getKey(); NameNodeVisitor visitor= new NameNodeVisitor(edits, change, key); methodNode.accept(visitor); return visitor.getProblemNodes(); } private static class NameNodeVisitor extends ASTVisitor { private Collection fRanges; private Collection fProblemNodes; private String fKey; public NameNodeVisitor(TextEdit[] edits, TextChange change, String key) { Assert.isNotNull(edits); Assert.isNotNull(key); fRanges= new HashSet(Arrays.asList(RefactoringAnalyzeUtil.getNewRanges(edits, change))); fProblemNodes= new ArrayList(0); fKey= key; } public SimpleName[] getProblemNodes() { return (SimpleName[]) fProblemNodes.toArray(new SimpleName[fProblemNodes.size()]); } //----- visit methods public boolean visit(SimpleName node) { VariableDeclaration decl= getVariableDeclaration(node); if (decl == null) return super.visit(node); IVariableBinding binding= decl.resolveBinding(); if (binding == null) return super.visit(node); boolean keysEqual= fKey.equals(binding.getKey()); boolean rangeInSet= fRanges.contains(new Region(node.getStartPosition(), node.getLength())); if (keysEqual && !rangeInSet) fProblemNodes.add(node); if (!keysEqual && rangeInSet) fProblemNodes.add(node); /* * if (!keyEquals && !rangeInSet) * ok, different local variable. * * if (keyEquals && rangeInSet) * ok, renamed local variable & has been renamed. */ return super.visit(node); } } } static class LocalAnalyzePackage { public final TextEdit fDeclarationEdit; public final TextEdit[] fOccurenceEdits; public LocalAnalyzePackage(final TextEdit declarationEdit, final TextEdit[] occurenceEdits) { fDeclarationEdit = declarationEdit; fOccurenceEdits = occurenceEdits; } } private RenameAnalyzeUtil() { //no instance } static RefactoringStatus analyzeRenameChanges(TextChangeManager manager, SearchResultGroup[] oldOccurrences, SearchResultGroup[] newOccurrences) { RefactoringStatus result= new RefactoringStatus(); for (int i= 0; i < oldOccurrences.length; i++) { SearchResultGroup oldGroup= oldOccurrences[i]; SearchMatch[] oldSearchResults= oldGroup.getSearchResults(); IJavaScriptUnit cunit= oldGroup.getCompilationUnit(); if (cunit == null) continue; for (int j= 0; j < oldSearchResults.length; j++) { SearchMatch oldSearchResult= oldSearchResults[j]; if (! RenameAnalyzeUtil.existsInNewOccurrences(oldSearchResult, newOccurrences, manager)){ addShadowsError(cunit, oldSearchResult, result); } } } return result; } static IJavaScriptUnit findWorkingCopyForCu(IJavaScriptUnit[] newWorkingCopies, IJavaScriptUnit cu){ IJavaScriptUnit original= cu == null ? null : cu.getPrimary(); for (int i= 0; i < newWorkingCopies.length; i++) { if (newWorkingCopies[i].getPrimary().equals(original)) return newWorkingCopies[i]; } return null; } static IJavaScriptUnit[] createNewWorkingCopies(IJavaScriptUnit[] compilationUnitsToModify, TextChangeManager manager, WorkingCopyOwner owner, SubProgressMonitor pm) throws CoreException { pm.beginTask("", compilationUnitsToModify.length); //$NON-NLS-1$ IJavaScriptUnit[] newWorkingCopies= new IJavaScriptUnit[compilationUnitsToModify.length]; for (int i= 0; i < compilationUnitsToModify.length; i++) { IJavaScriptUnit cu= compilationUnitsToModify[i]; newWorkingCopies[i]= createNewWorkingCopy(cu, manager, owner, new SubProgressMonitor(pm, 1)); } pm.done(); return newWorkingCopies; } static IJavaScriptUnit createNewWorkingCopy(IJavaScriptUnit cu, TextChangeManager manager, WorkingCopyOwner owner, SubProgressMonitor pm) throws CoreException { IJavaScriptUnit newWc= cu.getWorkingCopy(owner, null); String previewContent= manager.get(cu).getPreviewContent(new NullProgressMonitor()); newWc.getBuffer().setContents(previewContent); newWc.reconcile(IJavaScriptUnit.NO_AST, false, owner, pm); return newWc; } private static boolean existsInNewOccurrences(SearchMatch searchResult, SearchResultGroup[] newOccurrences, TextChangeManager manager) { SearchResultGroup newGroup= findOccurrenceGroup(searchResult.getResource(), newOccurrences); if (newGroup == null) return false; IRegion oldEditRange= getCorrespondingEditChangeRange(searchResult, manager); if (oldEditRange == null) return false; SearchMatch[] newSearchResults= newGroup.getSearchResults(); int oldRangeOffset = oldEditRange.getOffset(); for (int i= 0; i < newSearchResults.length; i++) { if (newSearchResults[i].getOffset() == oldRangeOffset) return true; } return false; } private static IRegion getCorrespondingEditChangeRange(SearchMatch searchResult, TextChangeManager manager) { TextChange change= getTextChange(searchResult, manager); if (change == null) return null; IRegion oldMatchRange= createTextRange(searchResult); TextEditChangeGroup[] editChanges= change.getTextEditChangeGroups(); for (int i= 0; i < editChanges.length; i++) { if (oldMatchRange.equals(editChanges[i].getRegion())) return TextEdit.getCoverage(change.getPreviewEdits(editChanges[i].getTextEdits())); } return null; } private static TextChange getTextChange(SearchMatch searchResult, TextChangeManager manager) { IJavaScriptUnit cu= SearchUtils.getCompilationUnit(searchResult); if (cu == null) return null; return manager.get(cu); } private static IRegion createTextRange(SearchMatch searchResult) { return new Region(searchResult.getOffset(), searchResult.getLength()); } private static SearchResultGroup findOccurrenceGroup(IResource resource, SearchResultGroup[] newOccurrences) { for (int i= 0; i < newOccurrences.length; i++) { if (newOccurrences[i].getResource().equals(resource)) return newOccurrences[i]; } return null; } //--- find missing changes in BOTH directions //TODO: Currently filters out declarations (MethodDeclarationMatch, FieldDeclarationMatch). //Long term solution: only pass reference search results in. static RefactoringStatus analyzeRenameChanges2(TextChangeManager manager, SearchResultGroup[] oldReferences, SearchResultGroup[] newReferences, String newElementName) { RefactoringStatus result= new RefactoringStatus(); HashMap cuToNewResults= new HashMap(newReferences.length); for (int i1= 0; i1 < newReferences.length; i1++) { IJavaScriptUnit cu= newReferences[i1].getCompilationUnit(); if (cu != null) cuToNewResults.put(cu.getPrimary(), newReferences[i1].getSearchResults()); } for (int i= 0; i < oldReferences.length; i++) { SearchResultGroup oldGroup= oldReferences[i]; SearchMatch[] oldMatches= oldGroup.getSearchResults(); IJavaScriptUnit cu= oldGroup.getCompilationUnit(); if (cu == null) continue; SearchMatch[] newSearchMatches= (SearchMatch[]) cuToNewResults.remove(cu); if (newSearchMatches == null) { for (int j = 0; j < oldMatches.length; j++) { SearchMatch oldMatch = oldMatches[j]; addShadowsError(cu, oldMatch, result); } } else { analyzeChanges(cu, manager.get(cu), oldMatches, newSearchMatches, newElementName, result); } } for (Iterator iter= cuToNewResults.entrySet().iterator(); iter.hasNext();) { Map.Entry entry= (Entry) iter.next(); IJavaScriptUnit cu= (IJavaScriptUnit) entry.getKey(); SearchMatch[] newSearchMatches= (SearchMatch[]) entry.getValue(); for (int i= 0; i < newSearchMatches.length; i++) { SearchMatch newMatch= newSearchMatches[i]; addReferenceShadowedError(cu, newMatch, newElementName, result); } } return result; } private static void analyzeChanges(IJavaScriptUnit cu, TextChange change, SearchMatch[] oldMatches, SearchMatch[] newMatches, String newElementName, RefactoringStatus result) { Map updatedOldOffsets= getUpdatedChangeOffsets(change, oldMatches); for (int i= 0; i < newMatches.length; i++) { SearchMatch newMatch= newMatches[i]; Integer offsetInNew= Integer.valueOf(newMatch.getOffset()); SearchMatch oldMatch= (SearchMatch) updatedOldOffsets.remove(offsetInNew); if (oldMatch == null) { addReferenceShadowedError(cu, newMatch, newElementName, result); } } for (Iterator iter= updatedOldOffsets.values().iterator(); iter.hasNext();) { // remaining old matches are not found any more -> they have been shadowed SearchMatch oldMatch= (SearchMatch) iter.next(); addShadowsError(cu, oldMatch, result); } } /** @return Map <Integer updatedOffset, SearchMatch oldMatch> */ private static Map getUpdatedChangeOffsets(TextChange change, SearchMatch[] oldMatches) { Map/*<Integer updatedOffset, SearchMatch oldMatch>*/ updatedOffsets= new HashMap(); Map oldToUpdatedOffsets= getEditChangeOffsetUpdates(change); for (int i= 0; i < oldMatches.length; i++) { SearchMatch oldMatch= oldMatches[i]; Integer updatedOffset= (Integer) oldToUpdatedOffsets.get(Integer.valueOf(oldMatch.getOffset())); if (updatedOffset == null) updatedOffset= Integer.valueOf(-1); //match not updated updatedOffsets.put(updatedOffset, oldMatch); } return updatedOffsets; } /** @return Map <Integer oldOffset, Integer updatedOffset> */ private static Map getEditChangeOffsetUpdates(TextChange change) { TextEditChangeGroup[] editChanges= change.getTextEditChangeGroups(); Map/*<oldOffset, newOffset>*/ offsetUpdates= new HashMap(editChanges.length); for (int i= 0; i < editChanges.length; i++) { TextEditChangeGroup editChange= editChanges[i]; IRegion oldRegion= editChange.getRegion(); if (oldRegion == null) continue; IRegion updatedRegion= TextEdit.getCoverage(change.getPreviewEdits(editChange.getTextEdits())); if (updatedRegion == null) continue; offsetUpdates.put(Integer.valueOf(oldRegion.getOffset()), Integer.valueOf(updatedRegion.getOffset())); } return offsetUpdates; } private static void addReferenceShadowedError(IJavaScriptUnit cu, SearchMatch newMatch, String newElementName, RefactoringStatus result) { //Found a new match with no corresponding old match. //-> The new match is a reference which was pointing to another element, //but that other element has been shadowed //TODO: should not have to filter declarations: if (newMatch instanceof MethodDeclarationMatch || newMatch instanceof FieldDeclarationMatch) return; ISourceRange range= getOldSourceRange(newMatch); RefactoringStatusContext context= JavaStatusContext.create(cu, range); String message= Messages.format( RefactoringCoreMessages.RenameAnalyzeUtil_reference_shadowed, new String[] {cu.getElementName(), newElementName}); result.addError(message, context); } private static ISourceRange getOldSourceRange(SearchMatch newMatch) { // cannot transfom offset in preview to offset in original -> just show enclosing method IJavaScriptElement newMatchElement= (IJavaScriptElement) newMatch.getElement(); IJavaScriptElement primaryElement= newMatchElement.getPrimaryElement(); ISourceRange range= null; if (primaryElement.exists() && primaryElement instanceof ISourceReference) { try { range= ((ISourceReference) primaryElement).getSourceRange(); } catch (JavaScriptModelException e) { // can live without source range } } return range; } private static void addShadowsError(IJavaScriptUnit cu, SearchMatch oldMatch, RefactoringStatus result) { // Old match not found in new matches -> reference has been shadowed //TODO: should not have to filter declarations: if (oldMatch instanceof MethodDeclarationMatch || oldMatch instanceof FieldDeclarationMatch) return; ISourceRange range= new SourceRange(oldMatch.getOffset(), oldMatch.getLength()); RefactoringStatusContext context= JavaStatusContext.create(cu, range); String message= Messages.format(RefactoringCoreMessages.RenameAnalyzeUtil_shadows, cu.getElementName()); result.addError(message, context); } /** * This method analyzes a set of local variable renames inside one cu. It checks whether * any new compile errors have been introduced by the rename(s) and whether the correct * node(s) has/have been renamed. * * @param analyzePackages the LocalAnalyzePackages containing the information about the local renames * @param cuChange the TextChange containing all local variable changes to be applied. * @param oldCUNode the fully (incl. bindings) resolved AST node of the original compilation unit * @param statementsRecovery whether statements recovery should be performed when parsing the changed CU * @return a RefactoringStatus containing errors if compile errors or wrongly renamed nodes are found * @throws CoreException thrown if there was an error greating the preview content of the change */ public static RefactoringStatus analyzeLocalRenames(LocalAnalyzePackage[] analyzePackages, TextChange cuChange, JavaScriptUnit oldCUNode, boolean statementsRecovery) throws CoreException { RefactoringStatus result= new RefactoringStatus(); IJavaScriptUnit compilationUnit= (IJavaScriptUnit) oldCUNode.getJavaElement(); String newCuSource= cuChange.getPreviewContent(new NullProgressMonitor()); JavaScriptUnit newCUNode= new RefactoringASTParser(AST.JLS3).parse(newCuSource, compilationUnit, true, statementsRecovery, null); result.merge(analyzeCompileErrors(newCuSource, newCUNode, oldCUNode)); if (result.hasError()) return result; for (int i= 0; i < analyzePackages.length; i++) { ASTNode enclosing= getEnclosingBlockOrMethod(analyzePackages[i].fDeclarationEdit, cuChange, newCUNode); if(enclosing == null) enclosing = newCUNode; // get new declaration IRegion newRegion= RefactoringAnalyzeUtil.getNewTextRange(analyzePackages[i].fDeclarationEdit, cuChange); ASTNode newDeclaration= NodeFinder.perform(newCUNode, newRegion.getOffset(), newRegion.getLength()); Assert.isTrue(newDeclaration instanceof Name); VariableDeclaration declaration= getVariableDeclaration((Name) newDeclaration); Assert.isNotNull(declaration); SimpleName[] problemNodes= ProblemNodeFinder.getProblemNodes(enclosing, declaration, analyzePackages[i].fOccurenceEdits, cuChange); result.merge(RefactoringAnalyzeUtil.reportProblemNodes(newCuSource, problemNodes)); } return result; } private static VariableDeclaration getVariableDeclaration(Name node) { IBinding binding= node.resolveBinding(); if (binding == null && node.getParent() instanceof VariableDeclaration) return (VariableDeclaration) node.getParent(); if (binding != null && binding.getKind() == IBinding.VARIABLE) { JavaScriptUnit cu= (JavaScriptUnit) ASTNodes.getParent(node, JavaScriptUnit.class); return ASTNodes.findVariableDeclaration( ((IVariableBinding) binding), cu); } return null; } private static ASTNode getEnclosingBlockOrMethod(TextEdit declarationEdit, TextChange change, JavaScriptUnit newCUNode) { ASTNode enclosing= RefactoringAnalyzeUtil.getBlock(declarationEdit, change, newCUNode); if (enclosing == null) enclosing= RefactoringAnalyzeUtil.getMethodDeclaration(declarationEdit, change, newCUNode); return enclosing; } private static RefactoringStatus analyzeCompileErrors(String newCuSource, JavaScriptUnit newCUNode, JavaScriptUnit oldCUNode) { RefactoringStatus result= new RefactoringStatus(); IProblem[] newProblems= RefactoringAnalyzeUtil.getIntroducedCompileProblems(newCUNode, oldCUNode); for (int i= 0; i < newProblems.length; i++) { IProblem problem= newProblems[i]; if (problem.isError()) result.addEntry(new RefactoringStatusEntry((problem.isError() ? RefactoringStatus.ERROR : RefactoringStatus.WARNING), problem.getMessage(), new JavaStringStatusContext(newCuSource, new SourceRange(problem)))); } return result; } }