/******************************************************************************* * Copyright (c) 2008, 2015 Zend Technologies 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: * Zend Technologies - initial API and implementation *******************************************************************************/ package org.eclipse.php.refactoring.core.extract.function; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.*; import org.eclipse.dltk.ast.Modifiers; import org.eclipse.dltk.core.Flags; import org.eclipse.dltk.core.IScriptProject; import org.eclipse.dltk.core.ISourceModule; import org.eclipse.jface.text.IDocument; import org.eclipse.ltk.core.refactoring.*; import org.eclipse.php.core.ast.nodes.*; import org.eclipse.php.core.ast.visitor.AbstractVisitor; import org.eclipse.php.internal.core.ast.locator.PHPElementConciliator; import org.eclipse.php.internal.core.ast.rewrite.ASTRewrite; import org.eclipse.php.internal.core.ast.rewrite.ASTRewriteFlattener; import org.eclipse.php.internal.core.ast.rewrite.ListRewrite; import org.eclipse.php.internal.core.ast.scanner.php5.PHPAstLexer; import org.eclipse.php.internal.core.ast.util.Util; import org.eclipse.php.internal.core.corext.dom.Selection; import org.eclipse.php.internal.ui.corext.util.Resources; import org.eclipse.php.refactoring.core.LinkedNodeFinder; import org.eclipse.php.refactoring.core.PhpRefactoringCoreMessages; import org.eclipse.php.refactoring.core.changes.ProgramDocumentChange; import org.eclipse.php.refactoring.core.extract.function.SnippetFinder.Match; import org.eclipse.php.refactoring.core.utils.ASTUtils; import org.eclipse.php.refactoring.core.utils.RefactoringUtility; import org.eclipse.php.ui.CodeGeneration; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.text.edits.TextEditGroup; public class ExtractFunctionRefactoring extends Refactoring { private ISourceModule sourceModule = null; private IDocument document = null; // private IExpressionFragment fSelectedExpression; private int selectionStartOffset; private int selectionLength; private Program astRoot; private boolean fReplaceAllOccurrences; private String fNewFunctionName = null; protected String[] arguments = null; private DocumentChange textFileChange = null; private RefactoringStatus matchingFragmentsStatus; /** * The root change for all changes */ protected CompositeChange rootChange; private ASTRewrite fRewriter; private AST fAST; private ExtractFunctionAnalyzer fAnalyzer; private ArrayList<ParameterInfo> fParameterInfos; private int fVisibility; private Match[] fDuplicates; private boolean fReplaceDuplicates; private boolean fGeneratePHPDoc; public ExtractFunctionRefactoring(ISourceModule sourceModule, IDocument document, int offset, int length) { this.sourceModule = sourceModule; this.document = document; this.selectionStartOffset = offset; this.selectionLength = length; fReplaceAllOccurrences = true; // default fVisibility = -1; fNewFunctionName = "extracted"; //$NON-NLS-1$ } /** * Sets the new variable name (given by the user) * * @param newVariableName */ public void setNewFunctionName(String newVariableName) { this.fNewFunctionName = newVariableName; } /** * Sets the value for replace all occurrences (given by the user) * * @param replaceAllOccurrences */ public void setReplaceDuplicates(boolean replaceAllOccurrences) { this.fReplaceAllOccurrences = replaceAllOccurrences; } public boolean getReplaceDuplicates() { return fReplaceAllOccurrences; } @Override public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { try { pm.beginTask("", 8); //$NON-NLS-1$ // check if the file is in sync RefactoringStatus status = validateModifiesFiles(new IResource[] { sourceModule.getResource() }, getValidationContext()); if (status.hasFatalError()) { return status; } try { astRoot = ASTUtils.createProgramFromSource(sourceModule); final Reader reader = new StringReader(document.get()); astRoot.initCommentMapper(document, new PHPAstLexer(reader)); fAST = astRoot.getAST(); astRoot.accept(createVisitor()); } catch (Exception e) { return RefactoringStatus .createFatalErrorStatus(PhpRefactoringCoreMessages.getString("ExtractFunctionRefactoring.10")); //$NON-NLS-1$ } status.merge(fAnalyzer.checkInitialConditions()); status.merge(fAnalyzer.checkSelection(status, new SubProgressMonitor(pm, 3))); if (status.hasFatalError()) return status; if (fVisibility == -1) { setVisibility(Modifiers.AccPrivate); } initializeParameterInfos(); initializeDuplicates(); return status; } finally { pm.done(); } } public boolean isClassMethod() { return this.fAnalyzer.getEnclosingBodyDeclaration() instanceof MethodDeclaration; } public boolean isStaticMethod() { if (this.fAnalyzer.getEnclosingBodyDeclaration() instanceof MethodDeclaration) { return Flags.isStatic(((MethodDeclaration) fAnalyzer.getEnclosingBodyDeclaration()).getModifier()); } return false; } private void initializeDuplicates() { ASTNode start = fAnalyzer.getEnclosingBodyDeclaration(); while (isClassMethod() && !(start instanceof ClassDeclaration)) { start = start.getParent(); } fDuplicates = SnippetFinder.perform(start, fAnalyzer.getSelectedNodes()); fReplaceDuplicates = fDuplicates.length > 0; } private void initializeParameterInfos() { IVariableBinding[] arguments = fAnalyzer.getArguments(); if (arguments != null) { fParameterInfos = new ArrayList<ParameterInfo>(arguments.length); for (int i = 0; i < arguments.length; i++) { IVariableBinding argument = arguments[i]; if (argument == null) continue; ParameterInfo info = new ParameterInfo(argument, argument.getName(), i); fParameterInfos.add(info); } } } private AbstractVisitor createVisitor() throws CoreException, IOException { fAnalyzer = new ExtractFunctionAnalyzer(astRoot, sourceModule, document, Selection.createFromStartLength(selectionStartOffset, selectionLength)); return fAnalyzer; } public ASTNode[] getSelectedNodes() { return fAnalyzer.getSelectedNodes(); } // -------- validateEdit checks ---- public static RefactoringStatus validateModifiesFiles(IResource[] filesToModify, Object context) { RefactoringStatus result = new RefactoringStatus(); IStatus status = Resources.checkInSync(filesToModify); if (!status.isOK()) result.merge(RefactoringStatus.create(status)); status = Resources.makeCommittable(filesToModify, context); if (!status.isOK()) { result.merge(RefactoringStatus.create(status)); if (!result.hasFatalError()) { result.addFatalError(PhpRefactoringCoreMessages.getString("ExtractFunctionRefactoring.11")); //$NON-NLS-1$ } } return result; } @Override public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { RefactoringStatus status = new RefactoringStatus(); // createChange(pm); status.merge(matchingFragmentsStatus); status.merge(doesNameAlreadyExist(fNewFunctionName)); return status; } /** * Checks whether the user given name already exists in the visible scope * * @return the status including necessary warnings */ private RefactoringStatus doesNameAlreadyExist(String name) { RefactoringStatus status = new RefactoringStatus(); // if the selection is enclosed by a function, // check if the user given variable name already exists in the function // scope if (fAnalyzer.getEnclosingBodyDeclaration().getType() == ASTNode.FUNCTION_DECLARATION || fAnalyzer.getEnclosingBodyDeclaration().getType() == ASTNode.METHOD_DECLARATION) { if (PHPElementConciliator.functionAlreadyExists(astRoot, name)) { status.addWarning(PhpRefactoringCoreMessages.getString("ExtractFunctionRefactoring.3")); //$NON-NLS-1$ } } return status; } @Override public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException { try { pm.beginTask(PhpRefactoringCoreMessages.getString("ExtractFunctionRefactoring"), 1); //$NON-NLS-1$ ASTNode declaration = fAnalyzer.getEnclosingBodyDeclaration(); fRewriter = ASTRewrite.create(declaration.getAST()); rootChange = new CompositeChange(PhpRefactoringCoreMessages.format("ExtractFunctionRefactoring.2", //$NON-NLS-1$ new String[] { fNewFunctionName })); rootChange.markAsSynthetic(); MultiTextEdit root = new MultiTextEdit(); textFileChange = new ProgramDocumentChange(PhpRefactoringCoreMessages.format("ExtractFunctionRefactoring.2", //$NON-NLS-1$ new String[] { fNewFunctionName }), document, astRoot); textFileChange.setEdit(root); textFileChange.setTextType("php"); //$NON-NLS-1$ rootChange.add(textFileChange); ASTNode[] selectedNodes = fAnalyzer.getSelectedNodes(); TextEditGroup substituteDesc = new TextEditGroup(PhpRefactoringCoreMessages .format("ExtractFunctionRefactoring.2", new String[] { fNewFunctionName })); //$NON-NLS-1$ textFileChange.addTextEditGroup(substituteDesc); String lineDelimiter = Util.getLineSeparator(astRoot.getSourceModule().getSource(), astRoot.getSourceModule().getScriptProject()); FunctionDeclaration function = createNewFunction(selectedNodes, lineDelimiter, substituteDesc); MethodDeclaration method = null; Comment funcComment = null; if (isClassMethod()) { method = fAST.newMethodDeclaration(); method.setFunction(function); int flags = fVisibility; if (isStaticMethod()) { flags = flags | Modifiers.AccStatic; } method.setModifier(flags); if (fGeneratePHPDoc) { IScriptProject sp = fAnalyzer.getEnclosingBodyDeclaration().getProgramRoot().getSourceModule() .getScriptProject(); ITypeBinding classDecl = ((MethodDeclaration) fAnalyzer.getEnclosingBodyDeclaration()) .resolveMethodBinding().getDeclaringClass(); List<FormalParameter> parameters = method.getFunction().formalParameters(); String[] paramNames = new String[parameters.size()]; for (int i = 0; i < parameters.size(); i++) { Expression name = parameters.get(i).getParameterName(); if (name instanceof Scalar) { paramNames[i] = removeDollar(((Scalar) name).getStringValue()); } if (name instanceof Identifier) { paramNames[i] = ((Identifier) name).getName(); } } String comments = CodeGeneration.getMethodComment(sp, classDecl.getName(), method.getFunction().getFunctionName().getName(), paramNames, null, null, null, lineDelimiter, null); Comment commentNode = (Comment) fRewriter.createStringPlaceholder(comments, ASTNode.COMMENT); commentNode.setCommentType(Comment.TYPE_PHPDOC); method.setComment(commentNode); } } else { if (fGeneratePHPDoc) { IScriptProject sp = fAnalyzer.getEnclosingBodyDeclaration().getProgramRoot().getSourceModule() .getScriptProject(); List<FormalParameter> parameters = function.formalParameters(); String[] paramNames = new String[parameters.size()]; for (int i = 0; i < parameters.size(); i++) { Expression name = parameters.get(i).getParameterName(); if (name instanceof Scalar) { paramNames[i] = removeDollar(((Scalar) name).getStringValue()); } if (name instanceof Identifier) { paramNames[i] = ((Identifier) name).getName(); } } String comments = CodeGeneration.getMethodComment(sp, "", //$NON-NLS-1$ function.getFunctionName().getName(), paramNames, null, null, null, lineDelimiter, null); comments = lineDelimiter + comments + lineDelimiter; funcComment = (Comment) fRewriter.createStringPlaceholder(comments, ASTNode.COMMENT); funcComment.setCommentType(Comment.TYPE_PHPDOC); } } TextEditGroup insertDesc = new TextEditGroup( PhpRefactoringCoreMessages.getString("ExtractFunctionRefactoring.4")); //$NON-NLS-1$ textFileChange.addTextEditGroup(insertDesc); ChildListPropertyDescriptor desc = (ChildListPropertyDescriptor) declaration.getLocationInParent(); ListRewrite container = null; if (declaration instanceof Program) { container = fRewriter.getListRewrite(declaration, Program.STATEMENTS_PROPERTY); } else { container = fRewriter.getListRewrite(declaration.getParent(), desc); } if (method != null) { container.insertAfter(method, declaration, insertDesc); } else { if (declaration instanceof Program) { // This is a work around to add the new function before the // empty statement of the Program. List<Statement> statements = ((Program) declaration).statements(); int length = statements.size(); // Since the program at least has the select expression and // a empty statement, // it's safe for assuming the length > 1. // Work ground for now. Statement node = statements.get(length - 1); if (length >= 2 && (node instanceof InLineHtml)) { container.insertBefore(function, (ASTNode) statements.get(length - 2), insertDesc); if (funcComment != null) { container.insertBefore(funcComment, function, insertDesc); } } else if (length > 1 && node instanceof EmptyStatement) { container.insertBefore(function, (ASTNode) statements.get(length - 1), insertDesc); if (funcComment != null) { container.insertBefore(funcComment, function, insertDesc); } } else { container.insertLast(function, insertDesc); if (funcComment != null) { container.insertBefore(funcComment, function, insertDesc); } } } else { container.insertAfter(function, declaration, insertDesc); if (funcComment != null) { container.insertBefore(funcComment, function, insertDesc); } } } if (getReplaceDuplicates()) { replaceDuplicates(textFileChange); } TextEdit edit = fRewriter.rewriteAST(document, null); root.addChild(edit); return rootChange; } finally { pm.done(); } } private void replaceDuplicates(DocumentChange textFileChange2) { int numberOf = getNumberOfDuplicates(); if (numberOf == 0 || !fReplaceDuplicates) return; String label = null; if (numberOf == 1) label = PhpRefactoringCoreMessages.format("ExtractFunctionRefactoring.5", //$NON-NLS-1$ new String[] { fNewFunctionName }); else label = PhpRefactoringCoreMessages.format("ExtractFunctionRefactoring.6", //$NON-NLS-1$ new String[] { fNewFunctionName }); TextEditGroup description = new TextEditGroup(label); textFileChange2.addTextEditGroup(description); for (int d = 0; d < fDuplicates.length; d++) { SnippetFinder.Match duplicate = fDuplicates[d]; if (!duplicate.isMethodBody()) { ASTNode[] callNodes = createCallNodes(duplicate); ASTNode[] nodes = duplicate.getNodes(); for (ASTNode node : nodes) { fRewriter.replace(node, callNodes[0], description); } } } } private FunctionDeclaration createNewFunction(ASTNode[] selectedNodes, String lineSeparator, TextEditGroup substitute) { FunctionDeclaration result = createNewFunctionDeclaration(); result.setBody(createFunctionBody(selectedNodes, substitute)); return result; } private Block createFunctionBody(ASTNode[] selectedNodes, TextEditGroup substitute) { Block result = fAST.newBlock(); ListRewrite statements = fRewriter.getListRewrite(result, Block.STATEMENTS_PROPERTY); for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter.hasNext();) { ParameterInfo parameter = iter.next(); if (parameter.isRenamed()) { for (int n = 0; n < selectedNodes.length; n++) { Identifier[] oldNames = LinkedNodeFinder.findByBinding(selectedNodes[n], parameter.getOldBinding()); for (int i = 0; i < oldNames.length; i++) { fRewriter.replace(oldNames[i], fAST.newIdentifier(removeDollar(parameter.getNewName())), substitute); } } } } boolean extractsExpression = fAnalyzer.isExpressionSelected(); ASTNode[] callNodes = createCallNodes(null); ASTNode replacementNode; if (callNodes.length == 1) { replacementNode = callNodes[0]; } else { replacementNode = fRewriter.createGroupNode(callNodes); } if (extractsExpression) { // if we have an expression then only one node is selected. ReturnStatement rs = fAST.newReturnStatement(); rs.setExpression((Expression) fRewriter.createMoveTarget(selectedNodes[0])); statements.insertLast(rs, null); fRewriter.replace(selectedNodes[0], replacementNode, substitute); } else { if (selectedNodes.length == 1) { statements.insertLast(fRewriter.createMoveTarget(selectedNodes[0]), substitute); fRewriter.replace(selectedNodes[0], replacementNode, substitute); } else { ListRewrite source = fRewriter.getListRewrite(selectedNodes[0].getParent(), (ChildListPropertyDescriptor) selectedNodes[0].getLocationInParent()); ASTNode[] nodes = filterComments(selectedNodes); if (nodes.length > 0) { ASTNode toMove = source.createMoveTarget(nodes[0], selectedNodes[nodes.length - 1], replacementNode, substitute); statements.insertLast(toMove, substitute); } } IVariableBinding returnValue = fAnalyzer.getReturnValue(); if (returnValue != null) { ReturnStatement rs = fAST.newReturnStatement(); rs.setExpression(fAST.newIdentifier((getName(returnValue)))); statements.insertLast(rs, null); } } return result; } private ASTNode[] filterComments(ASTNode[] selectedNodes) { ArrayList<ASTNode> nodes = new ArrayList<ASTNode>(selectedNodes.length); for (ASTNode node : selectedNodes) { if (!(node instanceof Comment)) { nodes.add(node); } } return nodes.toArray(new ASTNode[nodes.size()]); } private String getName(IVariableBinding binding) { for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter.hasNext();) { ParameterInfo info = iter.next(); if (binding.equals(info.getOldBinding())) { return info.getNewName(); } } return binding.getName(); } @SuppressWarnings("unchecked") private ASTNode[] createCallNodes(Object duplicate) { List result = new ArrayList(2); Expression invocation = null; List arguments = null; if (isClassMethod()) { if (isStaticMethod()) { invocation = fAST.newStaticMethodInvocation(); FunctionInvocation funcInv = fAST.newFunctionInvocation(); funcInv.setFunctionName(fAST.newFunctionName(fAST.newIdentifier(fNewFunctionName))); ((StaticMethodInvocation) invocation).setMethod(funcInv); ((StaticMethodInvocation) invocation).setClassName(fAST.newIdentifier("self")); //$NON-NLS-1$ arguments = ((StaticMethodInvocation) invocation).getMethod().parameters(); } else { invocation = fAST.newMethodInvocation(); FunctionInvocation funcInv = fAST.newFunctionInvocation(); funcInv.setFunctionName(fAST.newFunctionName(fAST.newIdentifier(fNewFunctionName))); ((MethodInvocation) invocation).setMethod(funcInv); ((MethodInvocation) invocation).setDispatcher(fAST.newVariable("this")); //$NON-NLS-1$ arguments = ((MethodInvocation) invocation).getMethod().parameters(); } } else { invocation = fAST.newFunctionInvocation(); ((FunctionInvocation) invocation) .setFunctionName(fAST.newFunctionName(fAST.newIdentifier(fNewFunctionName))); arguments = ((FunctionInvocation) invocation).parameters(); } for (int i = 0; i < fParameterInfos.size(); i++) { ParameterInfo parameter = (ParameterInfo) fParameterInfos.get(i); arguments.add(fAST.newScalar(parameter.getOldName())); } ASTNode call; int returnKind = fAnalyzer.getReturnKind(); switch (returnKind) { case ExtractFunctionAnalyzer.ACCESS_TO_LOCAL: Assignment assignment = fAST.newAssignment(); assignment.setLeftHandSide(fAST.newVariable(removeDollar(fAnalyzer.getReturnValue().getName()))); assignment.setRightHandSide(invocation); call = assignment; break; case ExtractFunctionAnalyzer.RETURN_STATEMENT_VALUE: ReturnStatement rs = fAST.newReturnStatement(); rs.setExpression(invocation); call = rs; break; default: call = invocation; } if (call instanceof Expression && !fAnalyzer.isExpressionSelected()) { call = fAST.newExpressionStatement((Expression) call); } result.add(call); return (ASTNode[]) result.toArray(new ASTNode[result.size()]); } private String removeDollar(String name) { String value = name; if (name != null && name.length() > 0 && name.startsWith("$")) { //$NON-NLS-1$ value = name.substring(1); } return value; } private FunctionDeclaration createNewFunctionDeclaration() { FunctionDeclaration result = fAST.newFunctionDeclaration(); result.setFunctionName(fAST.newIdentifier(this.fNewFunctionName)); List<FormalParameter> parameters = result.formalParameters(); for (int i = 0; i < fParameterInfos.size(); i++) { ParameterInfo info = (ParameterInfo) fParameterInfos.get(i); FormalParameter parameter = fAST.newFormalParameter(); parameter.setParameterName(fAST.newScalar(info.getNewName())); parameters.add(parameter); } return result; } // private Variable getVariableDeclaration(ParameterInfo parameter) { // return ((VariableBinding) parameter.getOldBinding()).getVarialbe(); // } /* * (non-Javadoc) * * @see org.eclipse.ltk.core.refactoring.Refactoring#getName() */ @Override public String getName() { return PhpRefactoringCoreMessages.getString("ExtractFunctionRefactoring.8"); //$NON-NLS-1$ } // public Change getChange() { // return rootChange; // } /** * @param text * @return */ public RefactoringStatus checkNewVariableName(String text) { return doesNameAlreadyExist(text); } /** * @return */ public int getVisibility() { return fVisibility; } public List<ParameterInfo> getParameterInfos() { return fParameterInfos; } public int getNumberOfDuplicates() { return fDuplicates.length; } public void setVisibility(int intValue) { fVisibility = intValue; } public boolean getGeneratePHPdoc() { return fGeneratePHPDoc; } public void setGeneratePHPdoc(boolean value) { fGeneratePHPDoc = value; } /** * Returns the signature of the new method. * * @return the signature of the extracted method */ public String getSignature() { return getSignature(fNewFunctionName); } /** * Returns the signature of the new method. * * @param methodName * the method name used for the new method * @return the signature of the extracted method */ public String getSignature(String methodName) { FunctionDeclaration methodDecl = createNewFunctionDeclaration(); methodDecl.setBody(fAST.newBlock()); methodDecl.getFunctionName(); String str = ASTRewriteFlattener.asString(methodDecl, null); return str.substring(0, str.indexOf('{')); } public void setMethodName(String text) { this.fNewFunctionName = text; } public RefactoringStatus checkFunctionName() { RefactoringStatus status = new RefactoringStatus(); status.merge(RefactoringUtility.checkNewElementName(fNewFunctionName)); status.merge(doesNameAlreadyExist(fNewFunctionName)); return status; } public RefactoringStatus checkParameterNames() { RefactoringStatus status = new RefactoringStatus(); status.merge(RefactoringUtility.checkNewElementName(fNewFunctionName)); status.merge(doesNameAlreadyExist(fNewFunctionName)); return status; } }