package com.redhat.ceylon.eclipse.code.refactor; import static com.redhat.ceylon.eclipse.java2ceylon.Java2CeylonProxies.utilJ2C; import static com.redhat.ceylon.eclipse.util.EditorUtil.getDocument; import static com.redhat.ceylon.eclipse.util.Nodes.getDefaultArgSpecifier; import static com.redhat.ceylon.eclipse.util.Nodes.getIdentifyingNode; import static com.redhat.ceylon.eclipse.util.Nodes.getReferencedExplicitDeclaration; import static com.redhat.ceylon.eclipse.util.Nodes.getReferencedNode; import static com.redhat.ceylon.eclipse.util.Nodes.text; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.antlr.runtime.CommonToken; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jface.text.IDocument; import org.eclipse.ltk.core.refactoring.CompositeChange; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextChange; 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.ui.IEditorPart; import com.redhat.ceylon.compiler.typechecker.tree.Node; import com.redhat.ceylon.compiler.typechecker.tree.Tree; import com.redhat.ceylon.compiler.typechecker.tree.Tree.MemberOrTypeExpression; import com.redhat.ceylon.compiler.typechecker.tree.Tree.PositionalArgument; import com.redhat.ceylon.compiler.typechecker.tree.Visitor; import com.redhat.ceylon.model.typechecker.model.Class; import com.redhat.ceylon.model.typechecker.model.Constructor; import com.redhat.ceylon.model.typechecker.model.Declaration; import com.redhat.ceylon.model.typechecker.model.FunctionOrValue; import com.redhat.ceylon.model.typechecker.model.Functional; import com.redhat.ceylon.model.typechecker.model.Parameter; import com.redhat.ceylon.model.typechecker.model.ParameterList; import com.redhat.ceylon.model.typechecker.model.Referenceable; import com.redhat.ceylon.model.typechecker.model.Unit; public class ChangeParametersRefactoring extends AbstractRefactoring { private static class FindInvocationsVisitor extends Visitor { private Declaration declaration; private final Set<Tree.PositionalArgumentList> posResults = new HashSet<Tree.PositionalArgumentList>(); private final Set<Tree.NamedArgumentList> namedResults = new HashSet<Tree.NamedArgumentList>(); Set<Tree.PositionalArgumentList> getPositionalArgLists() { return posResults; } Set<Tree.NamedArgumentList> getNamedArgLists() { return namedResults; } private FindInvocationsVisitor(Declaration declaration) { this.declaration = declaration; } @Override public void visit(Tree.InvocationExpression that) { super.visit(that); Tree.Primary primary = that.getPrimary(); if (primary instanceof Tree.MemberOrTypeExpression) { MemberOrTypeExpression mte = (Tree.MemberOrTypeExpression) primary; Declaration dec = mte.getDeclaration(); if (dec.refines(declaration)) { Tree.PositionalArgumentList pal = that.getPositionalArgumentList(); if (pal != null) { posResults.add(pal); } Tree.NamedArgumentList nal = that.getNamedArgumentList(); if (nal != null) { namedResults.add(nal); } } } } } private static class FindArgumentsVisitor extends Visitor { private Declaration declaration; private final Set<Tree.MethodArgument> results = new HashSet<Tree.MethodArgument>(); Set<Tree.MethodArgument> getResults() { return results; } private FindArgumentsVisitor(Declaration declaration) { this.declaration = declaration; } @Override public void visit(Tree.MethodArgument that) { super.visit(that); Parameter p = that.getParameter(); if (p != null && p.getModel().equals(declaration)) { results.add(that); } } } private List<Integer> order = new ArrayList<Integer>(); private List<Boolean> defaulted = new ArrayList<Boolean>(); private List<String> names = new ArrayList<String>(); private final Declaration declaration; private final List<Parameter> parameters; private Map<FunctionOrValue, String> arguments = new HashMap<FunctionOrValue, String>(); private final Map<FunctionOrValue, String> defaultArgs = new HashMap<FunctionOrValue, String>(); private final Map<FunctionOrValue, String> originalDefaultArgs = new HashMap<FunctionOrValue, String>(); private final Map<FunctionOrValue, String> paramLists = new HashMap<FunctionOrValue, String>(); public Map<FunctionOrValue, String> getDefaultArgs() { return defaultArgs; } public List<Parameter> getParameters() { return parameters; } public Node getNode() { return node; } public List<Integer> getOrder() { return order; } public List<Boolean> getDefaulted() { return defaulted; } public List<String> getNames() { return names; } public Map<FunctionOrValue, String> getArguments() { return arguments; } public ChangeParametersRefactoring(IEditorPart textEditor) { super(textEditor); if (rootNode != null) { Referenceable refDec = getReferencedExplicitDeclaration(node, rootNode); if (refDec instanceof Functional && refDec instanceof Declaration) { Declaration dec = (Declaration) refDec; refDec = dec.getRefinedDeclaration(); Functional fd = (Functional) refDec; List<ParameterList> pls = fd.getParameterLists(); if (pls.isEmpty()) { declaration = null; parameters = null; } else { if (dec instanceof Class) { Class c = (Class) dec; Constructor defaultConstructor = c.getDefaultConstructor(); if (defaultConstructor!=null) { dec = defaultConstructor; } } declaration = dec; List<Parameter> paramList = pls.get(0).getParameters(); parameters = new ArrayList<Parameter> (paramList); for (int i=0; i<parameters.size(); i++) { order.add(i); Parameter parameter = parameters.get(i); defaulted.add(parameter.isDefaulted()); names.add(parameter.getName()); } Node decNode = getReferencedNode(refDec); Tree.ParameterList pl = null; if (decNode instanceof Tree.AnyClass) { Tree.AnyClass ac = (Tree.AnyClass) decNode; pl = ac.getParameterList(); } if (decNode instanceof Tree.Constructor) { Tree.Constructor c = (Tree.Constructor) decNode; pl = c.getParameterList(); } else if (decNode instanceof Tree.AnyMethod) { Tree.AnyMethod am = (Tree.AnyMethod) decNode; pl = am.getParameterLists().get(0); } if (pl!=null) { for (Tree.Parameter p: pl.getParameters()) { Tree.SpecifierOrInitializerExpression sie = getDefaultArgSpecifier(p); FunctionOrValue pm = p.getParameterModel() .getModel(); if (sie != null) { Tree.Expression e = sie.getExpression(); defaultArgs.put(pm, text(e, tokens)); } if (p instanceof Tree.FunctionalParameterDeclaration) { Tree.FunctionalParameterDeclaration fp = (Tree.FunctionalParameterDeclaration) p; Tree.MethodDeclaration pd = (Tree.MethodDeclaration) fp.getTypedDeclaration(); Tree.ParameterList first = pd.getParameterLists() .get(0); paramLists.put(pm, text(first, tokens)); } } originalDefaultArgs.putAll(defaultArgs); } } } else { declaration = null; parameters = null; } } else { declaration = null; parameters = null; } } @Override public boolean getEnabled() { return declaration instanceof Functional && project != null && inSameProject(declaration); } public int getCount() { return declaration == null ? 0 : countDeclarationOccurrences(); } @Override int countReferences(Tree.CompilationUnit cu) { FindInvocationsVisitor frv = new FindInvocationsVisitor(declaration); FindRefinementsVisitor fdv = new FindRefinementsVisitor(declaration); FindArgumentsVisitor fav = new FindArgumentsVisitor(declaration); cu.visit(frv); cu.visit(fdv); cu.visit(fav); return frv.getPositionalArgLists().size() + fdv.getDeclarationNodes().size() + fav.getResults().size(); } public String getName() { return "Change Parameter List"; } public RefactoringStatus checkInitialConditions( IProgressMonitor pm) throws CoreException, OperationCanceledException { // Check parameters retrieved from editor context return new RefactoringStatus(); } public RefactoringStatus checkFinalConditions( IProgressMonitor pm) throws CoreException, OperationCanceledException { RefactoringStatus result = new RefactoringStatus(); boolean foundDefaulted = false; boolean foundVariadic = false; for (int index=0; index<defaulted.size(); index++) { if (foundVariadic) { result.addWarning("parameters occur after variadic parameter"); } Parameter p = parameters.get(order.get(index)); if (p.isSequenced()) { foundVariadic = true; } if (defaulted.get(index)) { foundDefaulted = true; } else { if (foundDefaulted && !p.isSequenced()) { result.addWarning("defaulted parameters occur before required parameters"); break; } } } for (int index=0; index<defaulted.size(); index++) { Parameter p = parameters.get(order.get(index)); if (defaulted.get(index)) { String arg = defaultArgs.get(p.getModel()); if (arg == null || arg.isEmpty()) { result.addWarning("missing default argument for " + p.getName()); } } } return result; } @Override protected void refactorInFile(TextChange tfc, CompositeChange cc, Tree.CompilationUnit root, List<CommonToken> tokens) { tfc.setEdit(new MultiTextEdit()); if (declaration != null) { refactorArgumentLists(tfc, root, tokens); refactorDeclarations(tfc, root, tokens); refactorReferences(tfc, root); } if (tfc.getEdit().hasChildren()) { cc.add(tfc); } } private void refactorReferences(TextChange tfc, Tree.CompilationUnit root) { for (int i=0; i<names.size(); i++) { String newName = names.get(i); final Parameter param = parameters.get(order.get(i)); FunctionOrValue model = param.getModel(); FindReferencesVisitor fprv = new FindReferencesVisitor(model) { @Override public void visit(Tree.InitializerParameter that) { //initializer parameters will be handled when //we refactor the parameter list Tree.SpecifierExpression se = that.getSpecifierExpression(); if (se!=null) { se.visit(this); } } @Override public void visit(Tree.ParameterDeclaration that) { //don't confuse a parameter declaration with //a split declaration below Tree.TypedDeclaration td = that.getTypedDeclaration(); if (td instanceof Tree.AttributeDeclaration) { Tree.AttributeDeclaration ad = (Tree.AttributeDeclaration) td; Tree.SpecifierOrInitializerExpression se = ad.getSpecifierOrInitializerExpression(); if (se!=null) { se.visit(this); } } if (td instanceof Tree.MethodDeclaration) { Tree.MethodDeclaration md = (Tree.MethodDeclaration) td; Tree.SpecifierExpression se = md.getSpecifierExpression(); if (se!=null) { se.visit(this); } } } @Override public void visit(Tree.TypedDeclaration that) { //handle split declarations super.visit(that); Tree.Identifier id = that.getIdentifier(); if (id!=null && isReference( that.getDeclarationModel(), id.getText())) { nodes.add(that); } } @Override protected boolean isReference(Parameter p) { return isSameParameter(param, p); } @Override protected boolean isReference( Declaration ref, String id) { if (ref.isParameter()) { FunctionOrValue fov = (FunctionOrValue) ref; return isSameParameter(param, fov.getInitializerParameter()); } else { return false; } } }; root.visit(fprv); for (Node ref: fprv.getNodes()) { Node idn = getIdentifyingNode(ref); if (idn instanceof Tree.Identifier) { Tree.Identifier id = (Tree.Identifier) idn; if (!id.getText().equals(newName)) { tfc.addEdit(new ReplaceEdit( id.getStartIndex(), id.getDistance(), newName)); } } } } } private void refactorDeclarations(TextChange tfc, Tree.CompilationUnit root, List<CommonToken> tokens) { FindRefinementsVisitor frv = new FindRefinementsVisitor(declaration); root.visit(frv); for (Tree.StatementOrArgument decNode: frv.getDeclarationNodes()) { boolean actual; Tree.ParameterList pl; if (decNode instanceof Tree.AnyMethod) { Tree.AnyMethod m = (Tree.AnyMethod) decNode; pl = m.getParameterLists().get(0); actual = m.getDeclarationModel().isActual(); } else if (decNode instanceof Tree.AnyClass) { Tree.AnyClass c = (Tree.AnyClass) decNode; pl = c.getParameterList(); actual = c.getDeclarationModel().isActual(); } else if (decNode instanceof Tree.Constructor) { Tree.Constructor c = (Tree.Constructor) decNode; pl = c.getParameterList(); actual = c.getDeclarationModel().isActual(); } else if (decNode instanceof Tree.SpecifierStatement) { Tree.SpecifierStatement ss = (Tree.SpecifierStatement) decNode; Tree.Term bme = ss.getBaseMemberExpression(); if (bme instanceof Tree.ParameterizedExpression) { Tree.ParameterizedExpression pe = (Tree.ParameterizedExpression) bme; pl = pe.getParameterLists().get(0); actual = true; } else { continue; } } else { continue; } tfc.addEdit(reorderParamsEdit(pl, reorderedParameters(pl.getParameters()), actual, tokens)); } } private static boolean isSameParameter (Parameter x, Parameter y) { if (x==null || y==null) return false; Declaration xd = x.getDeclaration(); Declaration yd = y.getDeclaration(); Functional fx = (Functional) xd; Functional fy = (Functional) yd; List<ParameterList> xpl = fx.getParameterLists(); List<ParameterList> ypl = fy.getParameterLists(); return !xpl.isEmpty() && !ypl.isEmpty() && xd.getRefinedDeclaration() .equals(yd.getRefinedDeclaration()) && xpl.get(0).getParameters().indexOf(x) == ypl.get(0).getParameters().indexOf(y); } private void refactorArgumentLists(TextChange tfc, Tree.CompilationUnit root, List<CommonToken> tokens) { FindInvocationsVisitor fiv = new FindInvocationsVisitor(declaration); root.visit(fiv); int requiredParams = countRequiredParameters(); for (Tree.PositionalArgumentList pal: fiv.getPositionalArgLists()) { tfc.addEdit(reorderArgsEdit(pal, reorderedArguments(requiredParams, pal.getPositionalArguments()), tokens)); } for (Tree.NamedArgumentList nal: fiv.getNamedArgLists()) { List<Tree.NamedArgument> nas = nal.getNamedArguments(); Tree.NamedArgument last = null; for (Tree.NamedArgument na: nas) { Parameter nap = na.getParameter(); if (nap != null) { boolean found = false; for (int i=0; i<defaulted.size(); i++) { Parameter p = parameters.get(order.get(i)); if (isSameParameter(p,nap)) { found = true; break; } } if (!found) { int start = last == null ? nal.getStartIndex() + 1 : last.getEndIndex(); tfc.addEdit(new DeleteEdit(start, na.getEndIndex() - start)); } } last = na; } for (int i=0; i<defaulted.size(); i++) { int index = order.get(i); Parameter p = parameters.get(index); if (!defaulted.get(i) || defaultHasChanged(p)) { boolean found = false; for (Tree.NamedArgument na : nas) { Parameter nap = na.getParameter(); if (isSameParameter(p,nap)) { found = true; break; } } if (!found) { String arg = arguments.get(p.getModel()); String argString = getInlinedNamedArg(p, arg); int startOffset = nal.getStartIndex(); int stopOffset = nal.getStopIndex(); try { IDocument doc = getDocument(tfc); if (doc.getLineOfOffset(stopOffset) > doc.getLineOfOffset(startOffset)) { argString = utilJ2C().indents().getDefaultIndent() + argString + ';' + utilJ2C().indents().getDefaultLineDelimiter(doc) + utilJ2C().indents().getIndent(nal, doc); } else if (startOffset==stopOffset-1) { argString = ' ' + argString + ';' + ' '; } else { argString = argString + ';' + ' '; } } catch (Exception e) { e.printStackTrace(); } tfc.addEdit(new InsertEdit(stopOffset, argString)); } } } } FindArgumentsVisitor fav = new FindArgumentsVisitor(declaration); root.visit(fav); for (Tree.MethodArgument decNode: fav.getResults()) { Tree.ParameterList pl = decNode.getParameterLists() .get(0); tfc.addEdit(reorderParamsEdit(pl, reorderedParameters(pl.getParameters()), tokens)); } } private int countRequiredParameters() { int requiredParams = -1; for (int i=0; i<defaulted.size(); i++) { int index = order.get(i); Parameter p = parameters.get(index); if (!defaulted.get(i) || defaultHasChanged(p)) { if (i > requiredParams) { requiredParams = i; } } } return requiredParams; } private int countExistingArgs(List<Tree.PositionalArgument> pas) { int existingArgs = 0; for (int i=0; i<pas.size(); i++) { Parameter p = pas.get(i).getParameter(); if (p != null) { int newLoc = order.indexOf(i); if (newLoc > existingArgs) { existingArgs = newLoc; } } } return existingArgs; } private Tree.PositionalArgument[] reorderedArguments( int requiredParams, List<Tree.PositionalArgument> pas) { int existingArgs = countExistingArgs(pas); int len = Math.max(requiredParams + 1, existingArgs + 1); Tree.PositionalArgument[] args = new Tree.PositionalArgument[len]; for (int i=0; i<pas.size(); i++) { PositionalArgument argument = pas.get(i); int index = order.indexOf(i); if (index >= 0) { args[index] = argument; } } return args; } Tree.Parameter[] reorderedParameters(List<Tree.Parameter> ps) { Tree.Parameter[] params = new Tree.Parameter[defaulted.size()]; for (int i=0; i<ps.size(); i++) { int index = order.indexOf(i); if (index >= 0) { params[index] = ps.get(i); } } return params; } boolean defaultHasChanged(Parameter p) { String original = originalDefaultArgs.get(p.getModel()); String current = defaultArgs.get(p.getModel()); return p.isDefaulted() && original != null && // the default arg has been modified (current == null || !current.equals(original)); } public ReplaceEdit reorderArgsEdit(Node list, Tree.PositionalArgument[] arguments, List<CommonToken> tokens) { StringBuilder sb = new StringBuilder("("); for (int i=0; i<arguments.length; i++) { Tree.PositionalArgument elem = arguments[i]; String argString; if (elem == null) { int index = order.get(i); Parameter p = parameters.get(index); String arg = this.arguments.get(p.getModel()); argString = getInlinedArg(p, arg); } else { argString = text(elem, tokens); } sb.append(argString).append(", "); } if (sb.toString().endsWith(", ")) { sb.setLength(sb.length() - 2); } sb.append(")"); return new ReplaceEdit(list.getStartIndex(), list.getDistance(), sb.toString()); } public ReplaceEdit reorderParamsEdit(Node list, Tree.Parameter[] parameters, List<CommonToken> tokens) { StringBuilder sb = new StringBuilder("("); for (int i=0; i<parameters.length; i++) { String paramString = paramString(parameters[i], names.get(i), tokens); sb.append(paramString) .append(", "); } if (sb.toString().endsWith(", ")) { sb.setLength(sb.length() - 2); } sb.append(")"); return new ReplaceEdit(list.getStartIndex(), list.getDistance(), sb.toString()); } public ReplaceEdit reorderParamsEdit(Node list, Tree.Parameter[] parameters, boolean actual, List<CommonToken> tokens) { StringBuilder sb = new StringBuilder("("); for (int i=0; i<parameters.length; i++) { Tree.Parameter parameter = parameters[i]; String paramString; String newName = names.get(i); if (parameter == null) { int index = order.get(i); Parameter addedParameter = this.parameters.get(index); Unit unit = list.getUnit(); paramString = addedParameter.getType() .asString(unit) + ' ' + newName; if (defaulted.get(i) && !actual) { FunctionOrValue model = addedParameter.getModel(); paramString += " = " + defaultArgs.get(model); } } else { paramString = paramStringWithoutDefaultArg( parameter, newName, tokens); if (defaulted.get(i) && !actual) { // now add the new default arg // TODO: this results in incorrectly-typed // code for void functional parameters Parameter p = parameter.getParameterModel(); paramString = paramString + getSpecifier(parameter) + getNewDefaultArg(p); } } sb.append(paramString).append(", "); } if (sb.toString().endsWith(", ")) { sb.setLength(sb.length() - 2); } sb.append(")"); return new ReplaceEdit(list.getStartIndex(), list.getDistance(), sb.toString()); } private String paramString(Tree.Parameter parameter, String newName, List<CommonToken> tokens) { String paramString = text(parameter, tokens); int loc = parameter.getStartIndex(); Tree.Identifier id = getIdentifier(parameter); int start = id.getStartIndex() - loc; int end = id.getEndIndex() - loc; return paramString.substring(0, start) + newName + paramString.substring(end); } private String paramStringWithoutDefaultArg( Tree.Parameter parameter, String newName, List<CommonToken> tokens) { String paramString = text(parameter, tokens); // first remove the default arg Node sie = getDefaultArgSpecifier(parameter); int loc = parameter.getStartIndex(); if (sie!=null) { int start = sie.getStartIndex() - loc; paramString = paramString.substring(0,start).trim(); } Tree.Identifier id = getIdentifier(parameter); int start = id.getStartIndex() - loc; int end = id.getEndIndex() - loc; return paramString.substring(0, start) + newName + paramString.substring(end); } private Tree.Identifier getIdentifier(Tree.Parameter parameter) { if (parameter instanceof Tree.InitializerParameter) { Tree.InitializerParameter ip = (Tree.InitializerParameter) parameter; return ip.getIdentifier(); } else if (parameter instanceof Tree.ParameterDeclaration) { Tree.ParameterDeclaration pd = (Tree.ParameterDeclaration) parameter; return pd.getTypedDeclaration().getIdentifier(); } else { throw new RuntimeException(); } } private static String getSpecifier(Tree.Parameter parameter) { if (parameter instanceof Tree.FunctionalParameterDeclaration) { return " => "; } else { return " = "; } } private String getInlinedArg(Parameter p, String argString) { if (argString == null || argString.isEmpty()) { argString = originalDefaultArgs.get(p.getModel()); if (argString == null || argString.isEmpty()) { argString = "nothing"; } } String params = paramLists.get(p.getModel()); if (params != null) { argString = params + " => " + argString; } return argString; } private String getInlinedNamedArg(Parameter p, String argString) { if (argString == null || argString.isEmpty()) { argString = originalDefaultArgs.get(p.getModel()); if (argString == null || argString.isEmpty()) { argString = "nothing"; } } String paramList = paramLists.get(p.getModel()); if (paramList == null) { int index = order.indexOf(parameters.indexOf(p)); return names.get(index) + " = " + argString; } else { return "function " + p.getName() + paramList + " => " + argString; } } private String getNewDefaultArg(Parameter p) { String argString = defaultArgs.get(p.getModel()); if (argString == null || argString.isEmpty()) { argString = "nothing"; } return argString; } public Declaration getDeclaration() { return declaration; } @Override protected boolean isAffectingOtherFiles() { if (declaration==null) { return false; } if (declaration.isToplevel() || declaration.isShared()) { return true; } return false; } }