blob: 8e22688832b554ddbd90e6e715299dc912484080 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2008 IBM Corporation and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Dmitry Stalnov (dstalnov@fusionone.com) - contributed fixes for:
* o inline call that is used in a field initializer
* (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38137)
* o Allow 'this' constructor to be inlined
* (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38093)
*******************************************************************************/
package org.eclipse.dltk.internal.javascript.corext.refactoring.code;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.dltk.core.IMethod;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.manipulation.RefactoringChecks;
import org.eclipse.dltk.core.manipulation.SourceModuleChange;
import org.eclipse.dltk.internal.corext.refactoring.ScriptRefactoringDescriptor;
import org.eclipse.dltk.internal.corext.refactoring.changes.DynamicValidationRefactoringChange;
import org.eclipse.dltk.internal.corext.refactoring.util.TextChangeManager;
import org.eclipse.dltk.internal.javascript.core.manipulation.JavascriptManipulationPlugin;
import org.eclipse.dltk.internal.javascript.core.manipulation.Messages;
import org.eclipse.dltk.internal.javascript.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.dltk.javascript.core.dom.CallExpression;
import org.eclipse.dltk.javascript.core.dom.FunctionExpression;
import org.eclipse.dltk.javascript.core.dom.Node;
import org.eclipse.dltk.javascript.core.dom.Source;
import org.eclipse.dltk.javascript.core.dom.rewrite.ASTConverter;
import org.eclipse.dltk.javascript.core.dom.rewrite.NodeFinder;
import org.eclipse.dltk.javascript.core.dom.rewrite.RefactoringUtils;
import org.eclipse.dltk.javascript.core.dom.rewrite.RewriteAnalyzer;
import org.eclipse.dltk.javascript.core.refactoring.descriptors.InlineMethodDescriptor;
import org.eclipse.dltk.javascript.parser.JavaScriptParserUtil;
import org.eclipse.emf.ecore.change.ChangeDescription;
import org.eclipse.emf.ecore.change.util.ChangeRecorder;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.participants.ResourceChangeChecker;
import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
/*
* Open items:
* - generate import statements for newly generated local variable declarations.
* - forbid cases like foo(foo(10)) when inlining foo().
* - case ref.foo(); and we want to inline foo. Inline a method in a different context;
* - optimize code when the method to be inlined returns an argument and that one is
* assigned to a parameter again. No need for a separate local (important to be able
* to reverse extract method correctly).
*/
public class InlineMethodRefactoring extends Refactoring {
private static final String ATTRIBUTE_MODE= "mode"; //$NON-NLS-1$
private static final String ATTRIBUTE_DELETE= "delete"; //$NON-NLS-1$
public enum Mode {
ALL,SINGLE;
}
// constructor
private ISourceModule cu;
private int offset;
private int length;
// initial conditions
private IMethod method;
private Mode initialMode;
private Mode currentMode;
private boolean deleteSource;
private Source declRoot;
private Node declNode;
private SourceProvider sourceProvider;
private TargetProvider targetProvider;
// final conditions
private TextChangeManager changeManager;
public InlineMethodRefactoring(ISourceModule cu, int offset, int length) {
this.cu = cu;
this.offset = offset;
this.length = length;
deleteSource= false;
}
public String getName() {
return RefactoringCoreMessages.InlineMethodRefactoring_name;
}
/**
* Returns the method to inline, or null if the method could not be found or
* {@link #checkInitialConditions(IProgressMonitor)} has not been called yet.
*
* @return the method, or <code>null</code>
*/
public IMethod getMethod() {
return method;
}
public boolean canEnableDeleteSource() {
return !method.isReadOnly();
}
/*public boolean getDeleteSource() {
return fDeleteSource;
}*/
public void setDeleteSource(boolean remove) {
if (remove)
Assert.isTrue(canEnableDeleteSource());
deleteSource= remove;
}
public Mode getInitialMode() {
return initialMode;
}
public RefactoringStatus setCurrentMode(Mode mode) {
if (currentMode == mode)
return new RefactoringStatus();
Assert.isTrue(getInitialMode() == Mode.SINGLE);
currentMode= mode;
/*if (mode == Mode.INLINE_SINGLE) {
if (fInitialNode instanceof MethodInvocation)
fTargetProvider= TargetProvider.create((ICompilationUnit) fInitialTypeRoot, (MethodInvocation)fInitialNode);
else if (fInitialNode instanceof SuperMethodInvocation)
fTargetProvider= TargetProvider.create((ICompilationUnit) fInitialTypeRoot, (SuperMethodInvocation)fInitialNode);
else if (fInitialNode instanceof ConstructorInvocation)
fTargetProvider= TargetProvider.create((ICompilationUnit) fInitialTypeRoot, (ConstructorInvocation)fInitialNode);
else
throw new IllegalStateException(String.valueOf(fInitialNode));
} else {
fTargetProvider= TargetProvider.create(fSourceProvider.getDeclaration());
}
return fTargetProvider.checkActivation();*/
return new RefactoringStatus();
}
/**
* Creates a new inline method refactoring
* @param unit the compilation unit or class file
* @param selectionStart start
* @param selectionLength length
* @return returns the refactoring
*/
/*public static InlineMethodRefactoring create(ISourceModule unit, int selectionStart, int selectionLength) {
try {
IModelElement[] elements = unit.codeSelect(selectionStart, selectionLength);
if (elements.length != 1 || !(elements[0] instanceof IMethod))
return null;
IMethod method = (IMethod)elements[0];
if (method.getSourceRange().getOffset() == selectionStart && method.getSourceModule() == unit) {
return new InlineMethodRefactoring(unit, method, selectionStart, selectionLength, Mode.ALL);
} else {
return new InlineMethodRefactoring(unit, method, selectionStart, selectionLength, Mode.SINGLE);
}
} catch (ModelException e) {
JavascriptManipulationPlugin.log(e);
}
return null;
}*/
public RefactoringStatus checkInitialConditions(IProgressMonitor pm) {
RefactoringStatus result= new RefactoringStatus();
if (method == null) {
try {
IModelElement[] elements = cu.codeSelect(offset, length);
if (elements.length != 1 || !(elements[0] instanceof IMethod))
return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.InlineMethodRefactoring_error_noMethodDeclaration);
method = (IMethod)elements[0];
int start = method.getNameRange().getOffset();
if (start <= offset && offset <= start+method.getNameRange().getLength() && method.getSourceModule() == cu)
initialMode = Mode.ALL;
else
initialMode = Mode.SINGLE;
currentMode = initialMode;
declRoot = (Source)ASTConverter.convert(JavaScriptParserUtil.parse(method.getSourceModule()));
declNode = NodeFinder.findNode(declRoot, method.getNameRange());
FunctionExpression expr = RefactoringUtils.getFunctionDeclaration(declNode);
if (expr == null)
return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.InlineMethodRefactoring_error_noMethodDeclaration);
sourceProvider = new SourceProvider(expr, method);
if (result.hasFatalError())
return result;
} catch (ModelException e) {
JavascriptManipulationPlugin.log(e);
return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.InlineMethodRefactoring_error_noMethodDeclaration);
}
}
result.merge(sourceProvider.checkActivation());
return result;
}
public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException {
pm.beginTask("", 20); //$NON-NLS-1$
changeManager= new TextChangeManager();
RefactoringStatus result= new RefactoringStatus();
sourceProvider.initialize();
if (currentMode == Mode.ALL)
targetProvider = TargetProvider.create(method);
else {
targetProvider = TargetProvider.create(cu, offset, length);
deleteSource = false;
}
//fTargetProvider.initialize();
pm.setTaskName(RefactoringCoreMessages.InlineMethodRefactoring_searching);
RefactoringStatus searchStatus= new RefactoringStatus();
/*String binaryRefsDescription= Messages.format(RefactoringCoreMessages.ReferencesInBinaryContext_ref_in_binaries_description , BasicElementLabels.getJavaElementName(fSourceProvider.getMethodName()));
ReferencesInBinaryContext binaryRefs= new ReferencesInBinaryContext(binaryRefsDescription);*/
List<ISourceModule> units= targetProvider.getAffectedSourceModules(searchStatus, new SubProgressMonitor(pm, 1));
//binaryRefs.addErrorIfNecessary(searchStatus);
if (searchStatus.hasFatalError()) {
result.merge(searchStatus);
return result;
}
IFile[] filesToBeModified= getFilesToBeModified(units);
result.merge(RefactoringChecks.validateModifiesFiles(filesToBeModified, getValidationContext()));
if (result.hasFatalError())
return result;
result.merge(ResourceChangeChecker.checkFilesToBeChanged(filesToBeModified, new SubProgressMonitor(pm, 1)));
// TODO check overrides
//checkOverridden(result, new SubProgressMonitor(pm, 4));
IProgressMonitor sub= new SubProgressMonitor(pm, 15);
sub.beginTask("", (units.size()+1) * 3); //$NON-NLS-1$
boolean occursInDeclUnit = false;
for(ISourceModule unit : units) {
if (!unit.equals(method.getSourceModule()))
processUnit(unit,sub,result);
else
occursInDeclUnit = true;
}
if (occursInDeclUnit)
sub.worked(3);
if (deleteSource || occursInDeclUnit)
processUnit(method.getSourceModule(),sub,result);
result.merge(searchStatus);
sub.done();
pm.done();
return result;
}
private void processUnit(ISourceModule unit, IProgressMonitor pm, RefactoringStatus result) throws CoreException {
pm.subTask(Messages.format(RefactoringCoreMessages.InlineMethodRefactoring_processing, BasicElementLabels.getResourceName(unit.getCorrespondingResource())));
CallInliner inliner= null;
boolean modified = false;
Source root = targetProvider.getRoot(unit);
ChangeRecorder cr = new ChangeRecorder(targetProvider.getRoot(unit));
Node[] bodies=targetProvider.getAffectedBodyDeclarations(unit, new SubProgressMonitor(pm, 1));
if (bodies.length == 0)
return;
inliner = new CallInliner(unit, sourceProvider);
for(Node body : bodies) {
inliner.initialize(body);
RefactoringStatus nestedInvocations= new RefactoringStatus();
CallExpression[] invocations= removeNestedCalls(nestedInvocations, unit,
targetProvider.getInvocations(body, new SubProgressMonitor(pm, 2)));
for(CallExpression invocation : invocations) {
RefactoringStatus checkInline = inliner.initialize(invocation, targetProvider.getStatusSeverity());
result.merge(checkInline);
if (result.hasFatalError())
break;
if (checkInline.getSeverity() >= targetProvider.getStatusSeverity()) {
deleteSource= false;
} else {
modified = true;
inliner.perform();
}
}
// do this after we have inlined the method calls. We still want
// to generate the modifications.
if (!nestedInvocations.isOK()) {
result.merge(nestedInvocations);
deleteSource=false;
}
}
if (deleteSource && unit.equals(method.getSourceModule())) {
modified = true;
Node node = sourceProvider.getDeclaration();
node = NodeFinder.findNode(root, node.getBegin(), node.getEnd());
while (!node.eContainmentFeature().isMany())
node = (Node)node.eContainer();
List<? extends Node> list = (List<? extends Node>)node.eContainer().eGet(node.eContainmentFeature());
list.remove(node);
}
if (modified) {
ChangeDescription cd = cr.endRecording();
RewriteAnalyzer rewrite = new RewriteAnalyzer(cd, unit.getSource());
rewrite.rewrite(root);
cd.apply();
TextChange change = new SourceModuleChange(unit.getElementName(), unit);
change.setEdit(rewrite.getEdit());
changeManager.manage(unit, change);
} else
cr.endRecording();
/*if (!added) {
changeManager.remove(unit);
} else {
root.addChild(inliner.getModifications());
ImportRewrite rewrite= inliner.getImportEdit();
if (rewrite.hasRecordedChanges()) {
TextEdit edit= rewrite.rewriteImports(null);
if (edit instanceof MultiTextEdit ? ((MultiTextEdit)edit).getChildrenSize() > 0 : true) {
root.addChild(edit);
change.addTextEditGroup(
new TextEditGroup(RefactoringCoreMessages.InlineMethodRefactoring_edit_import, new TextEdit[] {edit}));
}
}
}*/
pm.worked(1);
if (pm.isCanceled())
throw new OperationCanceledException();
}
public Change createChange(IProgressMonitor pm) throws CoreException {
//if (deleteSource && currentMode == Mode.ALL) {
//TextChange change= changeManager.get((ISourceModule) sourceProvider.getTypeRoot());
//TextEdit delete= sourceProvider.getDeleteEdit();
//TextEditGroup description= new TextEditGroup(
// RefactoringCoreMessages.InlineMethodRefactoring_edit_delete, new TextEdit[] { delete });
//TextEdit root= change.getEdit();
//if (root != null) {
// TODO instead of finding the right insert position the call inliner should
// reuse the AST & rewriter of the source provide and we should rewrite the
// whole AST at the end. However, since recursive calls aren't allowed there
// shouldn't be a text edit overlap.
// root.addChild(delete);
//TextChangeCompatibility.insert(root, delete);
//} else {
//change.setEdit(delete);
//}
//change.addTextEditGroup(description);
//}
final Map arguments= new HashMap();
String project = null;
IScriptProject scriptProject= cu.getScriptProject();
if (scriptProject != null)
project= scriptProject.getElementName();
int flags= RefactoringDescriptor.STRUCTURAL_CHANGE | ScriptRefactoringDescriptor.ARCHIVE_REFACTORABLE | ScriptRefactoringDescriptor.ARCHIVE_IMPORTABLE;
//final IMethodBinding binding= sourceProvider.getDeclaration().resolveBinding();
//final ITypeBinding declaring= binding.getDeclaringClass();
//if (!Modifier.isPrivate(binding.getModifiers()))
// flags|= RefactoringDescriptor.MULTI_CHANGE;
final String description= Messages.format(RefactoringCoreMessages.InlineMethodRefactoring_descriptor_description_short, method.getElementName());
final String header= Messages.format(RefactoringCoreMessages.InlineMethodRefactoring_descriptor_description, new String[] { method.getElementName(), cu.getElementName() });
//final JDTRefactoringDescriptorComment comment= new JDTRefactoringDescriptorComment(project, this, header);
//comment.addSetting(Messages.format(RefactoringCoreMessages.InlineMethodRefactoring_original_pattern, BindingLabelProvider.getBindingLabel(binding, JavaElementLabels.ALL_FULLY_QUALIFIED)));
//if (fDeleteSource)
// comment.addSetting(RefactoringCoreMessages.InlineMethodRefactoring_remove_method);
//if (fCurrentMode == Mode.INLINE_ALL)
// comment.addSetting(RefactoringCoreMessages.InlineMethodRefactoring_replace_references);
final InlineMethodDescriptor descriptor = new InlineMethodDescriptor(project, description, ""/*comment.asString()*/, arguments, flags);
arguments.put(ScriptRefactoringDescriptor.ATTRIBUTE_INPUT, ScriptRefactoringDescriptor.elementToHandle(project, cu));
arguments.put(ScriptRefactoringDescriptor.ATTRIBUTE_SELECTION, new Integer(offset).toString() + " " + new Integer(length).toString()); //$NON-NLS-1$
arguments.put(ATTRIBUTE_DELETE, Boolean.valueOf(deleteSource).toString());
arguments.put(ATTRIBUTE_MODE, new Integer(currentMode == Mode.ALL ? 1 : 0).toString());
return new DynamicValidationRefactoringChange(descriptor, RefactoringCoreMessages.InlineMethodRefactoring_edit_inlineCall, changeManager.getAllChanges());
}
//private static SourceProvider resolveSourceProvider(RefactoringStatus status, Node nameNode) {
/*IMethodBinding methodBinding= Invocations.resolveBinding(invocation);
if (methodBinding == null) {
status.addFatalError(RefactoringCoreMessages.InlineMethodRefactoring_error_noMethodDeclaration);
return null;
}*/
/*MethodDeclaration declaration= (MethodDeclaration)root.findDeclaringNode(methodBinding);
if (declaration != null) {
return new SourceProvider(typeRoot, declaration);
}
IMethod method= (IMethod)methodBinding.getJavaElement();
if (method != null) {
CompilationUnit methodDeclarationAstRoot;
ICompilationUnit methodCu= method.getCompilationUnit();
if (methodCu != null) {
methodDeclarationAstRoot= new RefactoringASTParser(AST.JLS3).parse(methodCu, true);
} else {
IClassFile classFile= method.getClassFile();
if (! JavaElementUtil.isSourceAvailable(classFile)) {
String methodLabel= JavaElementLabels.getTextLabel(method, JavaElementLabels.M_FULLY_QUALIFIED | JavaElementLabels.M_PARAMETER_TYPES);
status.addFatalError(Messages.format(RefactoringCoreMessages.InlineMethodRefactoring_error_classFile, methodLabel));
return null;
}
methodDeclarationAstRoot= new RefactoringASTParser(AST.JLS3).parse(classFile, true);
}
ASTNode node= methodDeclarationAstRoot.findDeclaringNode(methodBinding.getMethodDeclaration().getKey());
if (node instanceof MethodDeclaration) {
return new SourceProvider(methodDeclarationAstRoot.getTypeRoot(), (MethodDeclaration) node);
}
}*/
//}
private IFile[] getFilesToBeModified(List<ISourceModule> units) {
List<IFile> result= new ArrayList<IFile>(units.size() + 1);
for (ISourceModule unit : units) {
IFile file= getFile(unit);
if (file != null)
result.add(file);
}
if (deleteSource) {
IFile file = getFile(method.getSourceModule());
if (file != null && !units.contains(file))
result.add(file);
}
return (IFile[])result.toArray(new IFile[result.size()]);
}
private IFile getFile(ISourceModule unit) {
unit= unit.getPrimary();
IResource resource= unit.getResource();
if (resource != null && resource.getType() == IResource.FILE)
return (IFile)resource;
return null;
}
/*private void checkOverridden(RefactoringStatus status, IProgressMonitor pm) throws JavaModelException {
pm.beginTask("", 9); //$NON-NLS-1$
pm.setTaskName(RefactoringCoreMessages.InlineMethodRefactoring_checking_overridden);
MethodDeclaration decl= fSourceProvider.getDeclaration();
IMethod method= (IMethod) decl.resolveBinding().getJavaElement();
if (method == null || Flags.isPrivate(method.getFlags())) {
pm.worked(8);
return;
}
IType type= method.getDeclaringType();
ITypeHierarchy hierarchy= type.newTypeHierarchy(new SubProgressMonitor(pm, 6));
checkSubTypes(status, method, hierarchy.getAllSubtypes(type), new SubProgressMonitor(pm, 1));
checkSuperClasses(status, method, hierarchy.getAllSuperclasses(type), new SubProgressMonitor(pm, 1));
checkSuperInterfaces(status, method, hierarchy.getAllSuperInterfaces(type), new SubProgressMonitor(pm, 1));
pm.setTaskName(""); //$NON-NLS-1$
}
private void checkSubTypes(RefactoringStatus result, IMethod method, IType[] types, IProgressMonitor pm) {
checkTypes(
result, method, types,
RefactoringCoreMessages.InlineMethodRefactoring_checking_overridden_error,
pm);
}
private void checkSuperClasses(RefactoringStatus result, IMethod method, IType[] types, IProgressMonitor pm) {
checkTypes(
result, method, types,
RefactoringCoreMessages.InlineMethodRefactoring_checking_overrides_error,
pm);
}
private void checkSuperInterfaces(RefactoringStatus result, IMethod method, IType[] types, IProgressMonitor pm) {
checkTypes(
result, method, types,
RefactoringCoreMessages.InlineMethodRefactoring_checking_implements_error,
pm);
}
private void checkTypes(RefactoringStatus result, IMethod method, IType[] types, String key, IProgressMonitor pm) {
pm.beginTask("", types.length); //$NON-NLS-1$
for (int i= 0; i < types.length; i++) {
pm.worked(1);
IMethod[] overridden= types[i].findMethods(method);
if (overridden != null && overridden.length > 0) {
result.addError(
Messages.format(key, JavaElementLabels.getElementLabel(types[i], JavaElementLabels.ALL_DEFAULT)),
JavaStatusContext.create(overridden[0]));
}
}
}*/
private CallExpression[] removeNestedCalls(RefactoringStatus status, ISourceModule unit, CallExpression[] invocations) {
List<Node> result= new ArrayList<Node>();
for(Node invocation : invocations) {
boolean ok = true;
for (Node parent : invocations) {
parent = (Node)parent.eContainer();
while (parent != null && parent != invocation)
parent=(Node)parent.eContainer();
if (parent != null)
ok = false;
}
if (ok)
result.add(invocation);
else
status.addError(RefactoringCoreMessages.InlineMethodRefactoring_nestedInvocation/*,
JavaStatusContext.create(unit, parent)*/);
}
return result.toArray(new CallExpression[result.size()]);
}
}