StreamToLoopInspection: added ExplicitCollectTerminalOperation; StreamVariable simplified; guard checks added (IDEA-CR-15249)

This commit is contained in:
Tagir Valeev
2016-11-07 12:13:39 +07:00
parent 2ac2ff5377
commit acbb7bddda
12 changed files with 183 additions and 57 deletions

View File

@@ -39,7 +39,6 @@ import java.util.function.Consumer;
*
* @author Tagir Valeev
*/
@SuppressWarnings("SameParameterValue")
abstract class FunctionHelper {
private static final Logger LOG = Logger.getInstance(FunctionHelper.class);
@@ -88,18 +87,18 @@ abstract class FunctionHelper {
}
}
@Contract("null, _, _ -> null")
@Contract("null, _ -> null")
@Nullable
static FunctionHelper create(PsiExpression expression, int paramCount, boolean singleExpression) {
static FunctionHelper create(PsiExpression expression, int paramCount) {
if (expression instanceof PsiLambdaExpression) {
PsiLambdaExpression lambda = (PsiLambdaExpression)expression;
PsiType functionalInterfaceType = lambda.getFunctionalInterfaceType();
if(functionalInterfaceType == null) return null;
PsiParameterList list = lambda.getParameterList();
if (list.getParametersCount() != paramCount) {
return null;
}
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;
if (body == null) return null;
return new LambdaFunctionHelper(body, parameters);
}
if (expression instanceof PsiMethodReferenceExpression) {
@@ -177,6 +176,8 @@ abstract class FunctionHelper {
@Override
String tryLightTransform(PsiType type) {
if(myMethodRef.isConstructor()) return null;
type = GenericsUtil.getVariableTypeByExpressionType(type);
if(type == null) return null;
PsiElement element = myMethodRef.resolve();
if(!(element instanceof PsiMethod)) return null;
PsiMethod method = (PsiMethod)element;

View File

@@ -65,11 +65,11 @@ abstract class Operation {
return new LimitOperation(args[0]);
}
if(name.equals("filter") && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
FunctionHelper fn = FunctionHelper.create(args[0], 1);
return fn == null ? null : new FilterOperation(fn);
}
if(name.equals("peek") && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
FunctionHelper fn = FunctionHelper.create(args[0], 1);
return fn == null ? null : new PeekOperation(fn);
}
if((name.equals("boxed") || name.equals("asLongStream") || name.equals("asDoubleStream")) && args.length == 0) {
@@ -84,7 +84,7 @@ abstract class Operation {
name.equals("mapToLong") ||
name.equals("mapToDouble") ||
name.equals("mapToObj")) && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
FunctionHelper fn = FunctionHelper.create(args[0], 1);
return fn == null ? null : new MapOperation(fn);
}
return null;
@@ -225,7 +225,7 @@ abstract class Operation {
@Nullable
public static FlatMapOperation from(StreamVariable outVar, PsiExpression arg, PsiType inType) {
FunctionHelper fn = FunctionHelper.create(arg, 1, true);
FunctionHelper fn = FunctionHelper.create(arg, 1);
if(fn == null) return null;
String varName = fn.tryLightTransform(inType);
if(varName == null) return null;

View File

@@ -81,12 +81,12 @@ abstract class SourceOperation extends Operation {
}
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);
FunctionHelper fn = FunctionHelper.create(args[0], 0);
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);
FunctionHelper fn = FunctionHelper.create(args[1], 1);
return fn == null ? null : new IterateSource(args[0], fn);
}
if (name.equals("stream") && args.length == 0 &&

View File

@@ -210,7 +210,7 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool {
List<OperationRecord> operations = extractOperations(StreamVariable.STUB, terminalCall);
TerminalOperation terminal = getTerminal(operations);
if (terminal == null) return;
annotateOperations(project, operations);
allOperations(operations).forEach(or -> or.myOperation.suggestNames(or.myInVar, or.myOutVar));
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
if(!FileModificationService.getInstance().preparePsiElementForWrite(element)) return;
terminalCall = ensureCodeBlock(terminalCall, factory);
@@ -279,14 +279,6 @@ public class StreamToLoopInspection extends BaseJavaBatchLocalInspectionTool {
.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));

View File

@@ -23,7 +23,6 @@ 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;
@@ -32,10 +31,8 @@ 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.
* 3. Register variable in {@code StreamToLoopReplacementContext}: actual variable name is assigned here
* 4. Usage in code generation: getName()/getType() could be called.
*
* @author Tagir Valeev
*/
@@ -47,10 +44,6 @@ class StreamVariable {
public void addBestNameCandidate(String candidate) {
}
@Override
public void addCandidatesFromType(JavaCodeStyleManager manager) {
}
@Override
void register(StreamToLoopReplacementContext context) {
}
@@ -62,14 +55,13 @@ class StreamVariable {
};
String myName;
String myType;
PsiType myPsiType;
@NotNull String myType;
private Collection<String> myBestCandidates = new LinkedHashSet<>();
private Collection<String> myOtherCandidates = new LinkedHashSet<>();
StreamVariable(@NotNull PsiType type) {
myPsiType = type;
myType = type.getCanonicalText();
}
/**
@@ -90,20 +82,6 @@ class StreamVariable {
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 StreamToLoopReplacementContext}.
* Must be called once after all name candidates are registered. Now variable gets an actual name.
@@ -112,7 +90,9 @@ class StreamVariable {
*/
void register(StreamToLoopReplacementContext context) {
LOG.assertTrue(myName == null);
List<String> variants = StreamEx.of(myBestCandidates).append(myOtherCandidates).distinct().toList();
String[] fromType = JavaCodeStyleManager.getInstance(context.getProject())
.suggestVariableName(VariableKind.LOCAL_VARIABLE, null, null, context.createType(myType), true).names;
List<String> variants = StreamEx.of(myBestCandidates).append(myOtherCandidates).append(fromType).distinct().toList();
if (variants.isEmpty()) variants.add("val");
myName = context.registerVarName(variants);
myBestCandidates = myOtherCandidates = null;
@@ -123,8 +103,8 @@ class StreamVariable {
return myName;
}
@NotNull
String getType() {
LOG.assertTrue(myType != null);
return myType;
}

View File

@@ -67,7 +67,7 @@ abstract class TerminalOperation extends Operation {
@NotNull PsiType elementType, @NotNull PsiType resultType, boolean isVoid) {
if(isVoid) {
if ((name.equals("forEach") || name.equals("forEachOrdered")) && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
FunctionHelper fn = FunctionHelper.create(args[0], 1);
return fn == null ? null : new ForEachTerminalOperation(fn);
}
return null;
@@ -94,19 +94,19 @@ abstract class TerminalOperation extends Operation {
return new FindTerminalOperation(resultType.getCanonicalText());
}
if((name.equals("anyMatch") || name.equals("allMatch") || name.equals("noneMatch")) && args.length == 1) {
FunctionHelper fn = FunctionHelper.create(args[0], 1, true);
FunctionHelper fn = FunctionHelper.create(args[0], 1);
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);
FunctionHelper fn = FunctionHelper.create(args[1], 2);
if(fn != null) {
return new ReduceTerminalOperation(args[0], fn, resultType.getCanonicalText());
}
}
if(args.length == 1) {
PsiType optionalElementType = getOptionalElementType(resultType);
FunctionHelper fn = FunctionHelper.create(args[0], 2, true);
FunctionHelper fn = FunctionHelper.create(args[0], 2);
if(fn != null && optionalElementType != null) {
return new ReduceToOptionalTerminalOperation(fn, optionalElementType.getCanonicalText());
}
@@ -135,6 +135,13 @@ abstract class TerminalOperation extends Operation {
"{acc}.toArray("+arr+")");
}
}
if(name.equals("collect") && args.length == 3) {
FunctionHelper supplier = FunctionHelper.create(args[0], 0);
if(supplier == null) return null;
FunctionHelper accumulator = FunctionHelper.create(args[1], 2);
if(accumulator == null) return null;
return new ExplicitCollectTerminalOperation(supplier, accumulator, resultType.getCanonicalText());
}
if(name.equals("collect") && args.length == 1) {
if(args[0] instanceof PsiMethodCallExpression) {
PsiMethodCallExpression collectorCall = (PsiMethodCallExpression)args[0];
@@ -150,20 +157,20 @@ abstract class TerminalOperation extends Operation {
return AccumulatedTerminalOperation.toCollection(resultType, CommonClassNames.JAVA_UTIL_HASH_SET, "set");
}
if(collector.getName().equals("toCollection") && collectorArgs.length == 1) {
FunctionHelper fn = FunctionHelper.create(collectorArgs[0], 0, true);
FunctionHelper fn = FunctionHelper.create(collectorArgs[0], 0);
if(fn != null) {
return new ToCollectionTerminalOperation(fn, resultType);
}
}
if(collector.getName().equals("reducing") && collectorArgs.length == 2) {
FunctionHelper fn = FunctionHelper.create(collectorArgs[1], 2, true);
FunctionHelper fn = FunctionHelper.create(collectorArgs[1], 2);
if(fn != null) {
return new ReduceTerminalOperation(collectorArgs[0], fn, resultType.getCanonicalText());
}
}
if(collector.getName().equals("reducing") && collectorArgs.length == 1) {
PsiType optionalElementType = getOptionalElementType(resultType);
FunctionHelper fn = FunctionHelper.create(collectorArgs[0], 2, true);
FunctionHelper fn = FunctionHelper.create(collectorArgs[0], 2);
if(fn != null && optionalElementType != null) {
return new ReduceToOptionalTerminalOperation(fn, optionalElementType.getCanonicalText());
}
@@ -265,6 +272,38 @@ abstract class TerminalOperation extends Operation {
}
}
static class ExplicitCollectTerminalOperation extends TerminalOperation {
private final FunctionHelper mySupplier;
private final FunctionHelper myAccumulator;
private final String myResultType;
public ExplicitCollectTerminalOperation(FunctionHelper supplier, FunctionHelper accumulator, String resultType) {
mySupplier = supplier;
myAccumulator = accumulator;
myResultType = resultType;
}
@Override
void registerUsedNames(Consumer<String> usedNameConsumer) {
mySupplier.registerUsedNames(usedNameConsumer);
myAccumulator.registerUsedNames(usedNameConsumer);
}
@Override
public void suggestNames(StreamVariable inVar, StreamVariable outVar) {
myAccumulator.suggestVariableName(inVar, 1);
}
@Override
String generate(StreamVariable inVar, StreamToLoopReplacementContext context) {
mySupplier.transform(context);
String candidate = myAccumulator.getParameterName(0);
String acc = context.declareResult(candidate == null ? "acc" : candidate, myResultType, mySupplier.getText());
myAccumulator.transform(context, acc, inVar.getName());
return myAccumulator.getText()+";\n";
}
}
static class AverageTerminalOperation extends TerminalOperation {
private boolean myDoubleAccumulator;

View File

@@ -0,0 +1,22 @@
// "Replace Stream API chain with loop" "true"
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class Main {
private static void test(List<String> list) {
StringBuilder sb = new StringBuilder();
for (String str : list) {
if (Objects.nonNull(str)) {
sb.append(str);
}
}
String result = sb.toString();
System.out.println(result);
}
public static void main(String[] args) {
test(Arrays.asList("aa", "bbb", "c", null, "dd"));
}
}

View File

@@ -0,0 +1,29 @@
// "Replace Stream API chain with loop" "true"
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Main {
private static long test(List<? extends String> list) {
long count = 0;
for (Object o : Arrays.<Object>asList(0, null, "1", list)) {
for (Object o1 : Arrays.<Object>asList(o)) {
for (Object o2 : Arrays.<Object>asList(o1)) {
for (Object o3 : Arrays.<Object>asList(o2)) {
for (Object o4 : Arrays.<Object>asList(o3)) {
for (Object o5 : Arrays.<Object>asList(o4)) {
count++;
}
}
}
}
}
}
return count;
}
public static void main(String[] args) {
test(Arrays.asList("aa", "bbb", "c", null, "dd"));
}
}

View File

@@ -0,0 +1,19 @@
// "Replace Stream API chain with loop" "true"
import java.util.List;
public class Main {
private static void test(List<String> list) {
long count1 = 0;
for (String s : list) {
if (!s.isEmpty()) {
count1++;
}
}
long count = count1;
if(count > 10) {
long result = count*2;
System.out.println(result);
}
}
}

View File

@@ -0,0 +1,16 @@
// "Replace Stream API chain with loop" "true"
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class Main {
private static void test(List<String> list) {
String result = list.stream().filter(Objects::nonNull).coll<caret>ect(StringBuilder::new, (sb, str) -> sb.append(str), StringBuilder::append).toString();
System.out.println(result);
}
public static void main(String[] args) {
test(Arrays.asList("aa", "bbb", "c", null, "dd"));
}
}

View File

@@ -0,0 +1,15 @@
// "Replace Stream API chain with loop" "true"
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Main {
private static long test(List<? extends String> list) {
return Stream.of(0, null, "1", list).flatMap(Stream::of).flatMap(Stream::of).flatMap(Stream::of).flatMap(Stream::of).flatMap(Stream::of).coun<caret>t();
}
public static void main(String[] args) {
test(Arrays.asList("aa", "bbb", "c", null, "dd"));
}
}

View File

@@ -0,0 +1,13 @@
// "Replace Stream API chain with loop" "true"
import java.util.List;
public class Main {
private static void test(List<String> list) {
long count = list.stream().filter(s -> !s.isEmpty()).co<caret>unt();
if(count > 10) {
long result = count*2;
System.out.println(result);
}
}
}