IDEA-161198 Migration from Stream API back to for loops

This commit is contained in:
Tagir Valeev
2016-11-01 15:38:57 +07:00
parent 994b0f84a9
commit 9caa6d5ede
181 changed files with 5276 additions and 0 deletions
@@ -0,0 +1,322 @@
/*
* Copyright 2000-2016 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 com.intellij.codeInspection.streamToLoop;
import com.intellij.codeInspection.streamToLoop.StreamToLoopInspection.StreamToLoopReplacementContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.SuggestedNameInfo;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.refactoring.util.LambdaRefactoringUtil;
import com.siyeh.ig.psiutils.ExpressionUtils;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.OptionalLong;
import java.util.function.Consumer;
/**
* A helper class which represents an expression mapped to the functional interface (like lambda, method reference or normal reference)
*
* @author Tagir Valeev
*/
@SuppressWarnings("SameParameterValue")
abstract class FunctionHelper {
private static final Logger LOG = Logger.getInstance(FunctionHelper.class);
String getText() {
return getExpression().getText();
}
abstract PsiExpression getExpression();
/**
* Perform an adaptation of current function helper to the replacement context with given parameter names.
* Must be called exactly once prior using getExpression() or getText()
*
* @param context a context for which transformation should be performed
* @param newNames names of the SAM parameters (the length must be exactly the same as number of SAM parameters)
*/
abstract void transform(StreamToLoopReplacementContext context, String... newNames);
/**
* Rename the references of some variable if it's used inside this function
*
* @param oldName old variable name
* @param newName new variable name
* @param context a context
*/
abstract void rename(String oldName, String newName, StreamToLoopReplacementContext context);
abstract void registerUsedNames(Consumer<String> consumer);
@Nullable
abstract String getParameterName(int index);
void suggestVariableName(StreamVariable var, int index) {
String name = getParameterName(index);
if (name != null) {
var.addBestNameCandidate(name);
}
}
@Contract("null, _, _ -> null")
@Nullable
static FunctionHelper create(PsiExpression expression, int paramCount, boolean singleExpression) {
if (expression instanceof PsiLambdaExpression) {
PsiLambdaExpression lambda = (PsiLambdaExpression)expression;
PsiParameterList list = lambda.getParameterList();
if (list.getParametersCount() != paramCount) {
return null;
}
String[] parameters = StreamEx.of(list.getParameters()).map(PsiVariable::getName).toArray(String[]::new);
PsiExpression body = LambdaUtil.extractSingleExpressionFromBody(lambda.getBody());
if (body == null && singleExpression) return null;
return new LambdaFunctionHelper(body, parameters);
}
if (expression instanceof PsiMethodReferenceExpression) {
PsiType functionalInterfaceType = ((PsiMethodReferenceExpression)expression).getFunctionalInterfaceType();
PsiMethod interfaceMethod = LambdaUtil.getFunctionalInterfaceMethod(functionalInterfaceType);
if (interfaceMethod == null) return null;
if (interfaceMethod.getParameterList().getParametersCount() != paramCount) return null;
return new MethodReferenceFunctionHelper(functionalInterfaceType, (PsiMethodReferenceExpression)expression);
}
if (expression instanceof PsiReferenceExpression && ExpressionUtils.isSimpleExpression(expression)) {
PsiType functionalInterfaceType = expression.getType();
PsiMethod interfaceMethod = LambdaUtil.getFunctionalInterfaceMethod(functionalInterfaceType);
if (interfaceMethod == null || interfaceMethod.getParameterList().getParametersCount() != paramCount) return null;
return new SimpleReferenceFunctionHelper(expression, interfaceMethod.getName());
}
return null;
}
/**
* Renames references to the variable oldName in given expression into newName
* @param expression
* @param oldName old name
* @param newName new name
* @param context context
* @return resulting expression (might be the same as input expression) or null if expression already had references to newName,
* so rename may merge two variables
*/
@NotNull
static PsiExpression renameVarReference(@NotNull PsiExpression expression,
String oldName,
String newName,
StreamToLoopReplacementContext context) {
if(oldName.equals(newName)) return expression;
PsiLambdaExpression lambda = (PsiLambdaExpression)context.createExpression("("+oldName+","+newName+")-> "+expression.getText());
PsiParameter[] parameters = lambda.getParameterList().getParameters();
PsiParameter oldVar = parameters[0];
PsiParameter newVar = parameters[1];
PsiElement body = lambda.getBody();
LOG.assertTrue(body != null);
if(ReferencesSearch.search(newVar, new LocalSearchScope(body)).findFirst() != null) {
throw new IllegalStateException("Reference with name "+newVar+" already exists in "+lambda.getText());
}
for (PsiReference ref : ReferencesSearch.search(oldVar, new LocalSearchScope(body)).findAll()) {
ref.handleElementRename(newName);
}
return (PsiExpression)lambda.getBody();
}
static void processUsedNames(PsiElement start, Consumer<String> action) {
start.accept(new JavaRecursiveElementVisitor() {
@Override
public void visitVariable(PsiVariable variable) {
super.visitVariable(variable);
action.accept(variable.getName());
}
});
}
private static class MethodReferenceFunctionHelper extends FunctionHelper {
private final String myType;
private final String myQualifierType;
private PsiMethodReferenceExpression myMethodRef;
private PsiExpression myExpression;
public MethodReferenceFunctionHelper(PsiType functionalInterfaceType, PsiMethodReferenceExpression methodRef) {
myMethodRef = methodRef;
myType = functionalInterfaceType.getCanonicalText();
PsiExpression qualifier = methodRef.getQualifierExpression();
PsiType type = qualifier == null ? null : qualifier.getType();
myQualifierType = type == null ? null : type.getCanonicalText();
}
@Override
PsiExpression getExpression() {
LOG.assertTrue(myExpression != null);
return myExpression;
}
@Override
void registerUsedNames(Consumer<String> consumer) {
processUsedNames(myMethodRef, consumer);
}
@Override
void transform(StreamToLoopReplacementContext context, String... newNames) {
PsiMethodReferenceExpression methodRef = fromText(context, myMethodRef.getText());
PsiExpression qualifier = methodRef.getQualifierExpression();
if(qualifier != null && !ExpressionUtils.isSimpleExpression(qualifier)) {
String type = myQualifierType;
if(type != null) {
String nameCandidate = "expr";
PsiType psiType = context.createType(myQualifierType);
SuggestedNameInfo info =
JavaCodeStyleManager
.getInstance(context.getProject()).suggestVariableName(VariableKind.LOCAL_VARIABLE, null, null, psiType, true);
if(info.names.length > 0) {
nameCandidate = info.names[0];
}
String expr = context.declare(nameCandidate, type, qualifier.getText());
PsiLambdaExpression lambdaExpression = (PsiLambdaExpression)context
.createExpression("(" + type + " " + expr + ")->(" + myType + ")" + expr + "::" + myMethodRef.getReferenceName());
PsiTypeCastExpression castExpr = (PsiTypeCastExpression)lambdaExpression.getBody();
LOG.assertTrue(castExpr != null);
methodRef = (PsiMethodReferenceExpression)castExpr.getOperand();
}
}
PsiLambdaExpression lambda = LambdaRefactoringUtil.convertMethodReferenceToLambda(methodRef, true, true);
LOG.assertTrue(lambda != null);
PsiElement body = lambda.getBody();
LOG.assertTrue(body instanceof PsiExpression);
myExpression = (PsiExpression)body;
EntryStream.zip(lambda.getParameterList().getParameters(), newNames)
.forKeyValue((param, newName) -> myExpression = renameVarReference(myExpression, param.getName(), newName, context));
}
@NotNull
private PsiMethodReferenceExpression fromText(StreamToLoopReplacementContext context, String text) {
PsiTypeCastExpression castExpr = (PsiTypeCastExpression)context.createExpression("(" + myType + ")" + text);
PsiMethodReferenceExpression methodRef = (PsiMethodReferenceExpression)castExpr.getOperand();
LOG.assertTrue(methodRef != null);
return methodRef;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
if(oldName.equals(newName)) return;
PsiExpression qualifier = myMethodRef.getQualifierExpression();
if(qualifier == null) return;
qualifier = renameVarReference(qualifier, oldName, newName, context);
myMethodRef = fromText(context, qualifier.getText()+"::"+myMethodRef.getReferenceName());
}
@Override
@Nullable
String getParameterName(int index) {
return null;
}
}
private static class SimpleReferenceFunctionHelper extends FunctionHelper {
private PsiExpression myReference;
private final String myName;
private PsiExpression myExpression;
public SimpleReferenceFunctionHelper(PsiExpression reference, String methodName) {
myReference = reference;
myName = methodName;
}
@Override
PsiExpression getExpression() {
LOG.assertTrue(myExpression != null);
return myExpression;
}
@Override
void transform(StreamToLoopReplacementContext context, String... newNames) {
myExpression = context.createExpression(myReference.getText() + "." + myName + "(" + String.join(",", newNames) + ")");
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myReference = renameVarReference(myReference, oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> consumer) {
processUsedNames(myReference, consumer);
}
@Nullable
@Override
String getParameterName(int index) {
return null;
}
}
private static class LambdaFunctionHelper extends FunctionHelper {
private String[] myParameters;
private PsiExpression myBody;
LambdaFunctionHelper(PsiExpression body, String[] parameters) {
myParameters = parameters;
myBody = body;
}
PsiExpression getExpression() {
return myBody;
}
void transform(StreamToLoopReplacementContext context, String... newNames) {
LOG.assertTrue(newNames.length == myParameters.length);
EntryStream.zip(myParameters, newNames).forKeyValue(
(oldName, newName) -> myBody = renameVarReference(myBody, oldName, newName, context));
}
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
OptionalLong idx = StreamEx.of(myParameters).indexOf(newName);
if(idx.isPresent()) {
for(int i = 1;; i++) {
String paramName = newName+'$'+i;
if (!paramName.equals(oldName) &&
!StreamEx.of(myParameters).has(paramName)) {
try {
myBody = renameVarReference(myBody, newName, paramName, context);
myParameters[(int)idx.getAsLong()] = paramName;
break;
}
catch(IllegalStateException ise) {
// something is really wrong if we already have references to all newName$1, newName$2, ... newName$50
// or probably IllegalStateException was thrown by something else: at least we don't stuck in endless loop
if(i > 50) throw ise;
}
}
}
}
myBody = renameVarReference(myBody, oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> consumer) {
processUsedNames(myBody, consumer);
}
String getParameterName(int index) {
return myParameters[index];
}
}
}
@@ -0,0 +1,293 @@
/*
* Copyright 2000-2016 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 com.intellij.codeInspection.streamToLoop;
import com.intellij.codeInspection.streamToLoop.StreamToLoopInspection.StreamToLoopReplacementContext;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.util.PsiTypesUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Consumer;
/**
* @author Tagir Valeev
*/
abstract class Operation {
boolean changesVariable() {
return false;
}
StreamEx<StreamToLoopInspection.OperationRecord> nestedOperations() {
return StreamEx.empty();
}
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {}
abstract String wrap(StreamVariable inVar,
StreamVariable outVar,
String code,
StreamToLoopReplacementContext context);
void registerUsedNames(Consumer<String> usedNameConsumer) {
}
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {}
@Nullable
static Operation createIntermediate(String name, PsiExpression[] args, StreamVariable outVar) {
if(name.equals("distinct") && args.length == 0) {
return new DistinctOperation();
}
if(name.equals("skip") && args.length == 1) {
return new SkipOperation(args[0]);
}
if(name.equals("limit") && args.length == 1) {
return new LimitOperation(args[0]);
}
if(name.equals("filter") && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
return fn == null ? null : new FilterOperation(fn);
}
if(name.equals("peek") && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
return fn == null ? null : new PeekOperation(fn);
}
if((name.equals("boxed") || name.equals("asLongStream") || name.equals("asDoubleStream")) && args.length == 0) {
return new WideningOperation();
}
if ((name.equals("flatMap") || name.equals("flatMapToInt") || name.equals("flatMapToLong") || name.equals("flatMapToDouble")) &&
args.length == 1) {
return FlatMapOperation.from(outVar, args[0]);
}
if ((name.equals("map") ||
name.equals("mapToInt") ||
name.equals("mapToLong") ||
name.equals("mapToDouble") ||
name.equals("mapToObj")) && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
return fn == null ? null : new MapOperation(fn);
}
return null;
}
static abstract class LambdaIntermediateOperation extends Operation {
final FunctionHelper myFn;
LambdaIntermediateOperation(FunctionHelper fn) {
myFn = fn;
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myFn.registerUsedNames(usedNameConsumer);
}
@Override
final String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
myFn.transform(context, inVar.getName());
return wrap(outVar, code, context);
}
abstract String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context);
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myFn.rename(oldName, newName, context);
}
@Override
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {
myFn.suggestVariableName(inVar, 0);
}
}
static class FilterOperation extends LambdaIntermediateOperation {
public FilterOperation(FunctionHelper fn) {
super(fn);
}
@Override
String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
return "if(" + myFn.getText() + ") {\n" + code + "}\n";
}
}
static class PeekOperation extends LambdaIntermediateOperation {
public PeekOperation(FunctionHelper fn) {
super(fn);
}
@Override
String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
return myFn.getText() + ";\n" + code;
}
}
static class MapOperation extends LambdaIntermediateOperation {
public MapOperation(FunctionHelper fn) {
super(fn);
}
@Override
String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
return outVar.getDeclaration() + " = " + myFn.getText() + ";\n" + code;
}
@Override
boolean changesVariable() {
return true;
}
}
static class WideningOperation extends Operation {
@Override
String wrap(StreamVariable inVar,
StreamVariable outVar,
String code,
StreamToLoopReplacementContext context) {
return outVar.getDeclaration() + " = " + inVar + ";\n" + code;
}
@Override
boolean changesVariable() {
return true;
}
}
static class FlatMapOperation extends Operation {
private final @Nullable String myOriginalName;
private final List<StreamToLoopInspection.OperationRecord> myRecords;
private FlatMapOperation(@Nullable String name, List<StreamToLoopInspection.OperationRecord> records) {
myOriginalName = name;
myRecords = records;
}
@Override
StreamEx<StreamToLoopInspection.OperationRecord> nestedOperations() {
return StreamEx.of(myRecords).flatMap(or -> StreamEx.of(or).append(or.myOperation.nestedOperations()));
}
@Override
boolean changesVariable() {
return true;
}
@Override
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {
if(myOriginalName != null) {
inVar.addBestNameCandidate(myOriginalName);
}
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myRecords.forEach(or -> or.myOperation.registerUsedNames(usedNameConsumer));
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myRecords.forEach(or -> or.myOperation.rename(oldName, newName, context));
}
@Override
String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
if(myOriginalName != null && !myOriginalName.equals(inVar.getName())) {
rename(myOriginalName, inVar.getName(), context);
}
StreamToLoopReplacementContext
innerContext = new StreamToLoopReplacementContext(context, myRecords);
String replacement = code;
for(StreamToLoopInspection.OperationRecord or : StreamEx.ofReversed(myRecords)) {
replacement = or.myOperation.wrap(or.myInVar, or.myOutVar, replacement, innerContext);
}
return StreamEx.of(innerContext.getDeclarations()).map(str -> str + "\n").joining()+replacement;
}
@Nullable
public static FlatMapOperation from(StreamVariable outVar, PsiExpression arg) {
FunctionHelper fn = FunctionHelper.create(arg, 1, true);
if(fn == null) return null;
PsiExpression body = fn.getExpression();
if(!(body instanceof PsiMethodCallExpression)) return null;
PsiMethodCallExpression terminalCall = (PsiMethodCallExpression)body;
List<StreamToLoopInspection.OperationRecord> records = StreamToLoopInspection.extractOperations(outVar, terminalCall);
if(records == null || StreamToLoopInspection.getTerminal(records) != null) return null;
return new FlatMapOperation(fn.getParameterName(0), records);
}
}
static class DistinctOperation extends Operation {
@Override
String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
String set =
context.declare("uniqueValues", "java.util.Set<" + PsiTypesUtil.boxIfPossible(inVar.getType()) + ">", "new java.util.HashSet<>()");
return "if(" + set + ".add(" + inVar + ")) {\n" + code + "}\n";
}
}
static class SkipOperation extends Operation {
PsiExpression myExpression;
SkipOperation(PsiExpression expression) {
myExpression = expression;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myExpression = FunctionHelper.renameVarReference(myExpression, oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
FunctionHelper.processUsedNames(myExpression, usedNameConsumer);
}
@Override
String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
String toSkip = context.declare("toSkip", "long", myExpression.getText());
return "if(" + toSkip + ">0) {\n" + toSkip + "--;\ncontinue;\n}\n" + code;
}
}
static class LimitOperation extends Operation {
PsiExpression myExpression;
LimitOperation(PsiExpression expression) {
myExpression = expression;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myExpression = FunctionHelper.renameVarReference(myExpression, oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
FunctionHelper.processUsedNames(myExpression, usedNameConsumer);
}
@Override
String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
String limit = context.declare("limit", "long", myExpression.getText());
return "if(" + limit + "--==0) " + context.getBreakStatement() + code;
}
}
}
@@ -0,0 +1,270 @@
/*
* Copyright 2000-2016 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 com.intellij.codeInspection.streamToLoop;
import com.intellij.codeInspection.streamToLoop.StreamToLoopInspection.StreamToLoopReplacementContext;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.siyeh.ig.psiutils.ExpressionUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.function.Consumer;
import static com.intellij.codeInspection.streamToLoop.FunctionHelper.processUsedNames;
import static com.intellij.codeInspection.streamToLoop.FunctionHelper.renameVarReference;
/**
* @author Tagir Valeev
*/
abstract class SourceOperation extends Operation {
@Contract(value = " -> true", pure = true)
@Override
final boolean changesVariable() {
return true;
}
@Override
final String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
return wrap(outVar, code, context);
}
abstract String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context);
@Nullable
static SourceOperation createSource(PsiMethodCallExpression call) {
PsiExpression[] args = call.getArgumentList().getExpressions();
PsiType callType = call.getType();
if(callType == null) return null;
PsiMethod method = call.resolveMethod();
if(method == null) return null;
String name = method.getName();
PsiClass aClass = method.getContainingClass();
if(aClass == null) return null;
String className = aClass.getQualifiedName();
if(className == null) return null;
if ((name.equals("range") || name.equals("rangeClosed")) && args.length == 2 && method.getModifierList().hasExplicitModifier(
PsiModifier.STATIC) && (className.equals(CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM) ||
className.equals(CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM))) {
return new RangeSource(args[0], args[1], name.equals("rangeClosed"));
}
if (name.equals("of") && method.getModifierList().hasExplicitModifier(
PsiModifier.STATIC) && className.startsWith("java.util.stream.")) {
if(args.length == 1) {
PsiType type = args[0].getType();
if(type instanceof PsiArrayType) {
PsiType componentType = ((PsiArrayType)type).getComponentType();
if(StreamToLoopInspection.getStreamElementType(callType).isAssignableFrom(componentType)) {
return new ForEachSource(args[0]);
}
}
}
return new ExplicitSource(args);
}
if (name.equals("generate") && args.length == 1 && method.getModifierList().hasExplicitModifier(
PsiModifier.STATIC) && className.startsWith("java.util.stream.")) {
FunctionHelper fn = FunctionHelper.create(args[0], 0, true);
return fn == null ? null : new GenerateSource(fn);
}
if (name.equals("iterate") && args.length == 2 && method.getModifierList().hasExplicitModifier(
PsiModifier.STATIC) && className.startsWith("java.util.stream.")) {
FunctionHelper fn = FunctionHelper.create(args[1], 1, true);
return fn == null ? null : new IterateSource(args[0], fn);
}
if (name.equals("stream") && args.length == 0 &&
InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_COLLECTION)) {
return new ForEachSource(call.getMethodExpression().getQualifierExpression());
}
if (name.equals("stream") && args.length == 1 &&
CommonClassNames.JAVA_UTIL_ARRAYS.equals(className)) {
return new ForEachSource(args[0]);
}
return null;
}
static class ForEachSource extends SourceOperation {
private PsiExpression myQualifier;
ForEachSource(PsiExpression qualifier) {
myQualifier = qualifier;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myQualifier = renameVarReference(myQualifier, oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
processUsedNames(myQualifier, usedNameConsumer);
}
@Override
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {
if(myQualifier instanceof PsiReferenceExpression) {
String name = ((PsiReferenceExpression)myQualifier).getReferenceName();
if(name != null) {
name = StringUtil.unpluralize(name);
if(name != null) {
outVar.addOtherNameCandidate(name);
}
}
}
}
@Override
public String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
return context.getLoopLabel() +
"for(" + outVar.getDeclaration() + ": " + (myQualifier == null ? "this" : myQualifier.getText()) + ") {" + code + "}\n";
}
}
static class ExplicitSource extends SourceOperation {
private PsiExpression[] myArgList;
ExplicitSource(PsiExpression[] argList) {
myArgList = argList;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
Arrays.asList(myArgList).replaceAll(arg -> renameVarReference(arg, oldName, newName, context));
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
for(PsiExpression arg : myArgList) {
processUsedNames(arg, usedNameConsumer);
}
}
@Override
public String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
String type = outVar.getType();
String args = StreamEx.of(myArgList).map(PsiExpression::getText).joining(", ");
// TODO: remove type argument if redundant
String collection =
TypeConversionUtil.isPrimitive(type) ? "new " + type + "[] {" + args + "}" : "java.util.Arrays.<" + type + ">asList(" + args + ")";
return context.getLoopLabel() +
"for(" + outVar.getDeclaration() + ": " + collection + ") {" + code + "}\n";
}
}
static class GenerateSource extends SourceOperation {
private FunctionHelper myFn;
GenerateSource(FunctionHelper fn) {
myFn = fn;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myFn.rename(oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myFn.registerUsedNames(usedNameConsumer);
}
@Override
String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
myFn.transform(context);
return context.getLoopLabel() +
"while(true) {\n" +
outVar.getDeclaration() + "=" + myFn.getText() + ";\n" + code +
"}\n";
}
}
static class IterateSource extends SourceOperation {
private PsiExpression myInitializer;
private FunctionHelper myFn;
IterateSource(PsiExpression initializer, FunctionHelper fn) {
myInitializer = initializer;
myFn = fn;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myInitializer = renameVarReference(myInitializer, oldName, newName, context);
myFn.rename(oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
processUsedNames(myInitializer, usedNameConsumer);
myFn.registerUsedNames(usedNameConsumer);
}
@Override
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {
myFn.suggestVariableName(outVar, 0);
}
@Override
String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
myFn.transform(context, outVar.getName());
return context.getLoopLabel() +
"for(" + outVar.getDeclaration() + "=" + myInitializer.getText() + ";;" +
outVar + "=" + myFn.getText() + ") {\n" + code + "}\n";
}
}
static class RangeSource extends SourceOperation {
private PsiExpression myOrigin;
private PsiExpression myBound;
private boolean myInclusive;
RangeSource(PsiExpression origin, PsiExpression bound, boolean inclusive) {
myOrigin = origin;
myBound = bound;
myInclusive = inclusive;
}
@Override
void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
myOrigin = renameVarReference(myOrigin, oldName, newName, context);
myBound = renameVarReference(myBound, oldName, newName, context);
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
processUsedNames(myOrigin, usedNameConsumer);
processUsedNames(myBound, usedNameConsumer);
}
@Override
String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
String bound;
if(!ExpressionUtils.isSimpleExpression(myBound)) {
bound = context.declare("bound", outVar.getType(), myBound.getText());
} else {
bound = myBound.getText();
}
return context.getLoopLabel() +
"for(" + outVar.getDeclaration() + " = " + myOrigin.getText() + ";" +
outVar + (myInclusive ? "<=" : "<") + bound + ";" +
outVar + "++) {\n" +
code + "}\n";
}
}
}
@@ -0,0 +1,437 @@
/*
* Copyright 2000-2016 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 com.intellij.codeInspection.streamToLoop;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInspection.BaseJavaBatchLocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilCore;
import one.util.streamex.IntStreamEx;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.codeInspection.streamToLoop.Operation.FlatMapOperation;
/**
* @author Tagir Valeev
*/
public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool {
private static final Logger LOG = Logger.getInstance(StreamToLoopInspection.class);
// To quickly filter out most of the non-interesting method calls
private static final Set<String> SUPPORTED_TERMINALS = StreamEx.of("count", "sum", "summaryStatistics", "reduce", "collect",
"findFirst", "findAny", "anyMatch", "allMatch", "noneMatch",
"toArray", "average", "forEach", "forEachOrdered").toSet();
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
return PsiElementVisitor.EMPTY_VISITOR;
}
return new JavaElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
super.visitMethodCallExpression(call);
PsiReferenceExpression expression = call.getMethodExpression();
if (!SUPPORTED_TERMINALS.contains(expression.getReferenceName()) || !isSupportedCodeLocation(call)) return;
PsiMethod method = call.resolveMethod();
if(method == null) return;
PsiClass aClass = method.getContainingClass();
if(!InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) return;
PsiMethodCallExpression currentCall = call;
while(true) {
Operation op = createOperationFromCall(StreamVariable.STUB, currentCall);
if(op == null) return;
if(op instanceof SourceOperation) {
holder.registerProblem(call, "Replace stream API chain with loop", new ReplaceStreamWithLoopFix());
return;
}
PsiExpression qualifier = currentCall.getMethodExpression().getQualifierExpression();
if(!(qualifier instanceof PsiMethodCallExpression)) return;
currentCall = (PsiMethodCallExpression)qualifier;
}
}
};
}
private static boolean isSupportedCodeLocation(PsiMethodCallExpression call) {
PsiElement cur = call;
PsiElement parent = cur.getParent();
while(parent instanceof PsiExpression) {
// TODO: support in single expression lambdas
if(parent instanceof PsiLambdaExpression) {
return false;
}
if(parent instanceof PsiPolyadicExpression) {
PsiPolyadicExpression polyadicExpression = (PsiPolyadicExpression)parent;
IElementType type = polyadicExpression.getOperationTokenType();
if ((type.equals(JavaTokenType.ANDAND) || type.equals(JavaTokenType.OROR)) && polyadicExpression.getOperands()[0] != cur) {
// not the first in the &&/|| chain: we cannot properly generate code which would short-circuit as well
return false;
}
}
if(parent instanceof PsiConditionalExpression && ((PsiConditionalExpression)parent).getCondition() != cur) {
return false;
}
cur = parent;
parent = cur.getParent();
}
if(parent instanceof PsiExportsStatement || parent instanceof PsiReturnStatement || parent instanceof PsiExpressionStatement) return true;
if(parent instanceof PsiLocalVariable) {
PsiElement grandParent = parent.getParent();
if(grandParent instanceof PsiDeclarationStatement && ((PsiDeclarationStatement)grandParent).getDeclaredElements().length == 1) {
return true;
}
}
if(parent instanceof PsiForeachStatement && ((PsiForeachStatement)parent).getIteratedValue() == cur) return true;
if(parent instanceof PsiIfStatement && ((PsiIfStatement)parent).getCondition() == cur) return true;
return false;
}
@Nullable
static Operation createOperationFromCall(StreamVariable outVar, PsiMethodCallExpression call) {
PsiMethod method = call.resolveMethod();
if(method == null) return null;
PsiClass aClass = method.getContainingClass();
if(aClass == null) return null;
PsiExpression[] args = call.getArgumentList().getExpressions();
String name = method.getName();
String className = aClass.getQualifiedName();
if(className == null) return null;
PsiType callType = call.getType();
if(callType == null) return null;
if(InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) {
Operation op = Operation.createIntermediate(name, args, outVar);
if (op != null) return op;
op = TerminalOperation.createTerminal(name, args, callType, className, call.getParent() instanceof PsiExpressionStatement);
if(op != null) return op;
}
return SourceOperation.createSource(call);
}
@Nullable
static List<OperationRecord> extractOperations(StreamVariable outVar, PsiMethodCallExpression terminalCall) {
List<OperationRecord> operations = new ArrayList<>();
PsiMethodCallExpression currentCall = terminalCall;
StreamVariable lastVar = outVar;
while(true) {
Operation op = createOperationFromCall(lastVar, currentCall);
if(op == null) return null;
OperationRecord or = new OperationRecord();
or.myOperation = op;
or.myOutVar = lastVar;
operations.add(or);
if(op instanceof SourceOperation) {
or.myInVar = StreamVariable.STUB;
Collections.reverse(operations);
return operations;
}
PsiExpression qualifier = currentCall.getMethodExpression().getQualifierExpression();
if(!(qualifier instanceof PsiMethodCallExpression)) return null;
currentCall = (PsiMethodCallExpression)qualifier;
if(op.changesVariable()) {
PsiType type = getStreamElementType(currentCall.getType());
if(type == null) return null;
lastVar = new StreamVariable(type);
}
or.myInVar = lastVar;
}
}
@Contract("null -> null")
@Nullable
static TerminalOperation getTerminal(List<OperationRecord> operations) {
if (operations == null || operations.isEmpty()) return null;
OperationRecord record = operations.get(operations.size()-1);
if(record.myOperation instanceof TerminalOperation) {
return (TerminalOperation)record.myOperation;
}
return null;
}
static class ReplaceStreamWithLoopFix implements LocalQuickFix {
@Nls
@NotNull
@Override
public String getFamilyName() {
return "Replace Stream API chain with loop";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement element = descriptor.getStartElement();
if(!(element instanceof PsiMethodCallExpression)) return;
PsiMethodCallExpression terminalCall = (PsiMethodCallExpression)element;
if(!isSupportedCodeLocation(terminalCall)) return;
boolean inReturn = terminalCall.getParent() instanceof PsiReturnStatement;
List<OperationRecord> operations = extractOperations(StreamVariable.STUB, terminalCall);
TerminalOperation terminal = getTerminal(operations);
if (terminal == null) return;
annotateOperations(project, operations);
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
if(!FileModificationService.getInstance().preparePsiElementForWrite(element)) return;
terminalCall = ensureCodeBlock(terminalCall, factory);
if(terminalCall == null) return;
PsiStatement statement = PsiTreeUtil.getParentOfType(terminalCall, PsiStatement.class);
LOG.assertTrue(statement != null);
PsiElement temporaryStreamPlaceholder = terminalCall.replace(factory.createExpressionFromText("$streamReplacement$", terminalCall));
try {
StreamToLoopReplacementContext context = new StreamToLoopReplacementContext(statement, operations, inReturn);
registerVariables(operations, context);
String replacement = "";
for (OperationRecord or : StreamEx.ofReversed(operations)) {
replacement = or.myOperation.wrap(or.myInVar, or.myOutVar, replacement, context);
}
for (String declaration : context.getDeclarations()) {
addStatement(project, statement, factory.createStatementFromText(declaration, statement));
}
for (PsiStatement addedStatement : ((PsiBlockStatement)factory.createStatementFromText("{" + replacement + "}", statement))
.getCodeBlock().getStatements()) {
addStatement(project, statement, addedStatement);
}
String finisher = context.getFinisher();
if (finisher == null) {
temporaryStreamPlaceholder.delete();
}
else {
temporaryStreamPlaceholder.replace(factory.createExpressionFromText(finisher, temporaryStreamPlaceholder));
}
}
catch (Exception ex) {
if(temporaryStreamPlaceholder.isPhysical()) {
// Just in case if something went wrong: at least try to restore the original stream code
temporaryStreamPlaceholder.replace(factory.createExpressionFromText(terminalCall.getText(), temporaryStreamPlaceholder));
}
throw ex;
}
}
void addStatement(@NotNull Project project, PsiStatement statement, PsiStatement context) {
CodeStyleManager.getInstance(project)
.reformat(JavaCodeStyleManager.getInstance(project).shortenClassReferences(statement.getParent().addBefore(context, statement)));
}
@Nullable
private static PsiMethodCallExpression ensureCodeBlock(PsiMethodCallExpression expression, PsiElementFactory factory) {
PsiStatement statement = PsiTreeUtil.getParentOfType(expression, PsiStatement.class);
if(statement == null) return null;
if(!(statement.getParent() instanceof PsiCodeBlock)) {
PsiElement nameElement = expression.getMethodExpression().getReferenceNameElement();
if(nameElement == null) return null;
int delta = nameElement.getTextOffset() - statement.getTextOffset();
PsiElement blockStatement = statement.replace(factory.createStatementFromText("{" + statement.getText() + "}", statement));
PsiStatement newStatement = ((PsiBlockStatement)blockStatement).getCodeBlock().getStatements()[0];
int targetOffset = newStatement.getTextOffset() + delta;
PsiElement element = PsiUtilCore.getElementAtOffset(newStatement.getContainingFile(), targetOffset);
PsiMethodCallExpression newExpression = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class);
LOG.assertTrue(newExpression != null);
return newExpression;
}
return expression;
}
private static StreamEx<OperationRecord> allOperations(List<OperationRecord> operations) {
return StreamEx.of(operations)
.flatMap(or -> or.myOperation.nestedOperations().append(or));
}
private static void annotateOperations(@NotNull Project project, List<OperationRecord> operations) {
JavaCodeStyleManager javaCodeStyleManager = JavaCodeStyleManager.getInstance(project);
allOperations(operations)
.peek(or -> or.myOperation.suggestNames(or.myInVar, or.myOutVar))
.flatMap(or -> StreamEx.of(or.myInVar, or.myOutVar)).distinct()
.forEach(var -> var.addCandidatesFromType(javaCodeStyleManager));
}
private static void registerVariables(List<OperationRecord> operations, StreamToLoopReplacementContext context) {
allOperations(operations).map(or -> or.myOperation).forEach(op -> op.registerUsedNames(context::addUsedVar));
allOperations(operations).map(or -> or.myInVar).distinct().forEach(var -> var.register(context));
}
}
@Contract("null -> null")
static PsiType getStreamElementType(PsiType type) {
if(!(type instanceof PsiClassType)) return null;
PsiClass aClass = ((PsiClassType)type).resolve();
if(InheritanceUtil.isInheritor(aClass, false, CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM)) {
return PsiType.INT;
}
if(InheritanceUtil.isInheritor(aClass, false, CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM)) {
return PsiType.LONG;
}
if(InheritanceUtil.isInheritor(aClass, false, CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM)) {
return PsiType.DOUBLE;
}
PsiType[] parameters = ((PsiClassType)type).getParameters();
if(parameters.length != 1) return null;
PsiType streamType = parameters[0];
if(streamType instanceof PsiCapturedWildcardType) {
streamType = ((PsiCapturedWildcardType)streamType).getUpperBound();
}
return streamType;
}
static class StreamToLoopReplacementContext {
private final boolean myHasNestedLoops;
private final boolean myInReturn;
private final String mySuffix;
private final PsiStatement myStatement;
private final Set<String> myUsedNames;
private final Set<String> myUsedLabels;
private final List<String> myDeclarations = new ArrayList<>();
private String myLabel;
private String myFinisher;
StreamToLoopReplacementContext(PsiStatement statement, List<OperationRecord> records, boolean inReturn) {
myStatement = statement;
myHasNestedLoops = records.stream().anyMatch(or -> or.myOperation instanceof FlatMapOperation);
myInReturn = inReturn;
mySuffix = myHasNestedLoops ? "Outer" : "";
myUsedNames = new HashSet<>();
myUsedLabels = StreamEx.iterate(statement, Objects::nonNull, PsiElement::getParent).select(PsiLabeledStatement.class)
.map(PsiLabeledStatement::getName).toSet();
}
StreamToLoopReplacementContext(StreamToLoopReplacementContext parentContext, List<OperationRecord> records) {
myUsedNames = parentContext.myUsedNames;
myUsedLabels = parentContext.myUsedLabels;
myInReturn = false;
myStatement = parentContext.myStatement;
myHasNestedLoops = records.stream().anyMatch(or -> or.myOperation instanceof FlatMapOperation);
mySuffix = "Inner";
}
public void addUsedVar(String name) {
myUsedNames.add(name);
}
@Nullable
private String allocateLabel() {
if(!myHasNestedLoops) return null;
if(myLabel == null) {
String base = mySuffix.toUpperCase(Locale.ENGLISH);
myLabel = IntStreamEx.ints().mapToObj(i -> i == 0 ? base : base + i)
.remove(myUsedLabels::contains).findFirst().orElseThrow(IllegalArgumentException::new);
myUsedLabels.add(myLabel);
}
return myLabel;
}
public String getLoopLabel() {
return myLabel == null ? "" : myLabel + ":\n";
}
public String getBreakStatement() {
String label = allocateLabel();
return label == null ? "break;\n" : "break "+label+";\n";
}
public List<String> getDeclarations() {
return myDeclarations;
}
public String registerVarName(Collection<String> variants) {
// TODO: avoid introducing conflicts with nested scopes (variables declared in lambdas, nested lambdas)
if(variants.isEmpty()) {
return registerVarName(Collections.singleton("val"));
}
for(int idx = 0; ; idx++) {
for(String variant : variants) {
String name = idx == 0 ? variant : variant + idx;
if(!isUsed(name)) {
myUsedNames.add(name);
return name;
}
}
}
}
private boolean isUsed(String varName) {
if(myUsedNames.contains(varName)) return true;
// TODO: cleaner solution
return !varName.equals(JavaCodeStyleManager.getInstance(myStatement.getProject()).suggestUniqueVariableName(varName, myStatement, true));
}
public String declare(String desiredName, String type, String initializer) {
String name = registerVarName(
mySuffix.isEmpty() ? Collections.singleton(desiredName) : Arrays.asList(desiredName, desiredName + mySuffix));
myDeclarations.add(type + " " + name + " = " + initializer + ";");
return name;
}
public String assignAndBreak(String desiredName, String type, String foundValue, String notFoundValue) {
if(myInReturn) {
setFinisher(notFoundValue);
return "return "+foundValue+";";
}
String found = declareResult(desiredName, type, notFoundValue);
return found + " = " +foundValue+";\n" + getBreakStatement();
}
public String declareResult(String desiredName, String type, String initializer) {
String name = registerVarName(Arrays.asList(desiredName, "result"));
myDeclarations.add(type + " " + name + " = " + initializer + ";");
if(myFinisher != null) {
throw new IllegalStateException("Finisher is already defined");
}
setFinisher(name);
return name;
}
public String getFinisher() {
return myFinisher;
}
public void setFinisher(String finisher) {
myFinisher = finisher;
}
public Project getProject() {
return myStatement.getProject();
}
public PsiExpression createExpression(String text) {
return JavaPsiFacade.getElementFactory(myStatement.getProject()).createExpressionFromText(text, myStatement);
}
public PsiType createType(String text) {
return JavaPsiFacade.getElementFactory(myStatement.getProject()).createTypeFromText(text, myStatement);
}
}
static class OperationRecord {
Operation myOperation;
StreamVariable myInVar, myOutVar;
}
}
@@ -0,0 +1,141 @@
/*
* Copyright 2000-2016 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 com.intellij.codeInspection.streamToLoop;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.PsiType;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.VariableKind;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
/**
* This class represents a variable which holds stream element. Its lifecycle is the following:
* 1. Construction: fast, in case you don't need to perform a fix actually
* 2. Gather name candidates (addBestNameCandidate/addOtherNameCandidate can be called).
* 3. addCandidatesFromType called which adds name candidates based on type
* (until this stage no changes in original file should be performed)
* 4. Register variable in {@code StreamToLoopReplacementContext}: actual variable name is assigned here
* 5. Usage in code generation: getName()/getType() could be called.
*
* @author Tagir Valeev
*/
class StreamVariable {
private static final Logger LOG = Logger.getInstance(StreamVariable.class);
static StreamVariable STUB = new StreamVariable(PsiType.VOID) {
@Override
public void addBestNameCandidate(String candidate) {
}
@Override
public void addCandidatesFromType(JavaCodeStyleManager manager) {
}
@Override
void register(StreamToLoopInspection.StreamToLoopReplacementContext context) {
}
@Override
public String toString() {
return "###STUB###";
}
};
String myName;
String myType;
PsiType myPsiType;
private Collection<String> myBestCandidates = new LinkedHashSet<>();
private Collection<String> myOtherCandidates = new LinkedHashSet<>();
StreamVariable(@NotNull PsiType type) {
myPsiType = type;
}
/**
* Register best name candidate for this variable (like lambda argument which was explicitly present in the original code).
*
* @param candidate name candidate
*/
void addBestNameCandidate(String candidate) {
myBestCandidates.add(candidate);
}
/**
* Register normal name candidate for this variable (for example, derived using unpluralize from collection name, etc.)
*
* @param candidate name candidate
*/
void addOtherNameCandidate(String candidate) {
myOtherCandidates.add(candidate);
}
/**
* Register name candidates based on variable type.
* This must be called only once and only after addBestNameCandidate/addOtherNameCandidate.
*
* @param manager project-specific {@link JavaCodeStyleManager}
*/
void addCandidatesFromType(JavaCodeStyleManager manager) {
LOG.assertTrue(myPsiType != null);
LOG.assertTrue(myType == null);
myOtherCandidates.addAll(Arrays.asList(manager.suggestVariableName(VariableKind.LOCAL_VARIABLE, null, null, myPsiType, true).names));
myType = myPsiType.getCanonicalText();
myPsiType = null;
}
/**
* Register variable within {@link com.intellij.codeInspection.streamToLoop.StreamToLoopInspection.StreamToLoopReplacementContext}.
* Must be called once after all name candidates are registered. Now variable gets an actual name.
*
* @param context context to use
*/
void register(StreamToLoopInspection.StreamToLoopReplacementContext context) {
LOG.assertTrue(myName == null);
List<String> variants = StreamEx.of(myBestCandidates).append(myOtherCandidates).distinct().toList();
if (variants.isEmpty()) variants.add("val");
myName = context.registerVarName(variants);
myBestCandidates = myOtherCandidates = null;
}
String getName() {
LOG.assertTrue(myName != null);
return myName;
}
String getType() {
LOG.assertTrue(myType != null);
return myType;
}
String getDeclaration() {
return getType() + " " + getName();
}
@Override
public String toString() {
if (myName == null) {
return "###(unregistered: " + myBestCandidates + "|" + myOtherCandidates + ")###";
}
return myName;
}
}
@@ -0,0 +1,447 @@
/*
* Copyright 2000-2016 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 com.intellij.codeInspection.streamToLoop;
import com.intellij.codeInspection.streamToLoop.StreamToLoopInspection.StreamToLoopReplacementContext;
import com.intellij.psi.*;
import com.intellij.psi.util.TypeConversionUtil;
import com.siyeh.ig.psiutils.BoolUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Consumer;
/**
* @author Tagir Valeev
*/
abstract class TerminalOperation extends Operation {
@Override
final String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
return generate(inVar, context);
}
@Override
final void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
throw new IllegalStateException("Should not be called for terminal operation (tried to rename " + oldName + " -> " + newName + ")");
}
@Override
final boolean changesVariable() {
return true;
}
abstract String generate(StreamVariable inVar, StreamToLoopReplacementContext context);
@NotNull
@Contract(pure = true)
private static String getOptionalClass(String type) {
switch (type) {
case "int":
return "java.util.OptionalInt";
case "long":
return "java.util.OptionalLong";
case "double":
return "java.util.OptionalDouble";
default:
return "java.util.Optional";
}
}
@Nullable
static TerminalOperation createTerminal(String name, PsiExpression[] args, PsiType callType, String className, boolean isVoid) {
if(isVoid) {
if ((name.equals("forEach") || name.equals("forEachOrdered")) && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
return fn == null ? null : new ForEachTerminalOperation(fn);
}
return null;
}
if(name.equals("count") && args.length == 0) {
return new AccumulatedTerminalOperation("count", "long", "0", "{acc}++;");
}
if(name.equals("sum") && args.length == 0) {
return new AccumulatedTerminalOperation("sum", callType.getCanonicalText(), "0", "{acc}+={item};");
}
if(name.equals("average") && args.length == 0) {
if(className.equals(CommonClassNames.JAVA_UTIL_STREAM_DOUBLE_STREAM)) {
return new AverageTerminalOperation(true);
}
else if(className.equals(CommonClassNames.JAVA_UTIL_STREAM_INT_STREAM) ||
className.equals(CommonClassNames.JAVA_UTIL_STREAM_LONG_STREAM)) {
return new AverageTerminalOperation(false);
}
}
if(name.equals("summaryStatistics") && args.length == 0) {
return new AccumulatedTerminalOperation("stat", callType.getCanonicalText(), "new " + callType.getCanonicalText() + "()",
"{acc}.accept({item});");
}
if((name.equals("findFirst") || name.equals("findAny")) && args.length == 0) {
return new FindTerminalOperation(callType.getCanonicalText());
}
if((name.equals("anyMatch") || name.equals("allMatch") || name.equals("noneMatch")) && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
return fn == null ? null : new MatchTerminalOperation(fn, name);
}
if(name.equals("reduce")) {
if(args.length == 2 || args.length == 3) {
FunctionHelper fn = FunctionHelper.create(args[1], 2, true);
if(fn != null) {
return new ReduceTerminalOperation(args[0], fn, callType.getCanonicalText());
}
}
if(args.length == 1) {
PsiType optionalElementType = getOptionalElementType(callType);
FunctionHelper fn = FunctionHelper.create(args[0], 2, true);
if(fn != null && optionalElementType != null) {
return new ReduceToOptionalTerminalOperation(fn, optionalElementType.getCanonicalText());
}
}
}
if(name.equals("toArray") && args.length < 2) {
if(!(callType instanceof PsiArrayType)) return null;
PsiType componentType = ((PsiArrayType)callType).getComponentType();
if (componentType instanceof PsiPrimitiveType) {
if(args.length == 0) return new ToPrimitiveArrayTerminalOperation(componentType.getCanonicalText());
}
else {
String arr = "";
if(args.length == 1) {
if(!(args[0] instanceof PsiMethodReferenceExpression)) return null;
PsiMethodReferenceExpression arrCtor = (PsiMethodReferenceExpression)args[0];
if(!arrCtor.isConstructor()) return null;
PsiTypeElement typeElement = arrCtor.getQualifierType();
if(typeElement == null) return null;
PsiType type = typeElement.getType();
if(!(type instanceof PsiArrayType)) return null;
arr = "new "+type.getCanonicalText().replaceFirst("\\[]", "[0]");
}
return new AccumulatedTerminalOperation("list", CommonClassNames.JAVA_UTIL_LIST + "<" + componentType.getCanonicalText() + ">",
"new "+ CommonClassNames.JAVA_UTIL_ARRAY_LIST+"<>()", "{acc}.add({item});",
"{acc}.toArray("+arr+")");
}
}
if(name.equals("collect") && args.length == 1) {
if(args[0] instanceof PsiMethodCallExpression) {
PsiMethodCallExpression collectorCall = (PsiMethodCallExpression)args[0];
PsiExpression[] collectorArgs = collectorCall.getArgumentList().getExpressions();
PsiMethod collector = collectorCall.resolveMethod();
if(collector == null) return null;
PsiClass collectorClass = collector.getContainingClass();
if(collectorClass != null && CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS.equals(collectorClass.getQualifiedName())) {
if(collector.getName().equals("toList") && collectorArgs.length == 0) {
return AccumulatedTerminalOperation.toCollection(callType, CommonClassNames.JAVA_UTIL_ARRAY_LIST, "list");
}
if(collector.getName().equals("toSet") && collectorArgs.length == 0) {
return AccumulatedTerminalOperation.toCollection(callType, CommonClassNames.JAVA_UTIL_HASH_SET, "set");
}
if(collector.getName().equals("toCollection") && collectorArgs.length == 1) {
FunctionHelper fn = FunctionHelper.create(collectorArgs[0], 0, true);
if(fn != null) {
return new ToCollectionTerminalOperation(fn, callType);
}
}
if(collector.getName().equals("reducing") && collectorArgs.length == 2) {
FunctionHelper fn = FunctionHelper.create(collectorArgs[1], 2, true);
if(fn != null) {
return new ReduceTerminalOperation(collectorArgs[0], fn, callType.getCanonicalText());
}
}
if(collector.getName().equals("reducing") && collectorArgs.length == 1) {
PsiType optionalElementType = getOptionalElementType(callType);
FunctionHelper fn = FunctionHelper.create(collectorArgs[0], 2, true);
if(fn != null && optionalElementType != null) {
return new ReduceToOptionalTerminalOperation(fn, optionalElementType.getCanonicalText());
}
}
if(collector.getName().equals("joining")) {
if(collectorArgs.length == 0) {
return new AccumulatedTerminalOperation("sb", CommonClassNames.JAVA_LANG_STRING_BUILDER,
"new " + CommonClassNames.JAVA_LANG_STRING_BUILDER + "()", "{acc}.append({item});",
"{acc}.toString()");
}
if(collectorArgs.length == 1 || collectorArgs.length == 3) {
String initializer = "new java.util.StringJoiner(" + StreamEx.of(collectorArgs).map(PsiElement::getText).joining(",") + ")";
return new AccumulatedTerminalOperation("joiner", "java.util.StringJoiner", initializer,
"{acc}.add({item});", "{acc}.toString()");
}
}
}
}
}
return null;
}
@Contract("null -> null")
static PsiType getOptionalElementType(PsiType type) {
if(!(type instanceof PsiClassType)) return null;
PsiClass aClass = ((PsiClassType)type).resolve();
if(aClass == null) return null;
if("java.util.OptionalInt".equals(aClass.getQualifiedName())) {
return PsiType.INT;
}
if("java.util.OptionalLong".equals(aClass.getQualifiedName())) {
return PsiType.LONG;
}
if("java.util.OptionalDouble".equals(aClass.getQualifiedName())) {
return PsiType.DOUBLE;
}
if(!CommonClassNames.JAVA_UTIL_OPTIONAL.equals(aClass.getQualifiedName())) return null;
PsiType[] parameters = ((PsiClassType)type).getParameters();
if(parameters.length != 1) return null;
PsiType streamType = parameters[0];
if(streamType instanceof PsiCapturedWildcardType) {
streamType = ((PsiCapturedWildcardType)streamType).getUpperBound();
}
return streamType;
}
static class ReduceTerminalOperation extends TerminalOperation {
private PsiExpression myIdentity;
private String myType;
private FunctionHelper myUpdater;
public ReduceTerminalOperation(PsiExpression identity, FunctionHelper updater, String type) {
myIdentity = identity;
myType = type;
myUpdater = updater;
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
FunctionHelper.processUsedNames(myIdentity, usedNameConsumer);
myUpdater.registerUsedNames(usedNameConsumer);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
String accumulator = context.declareResult("acc", myType, myIdentity.getText());
myUpdater.transform(context, accumulator, inVar.getName());
return accumulator + "=" + myUpdater.getText() + ";";
}
}
static class ReduceToOptionalTerminalOperation extends TerminalOperation {
private String myType;
private FunctionHelper myUpdater;
public ReduceToOptionalTerminalOperation(FunctionHelper updater, String type) {
myType = type;
myUpdater = updater;
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myUpdater.registerUsedNames(usedNameConsumer);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
String seen = context.declare("seen", "boolean", "false");
String accumulator = context.declareResult("acc", myType, TypeConversionUtil.isPrimitive(myType) ? "0" : "null");
myUpdater.transform(context, accumulator, inVar.getName());
String optionalClass = getOptionalClass(myType);
context.setFinisher("(" + seen + "?" + optionalClass + ".of(" + accumulator + "):" + optionalClass + ".empty())");
return "if(!" + seen + ") {\n" +
seen + "=true;\n" +
accumulator + "=" + inVar + ";\n" +
"} else {\n" +
accumulator + "=" + myUpdater.getText() + ";\n" +
"}\n";
}
}
static class AverageTerminalOperation extends TerminalOperation {
private boolean myDoubleAccumulator;
public AverageTerminalOperation(boolean doubleAccumulator) {
myDoubleAccumulator = doubleAccumulator;
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
String sum = context.declareResult("sum", myDoubleAccumulator ? "double" : "long", "0");
String count = context.declare("count", "long", "0");
context.setFinisher("("+count+"==0?java.util.OptionalDouble.empty():"
+"java.util.OptionalDouble.of("+(myDoubleAccumulator?"":"(double)")+sum+"/"+count+"))");
return sum + "+=" + inVar + ";\n" + count + "++;\n";
}
}
static class ToPrimitiveArrayTerminalOperation extends TerminalOperation {
private String myType;
ToPrimitiveArrayTerminalOperation(String type) {
myType = type;
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
String arr = context.declareResult("arr", myType + "[]", "new " + myType + "[10]");
String count = context.declare("count", "int", "0");
context.setFinisher("java.util.Arrays.copyOfRange("+arr+",0,"+count+")");
return "if(" + arr + ".length==" + count + ") " + arr + "=java.util.Arrays.copyOf(" + arr + "," + count + "*2);\n" +
arr + "[" + count + "++]=" + inVar + ";\n";
}
}
static class FindTerminalOperation extends TerminalOperation {
private String myType;
public FindTerminalOperation(String type) {
myType = type;
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
int pos = myType.indexOf('<');
String optType = pos == -1 ? myType : myType.substring(0, pos);
return context.assignAndBreak("found", myType, optType + ".of(" + inVar + ")", optType + ".empty()");
}
}
static class MatchTerminalOperation extends TerminalOperation {
private final FunctionHelper myFn;
private final String myName;
private final boolean myDefaultValue, myNegatePredicate;
public MatchTerminalOperation(FunctionHelper fn, String name) {
myFn = fn;
switch(name) {
case "anyMatch":
myName = "found";
myDefaultValue = false;
myNegatePredicate = false;
break;
case "allMatch":
myName = "allMatch";
myDefaultValue = true;
myNegatePredicate = true;
break;
case "noneMatch":
myName = "noneMatch";
myDefaultValue = true;
myNegatePredicate = false;
break;
default:
throw new IllegalArgumentException(name);
}
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myFn.registerUsedNames(usedNameConsumer);
}
@Override
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {
myFn.suggestVariableName(inVar, 0);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
myFn.transform(context, inVar.getName());
String expression = myNegatePredicate ? BoolUtils.getNegatedExpressionText(myFn.getExpression()) : myFn.getText();
return "if(" + expression + ") {\n" +
context.assignAndBreak(myName, PsiType.BOOLEAN.getCanonicalText(), String.valueOf(!myDefaultValue), String.valueOf(myDefaultValue)) +
"}\n";
}
}
static class AccumulatedTerminalOperation extends TerminalOperation {
private String myAccName;
private String myAccType;
private String myAccInitializer;
private String myUpdateTemplate;
private String myFinisherTemplate;
/**
* @param accName desired name for accumulator variable
* @param accType type of accumulator variable
* @param accInitializer initializer for accumulator variable
* @param updateTemplate template to update accumulator. May contain {@code {acc}} - reference to accumulator variable
* and {@code {item}} - reference to stream element.
* @param finisherTemplate template to final result. May contain {@code {acc}} - reference to accumulator variable.
* By default it's {@code "{acc}"}
*/
AccumulatedTerminalOperation(String accName, String accType, String accInitializer, String updateTemplate, String finisherTemplate) {
myAccName = accName;
myAccType = accType;
myAccInitializer = accInitializer;
myUpdateTemplate = updateTemplate;
myFinisherTemplate = finisherTemplate;
}
AccumulatedTerminalOperation(String accName, String accType, String accInitializer, String updateTemplate) {
this(accName, accType, accInitializer, updateTemplate, "{acc}");
}
@Override
public String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
String varName = context.declareResult(myAccName, myAccType, myAccInitializer);
context.setFinisher(myFinisherTemplate.replace("{acc}", varName));
return myUpdateTemplate.replace("{item}", inVar.getName()).replace("{acc}", varName);
}
public static AccumulatedTerminalOperation toCollection(PsiType collectionType, String implementationType, String varName) {
return new AccumulatedTerminalOperation(varName, collectionType.getCanonicalText(), "new " + implementationType + "<>()",
"{acc}.add({item});");
}
}
static class ToCollectionTerminalOperation extends TerminalOperation {
private final String myType;
private final FunctionHelper myFn;
public ToCollectionTerminalOperation(FunctionHelper fn, PsiType callType) {
myFn = fn;
myType = callType.getCanonicalText();
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myFn.registerUsedNames(usedNameConsumer);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
// TODO: remove redundant type arguments
myFn.transform(context);
String collection = context.declareResult("collection", myType, myFn.getText());
return collection+".add("+inVar+");\n";
}
}
static class ForEachTerminalOperation extends TerminalOperation {
private FunctionHelper myFn;
public ForEachTerminalOperation(FunctionHelper fn) {
myFn = fn;
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
myFn.registerUsedNames(usedNameConsumer);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
myFn.transform(context, inVar.getName());
return myFn.getText()+";\n";
}
}
}