blob: e2ef05c0a2de06b70fb52af6280bfc1c383bed85 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.plugins.groovy.refactoring.extract;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariableDeclaration;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrVariableDeclarationOwner;
import org.jetbrains.plugins.groovy.lang.psi.dataFlow.reachingDefs.VariableInfo;
import org.jetbrains.plugins.groovy.lang.psi.impl.ApplicationStatementUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.TypesUtil;
import org.jetbrains.plugins.groovy.lang.psi.util.GrStringUtil;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.GroovyNamesUtil;
import org.jetbrains.plugins.groovy.refactoring.extract.method.ExtractMethodInfoHelper;
import org.jetbrains.plugins.groovy.refactoring.introduce.StringPartInfo;
import java.util.*;
/**
* @author ilyas
*/
public class ExtractUtil {
private static final Logger LOG = Logger.getInstance(ExtractUtil.class);
private ExtractUtil() {
}
public static GrStatement replaceStatement(@Nullable GrStatementOwner declarationOwner, @NotNull ExtractInfoHelper helper) {
GrStatement realStatement;
if (declarationOwner != null && !isSingleExpression(helper.getStatements()) && helper.getStringPartInfo() == null) {
// Replace set of statements
final GrStatement[] newStatement = createResultStatement(helper);
// add call statement
final GrStatement[] statements = helper.getStatements();
LOG.assertTrue(statements.length > 0);
realStatement = null;
for (GrStatement statement : newStatement) {
realStatement = declarationOwner.addStatementBefore(statement, statements[0]);
JavaCodeStyleManager.getInstance(realStatement.getProject()).shortenClassReferences(realStatement);
}
LOG.assertTrue(realStatement != null);
// remove old statements
removeOldStatements(declarationOwner, helper);
PsiImplUtil.removeNewLineAfter(realStatement);
}
else {
GrExpression oldExpr;
if (helper.getStringPartInfo() != null) {
oldExpr = helper.getStringPartInfo().replaceLiteralWithConcatenation("xyz");
}
else {
oldExpr = (GrExpression)helper.getStatements()[0];
}
// Expression call replace
GrExpression methodCall = createMethodCall(helper);
realStatement = oldExpr.replaceWithExpression(methodCall, true);
JavaCodeStyleManager.getInstance(realStatement.getProject()).shortenClassReferences(realStatement);
}
return realStatement;
}
@NotNull
private static GrStatement[] createResultStatement(ExtractInfoHelper helper) {
VariableInfo[] outputVars = helper.getOutputVariableInfos();
PsiType type = helper.getOutputType();
GrStatement[] statements = helper.getStatements();
GrMethodCallExpression callExpression = createMethodCall(helper);
if ((outputVars.length == 0 || PsiType.VOID.equals(type)) && !helper.hasReturnValue()) return new GrStatement[]{callExpression};
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(helper.getProject());
if (helper.hasReturnValue()) {
return new GrStatement[]{factory.createStatementFromText("return " + callExpression.getText())};
}
LOG.assertTrue(outputVars.length > 0);
final List<VariableInfo> mustAdd = mustAddVariableDeclaration(statements, outputVars);
if (mustAdd.isEmpty()) {
return new GrStatement[]{
createAssignment(outputVars, callExpression, helper.getProject())
};
}
else if (mustAdd.size() == outputVars.length && outputVars.length == 1) {
return new GrVariableDeclaration[]{
factory.createVariableDeclaration(ArrayUtil.EMPTY_STRING_ARRAY, callExpression, outputVars[0].getType(), outputVars[0].getName())
};
}
else if (varsAreEqual(mustAdd, outputVars)) {
return createTupleDeclaration(outputVars, callExpression, helper.getProject());
}
else {
final List<GrStatement> result = generateVarDeclarations(mustAdd, helper.getProject(), null);
result.add(createAssignment(outputVars, callExpression, helper.getProject()));
return result.toArray(new GrStatement[result.size()]);
}
}
private static boolean varsAreEqual(List<VariableInfo> toAdd, VariableInfo[] outputVars) {
if (toAdd.size() != outputVars.length) return false;
Set<String> names = ContainerUtil.newHashSet();
for (VariableInfo info : toAdd) {
names.add(info.getName());
}
for (VariableInfo var : outputVars) {
if (!names.contains(var.getName())) return false;
}
return true;
}
private static GrStatement[] createTupleDeclaration(final VariableInfo[] infos,
GrMethodCallExpression callExpression,
final Project project) {
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(project);
StringBuilder tuple = new StringBuilder();
tuple.append("def (");
for (VariableInfo info : infos) {
final PsiType type = info.getType();
if (type != null) {
final PsiType unboxed = TypesUtil.unboxPrimitiveTypeWrapper(type);
tuple.append(unboxed.getCanonicalText());
tuple.append(' ');
}
tuple.append(info.getName());
tuple.append(",");
}
StringUtil.trimEnd(tuple, ",");
tuple.append(")=");
tuple.append(callExpression.getText());
return new GrStatement[]{factory.createStatementFromText(tuple)};
}
private static List<GrStatement> generateVarDeclarations(List<VariableInfo> varInfos,
Project project,
@Nullable GrExpression initializer) {
List<GrStatement> result = new ArrayList<GrStatement>();
if (varInfos.isEmpty()) return result;
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(project);
boolean distinctDeclaration = haveDifferentTypes(varInfos);
if (distinctDeclaration) {
for (VariableInfo info : varInfos) {
result.add(factory.createVariableDeclaration(ArrayUtil.EMPTY_STRING_ARRAY, "", info.getType(), info.getName()));
}
}
else {
String[] names = new String[varInfos.size()];
for (int i = 0, mustAddLength = varInfos.size(); i < mustAddLength; i++) {
names[i] = varInfos.get(i).getName();
}
result.add(factory.createVariableDeclaration(ArrayUtil.EMPTY_STRING_ARRAY, initializer, varInfos.get(0).getType(), names));
}
return result;
}
private static boolean haveDifferentTypes(List<VariableInfo> varInfos) {
if (varInfos.size() < 2) return true;
Set<String> diffTypes = new HashSet<String>();
for (VariableInfo info : varInfos) {
final PsiType t = info.getType();
diffTypes.add(t == null ? null : TypesUtil.unboxPrimitiveTypeWrapper(t).getCanonicalText());
}
return diffTypes.size() > 1;
}
private static GrStatement createAssignment(VariableInfo[] infos, GrMethodCallExpression callExpression, final Project project) {
StringBuilder text = new StringBuilder();
if (infos.length > 1) text.append('(');
for (VariableInfo info : infos) {
text.append(info.getName()).append(", ");
}
if (infos.length > 1) {
text.replace(text.length() - 2, text.length(), ") =");
}
else {
text.replace(text.length() - 2, text.length(), " = ");
}
text.append(callExpression.getText());
return GroovyPsiElementFactory.getInstance(project).createExpressionFromText(text.toString());
}
private static void removeOldStatements(GrStatementOwner owner, ExtractInfoHelper helper) throws IncorrectOperationException {
owner.removeElements(helper.getInnerElements());
}
/*
To declare or not a variable to which method call result will be assigned.
*/
private static List<VariableInfo> mustAddVariableDeclaration(@NotNull GrStatement[] statements, @NotNull VariableInfo[] vars) {
Map<String, VariableInfo> names = new HashMap<String, VariableInfo>();
for (VariableInfo var : vars) {
names.put(var.getName(), var);
}
List<VariableInfo> result = new ArrayList<VariableInfo>();
for (GrStatement statement : statements) {
if (statement instanceof GrVariableDeclaration) {
GrVariableDeclaration declaration = (GrVariableDeclaration)statement;
for (GrVariable variable : declaration.getVariables()) {
final VariableInfo removed = names.remove(variable.getName());
if (removed != null) {
result.add(removed);
}
}
}
}
for (String varName : names.keySet()) {
if (ResolveUtil.resolveProperty(statements[0], varName) == null) {
result.add(names.get(varName));
}
}
return result;
}
public static TextRange getRangeOfRefactoring(ExtractInfoHelper helper) {
final StringPartInfo stringPartInfo = helper.getStringPartInfo();
if (stringPartInfo != null) {
return stringPartInfo.getRange();
}
else {
final GrStatement[] statements = helper.getStatements();
int start = statements[0].getTextRange().getStartOffset();
int end = statements[statements.length - 1].getTextRange().getEndOffset();
return new TextRange(start, end);
}
}
private static Collection<GrVariable> collectUsedLocalVarsOrParamsDeclaredOutside(ExtractInfoHelper helper) {
final Collection<GrVariable> result = new HashSet<GrVariable>();
final TextRange range = getRangeOfRefactoring(helper);
final int start = range.getStartOffset();
final int end = range.getEndOffset();
final GroovyRecursiveElementVisitor visitor = new GroovyRecursiveElementVisitor() {
@Override
public void visitReferenceExpression(GrReferenceExpression ref) {
final PsiElement resolved = ref.resolve();
if ((resolved instanceof GrParameter || PsiUtil.isLocalVariable(resolved)) && resolved.isPhysical()) {
final int offset = resolved.getTextRange().getStartOffset();
//var is declared outside of selected code
if (offset < start || end <= offset) {
result.add((GrVariable)resolved);
}
}
}
};
final GrStatement[] statements = helper.getStatements();
for (GrStatement statement : statements) {
statement.accept(visitor);
}
return result;
}
public static GrMethod createMethod(ExtractMethodInfoHelper helper) {
StringBuilder buffer = new StringBuilder();
//Add signature
PsiType type = helper.getOutputType();
final PsiPrimitiveType outUnboxed = PsiPrimitiveType.getUnboxedType(type);
if (outUnboxed != null) type = outUnboxed;
String modifier = getModifierString(helper);
String typeText = getTypeString(helper, false, modifier);
buffer.append(modifier);
buffer.append(typeText);
appendName(buffer, helper.getName());
buffer.append("(");
for (String param : getParameterString(helper, true)) {
buffer.append(param);
}
buffer.append(") { \n");
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(helper.getProject());
generateBody(helper, type == PsiType.VOID, buffer, helper.isForceReturn());
buffer.append("\n}");
String methodText = buffer.toString();
return factory.createMethodFromText(methodText, helper.getContext());
}
public static void appendName(@NotNull final StringBuilder buffer, @NotNull final String name) {
if (GroovyNamesUtil.isIdentifier(name)) {
buffer.append(name);
}
else {
buffer.append("'");
buffer.append(GrStringUtil.escapeSymbolsForString(name, true, false));
buffer.append("'");
}
}
public static void generateBody(ExtractInfoHelper helper, boolean isVoid, StringBuilder buffer, boolean forceReturn) {
VariableInfo[] outputInfos = helper.getOutputVariableInfos();
ParameterInfo[] infos = helper.getParameterInfos();
Set<String> declaredVars = new HashSet<String>();
for (ParameterInfo info : infos) {
declaredVars.add(info.getName());
}
for (VariableInfo info : mustAddVariableDeclaration(helper.getStatements(), outputInfos)) {
declaredVars.add(info.getName());
}
List<VariableInfo> genDecl = new ArrayList<VariableInfo>();
final Collection<GrVariable> outside = collectUsedLocalVarsOrParamsDeclaredOutside(helper);
for (final GrVariable variable : outside) {
if (!declaredVars.contains(variable.getName())) {
genDecl.add(new VariableInfo() {
@NotNull
@Override
public String getName() {
return variable.getName();
}
@Override
public PsiType getType() {
return variable.getDeclaredType();
}
});
}
}
final List<GrStatement> statements = generateVarDeclarations(genDecl, helper.getProject(), null);
for (GrStatement statement : statements) {
buffer.append(statement.getText()).append('\n');
}
final StringPartInfo stringPartInfo = helper.getStringPartInfo();
if (!isSingleExpression(helper.getStatements()) && stringPartInfo == null) {
for (PsiElement element : helper.getInnerElements()) {
buffer.append(element.getText());
}
//append return statement
if (!isVoid && outputInfos.length > 0) {
buffer.append('\n');
if (forceReturn) {
buffer.append("return ");
}
if (outputInfos.length > 1) buffer.append('[');
for (VariableInfo info : outputInfos) {
buffer.append(info.getName()).append(", ");
}
buffer.delete(buffer.length() - 2, buffer.length());
if (outputInfos.length > 1) buffer.append(']');
}
}
else {
GrExpression expr = stringPartInfo != null
? stringPartInfo.createLiteralFromSelected()
: (GrExpression)PsiUtil.skipParentheses(helper.getStatements()[0], false);
boolean addReturn = !isVoid && forceReturn && !PsiUtil.isVoidMethodCall(expr);
if (addReturn) {
buffer.append("return ");
final GrExpression methodCall = ApplicationStatementUtil.convertToMethodCallExpression(expr);
buffer.append(methodCall.getText());
}
else {
buffer.append(expr != null ? expr.getText() : "");
}
}
}
public static String[] getParameterString(ExtractInfoHelper helper, boolean useCanonicalText) {
int i = 0;
ParameterInfo[] infos = helper.getParameterInfos();
int number = 0;
for (ParameterInfo info : infos) {
if (info.passAsParameter()) number++;
}
ArrayList<String> params = new ArrayList<String>();
for (ParameterInfo info : infos) {
if (info.passAsParameter()) {
PsiType paramType = info.getType();
final PsiPrimitiveType unboxed = PsiPrimitiveType.getUnboxedType(paramType);
if (unboxed != null) paramType = unboxed;
String paramTypeText;
if (paramType == null || paramType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) || paramType.equals(PsiType.NULL)) {
paramTypeText = "";
}
else {
paramTypeText = (useCanonicalText ? paramType.getCanonicalText() : paramType.getPresentableText()) + " ";
}
params.add(paramTypeText + info.getName() + (i < number - 1 ? ", " : ""));
i++;
}
}
return ArrayUtil.toStringArray(params);
}
@NotNull
public static String getTypeString(@NotNull ExtractMethodInfoHelper helper, boolean forPresentation, @NotNull String modifier) {
if (!helper.specifyType()) {
return modifier.isEmpty() ? "def " : "";
}
PsiType type = helper.getOutputType();
final PsiPrimitiveType unboxed = PsiPrimitiveType.getUnboxedType(type);
if (unboxed != null) type = unboxed;
final String returnType = StringUtil.notNullize(forPresentation ? type.getPresentableText() : type.getCanonicalText());
if (StringUtil.isEmptyOrSpaces(returnType) || "null".equals(returnType)) {
return modifier.isEmpty() ? "def " : "";
}
else {
return returnType + " ";
}
}
public static boolean isSingleExpression(GrStatement[] statements) {
return statements.length == 1 && statements[0] instanceof GrExpression &&
!(statements[0].getParent() instanceof GrVariableDeclarationOwner && statements[0] instanceof GrAssignmentExpression);
}
private static GrMethodCallExpression createMethodCall(ExtractInfoHelper helper) {
StringBuilder buffer = new StringBuilder();
appendName(buffer, helper.getName());
buffer.append("(");
int number = 0;
for (ParameterInfo info : helper.getParameterInfos()) {
if (info.passAsParameter()) number++;
}
int i = 0;
String[] argumentNames = helper.getArgumentNames();
for (String argName : argumentNames) {
if (!argName.isEmpty()) {
buffer.append(argName);
if (i < number - 1) {
buffer.append(",");
}
i++;
}
}
buffer.append(")");
String callText = buffer.toString();
GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(helper.getProject());
GrExpression expr = factory.createExpressionFromText(callText);
LOG.assertTrue(expr instanceof GrMethodCallExpression, callText);
return ((GrMethodCallExpression)expr);
}
public static int getCaretOffset(@NotNull GrStatement statement) {
if (statement instanceof GrVariableDeclaration) {
GrVariable[] variables = ((GrVariableDeclaration)statement).getVariables();
if (variables.length > 0) {
GrExpression initializer = variables[0].getInitializerGroovy();
if (initializer != null) {
return initializer.getTextOffset();
}
}
}
else if (statement instanceof GrAssignmentExpression) {
GrExpression value = ((GrAssignmentExpression)statement).getRValue();
if (value != null) {
return value.getTextOffset();
}
}
return statement.getTextOffset();
}
public static String getModifierString(ExtractMethodInfoHelper helper) {
String visibility = helper.getVisibility();
LOG.assertTrue(visibility != null && !visibility.isEmpty());
final StringBuilder builder = new StringBuilder();
builder.append(visibility);
builder.append(" ");
if (helper.isStatic()) {
builder.append("static ");
}
return builder.toString();
}
}