mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-18 09:34:34 +07:00
IDEA-161198 Migration from Stream API back to for loops
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+437
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user