constructor reference: don't ignore constructor parameters during method reference...
[idea/community.git] / java / java-impl / src / com / intellij / codeInspection / streamToLoop / SourceOperation.java
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInspection.streamToLoop;
3
4 import com.intellij.codeInspection.streamToLoop.StreamToLoopInspection.StreamToLoopReplacementContext;
5 import com.intellij.openapi.util.text.StringUtil;
6 import com.intellij.psi.*;
7 import com.intellij.psi.util.InheritanceUtil;
8 import com.intellij.psi.util.PsiUtil;
9 import com.siyeh.ig.psiutils.ExpressionUtils;
10 import com.siyeh.ig.psiutils.StreamApiUtil;
11 import one.util.streamex.StreamEx;
12 import org.jetbrains.annotations.Contract;
13 import org.jetbrains.annotations.NotNull;
14 import org.jetbrains.annotations.Nullable;
15
16 import java.util.Arrays;
17 import java.util.Objects;
18 import java.util.function.Consumer;
19
20 import static com.intellij.codeInspection.streamToLoop.FunctionHelper.replaceVarReference;
21
22 abstract class SourceOperation extends Operation {
23   @Contract(value = " -> true", pure = true)
24   @Override
25   final boolean changesVariable() {
26     return true;
27   }
28
29   @NotNull
30   @Override
31   final String wrap(StreamVariable inVar, StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
32     // Cannot inline "result" as wrap may register more beforeSteps
33     String result = wrap(outVar, code, context);
34     return context.drainBeforeSteps() + result + context.drainAfterSteps();
35   }
36
37   abstract String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context);
38
39   @Nullable
40   static SourceOperation createSource(PsiMethodCallExpression call, boolean supportUnknownSources) {
41     PsiExpression[] args = call.getArgumentList().getExpressions();
42     PsiType callType = call.getType();
43     if(callType == null || PsiType.VOID.equals(callType)) return null;
44     PsiMethod method = call.resolveMethod();
45     if(method == null) return null;
46     String name = method.getName();
47     PsiClass aClass = method.getContainingClass();
48     if(aClass == null) return null;
49     String className = aClass.getQualifiedName();
50     if(className == null) return null;
51     if ((name.equals("range") || name.equals("rangeClosed")) && args.length == 2 && method.getModifierList().hasExplicitModifier(
52       PsiModifier.STATIC) && InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) {
53       return new RangeSource(args[0], args[1], name.equals("rangeClosed"));
54     }
55     if (name.equals("of") && method.getModifierList().hasExplicitModifier(
56       PsiModifier.STATIC) && InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) {
57       if (method.getParameterList().getParametersCount() != 1) return null;
58       if (args.length == 1) {
59         PsiType type = args[0].getType();
60         PsiType componentType = null;
61         if (type instanceof PsiArrayType) {
62           componentType = ((PsiArrayType)type).getComponentType();
63         }
64         else if (InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_LANG_ITERABLE)) {
65           componentType = PsiUtil.substituteTypeParameter(type, CommonClassNames.JAVA_LANG_ITERABLE, 0, false);
66         }
67         PsiType elementType = StreamApiUtil.getStreamElementType(callType);
68         if (componentType != null && elementType.isAssignableFrom(componentType)) {
69           return new ForEachSource(args[0]);
70         }
71         if (type == null || !elementType.isAssignableFrom(type)) return null;
72       }
73       return new ExplicitSource(call);
74     }
75     if (name.equals("generate") && args.length == 1 && method.getModifierList().hasExplicitModifier(
76       PsiModifier.STATIC) && InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) {
77       FunctionHelper fn = FunctionHelper.create(args[0], 0);
78       return fn == null ? null : new GenerateSource(fn, null);
79     }
80     if (name.equals("iterate") && args.length == 2 && method.getModifierList().hasExplicitModifier(
81       PsiModifier.STATIC) && InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_STREAM_BASE_STREAM)) {
82       FunctionHelper fn = FunctionHelper.create(args[1], 1);
83       return fn == null ? null : new IterateSource(args[0], fn);
84     }
85     if (name.equals("stream") && args.length == 0 &&
86         InheritanceUtil.isInheritor(aClass, CommonClassNames.JAVA_UTIL_COLLECTION)) {
87       PsiExpression qualifier = ExpressionUtils.getEffectiveQualifier(call.getMethodExpression());
88       if (qualifier != null) {
89         return new ForEachSource(qualifier);
90       }
91     }
92     if (name.equals("stream") && args.length == 1 &&
93         CommonClassNames.JAVA_UTIL_ARRAYS.equals(className)) {
94       return new ForEachSource(args[0]);
95     }
96     if (name.equals("stream") &&
97         args.length == 3 &&
98         CommonClassNames.JAVA_UTIL_ARRAYS.equals(className) &&
99         args[0].getType() != null &&
100         PsiType.INT.equals(args[1].getType()) &&
101         PsiType.INT.equals(args[2].getType())) {
102       return new ArraySliceSource(args[0], args[1], args[2]);
103     }
104     if (supportUnknownSources) {
105       PsiType type = StreamApiUtil.getStreamElementType(call.getType(), false);
106       if (type != null) {
107         return new StreamIteratorSource(call, type);
108       }
109     }
110     return null;
111   }
112
113   static class ForEachSource extends SourceOperation {
114     private final boolean myEntrySet;
115     private @NotNull PsiExpression myQualifier;
116
117     ForEachSource(@NotNull PsiExpression qualifier) {
118       this(qualifier, false);
119     }
120
121     ForEachSource(@NotNull PsiExpression qualifier, boolean entrySet) {
122       myQualifier = qualifier;
123       myEntrySet = entrySet;
124     }
125
126     @Override
127     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
128       myQualifier = replaceVarReference(myQualifier, oldName, newName, context);
129     }
130
131     @Override
132     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
133       consumer.accept(myQualifier);
134     }
135
136     @Override
137     public void preprocessVariables(StreamToLoopReplacementContext context, StreamVariable inVar, StreamVariable outVar) {
138       if (myQualifier instanceof PsiReferenceExpression) {
139         String name = ((PsiReferenceExpression)myQualifier).getReferenceName();
140         if(name != null) {
141           String singularName = StringUtil.unpluralize(name);
142           if(singularName != null && !name.equals(singularName)) {
143             outVar.addOtherNameCandidate(singularName);
144           }
145         }
146       }
147     }
148
149     @Override
150     public String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
151       String iterationParameterText = myQualifier.getText() + (myEntrySet ? ".entrySet()" : "");
152       return context.getLoopLabel() + "for(" + outVar.getDeclaration() + ": " + iterationParameterText + ") {" + code + "}\n";
153     }
154   }
155
156   static class ExplicitSource extends SourceOperation {
157     private PsiMethodCallExpression myCall;
158
159     ExplicitSource(PsiMethodCallExpression call) {
160       myCall = call;
161     }
162
163     @Override
164     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
165       myCall = replaceVarReference(myCall, oldName, newName, context);
166     }
167
168     @Override
169     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
170       consumer.accept(myCall.getArgumentList());
171     }
172
173     @Override
174     public String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
175       PsiType type = outVar.getType();
176       String iterationParameter;
177       PsiExpressionList argList = myCall.getArgumentList();
178       if (type instanceof PsiPrimitiveType) {
179         // Not using argList.getExpressions() here as we want to preserve comments and formatting between the expressions
180         PsiElement[] children = argList.getChildren();
181         // first and last children are (parentheses), we need to remove them
182         iterationParameter = StreamEx.of(children, 1, children.length - 1)
183           .map(PsiElement::getText)
184           .joining("", "new " + type.getCanonicalText() + "[] {", "}");
185       }
186       else {
187         iterationParameter = "java.util.Arrays.<" + type.getCanonicalText() + ">asList" + argList.getText();
188       }
189       return context.getLoopLabel() +
190              "for(" + outVar.getDeclaration() + ": " + iterationParameter + ") {" + code + "}\n";
191     }
192   }
193
194   static class GenerateSource extends SourceOperation {
195     private final FunctionHelper myFn;
196     private PsiExpression myLimit;
197
198     GenerateSource(FunctionHelper fn, PsiExpression limit) {
199       myFn = fn;
200       myLimit = limit;
201     }
202
203     @Override
204     Operation combineWithNext(Operation next) {
205       if(myLimit == null && next instanceof LimitOperation) {
206         return new GenerateSource(myFn, ((LimitOperation)next).myLimit);
207       }
208       return null;
209     }
210
211     @Override
212     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
213       myFn.rename(oldName, newName, context);
214       if(myLimit != null) {
215         myLimit = replaceVarReference(myLimit, oldName, newName, context);
216       }
217     }
218
219     @Override
220     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
221       myFn.registerReusedElements(consumer);
222       if(myLimit != null) {
223         consumer.accept(myLimit);
224       }
225     }
226
227     @Override
228     String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
229       myFn.transform(context);
230       String loop = "while(true)";
231       if(myLimit != null) {
232         String loopIdx = context.registerVarName(Arrays.asList("count", "limit"));
233         loop = "for(long "+loopIdx+"="+myLimit.getText()+";"+loopIdx+">0;"+loopIdx+"--)";
234       }
235       return context.getLoopLabel() +
236              loop+"{\n" +
237              outVar.getDeclaration(myFn.getText()) + code +
238              "}\n";
239     }
240   }
241
242   static class IterateSource extends SourceOperation {
243     private PsiExpression myInitializer;
244     private final FunctionHelper myFn;
245
246     IterateSource(PsiExpression initializer, FunctionHelper fn) {
247       myInitializer = initializer;
248       myFn = fn;
249     }
250
251     @Override
252     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
253       myInitializer = replaceVarReference(myInitializer, oldName, newName, context);
254       myFn.rename(oldName, newName, context);
255     }
256
257     @Override
258     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
259       consumer.accept(myInitializer);
260       myFn.registerReusedElements(consumer);
261     }
262
263     @Override
264     public void preprocessVariables(StreamToLoopReplacementContext context, StreamVariable inVar, StreamVariable outVar) {
265       myFn.preprocessVariable(context, outVar, 0);
266     }
267
268     @Override
269     String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
270       myFn.transform(context, outVar.getName());
271       return context.getLoopLabel() +
272              "for(" + outVar.getDeclaration() + "=" + myInitializer.getText() + ";;" +
273              outVar + "=" + myFn.getText() + ") {\n" + code + "}\n";
274     }
275   }
276
277   static class RangeSource extends SourceOperation {
278     private PsiExpression myOrigin;
279     private PsiExpression myBound;
280     private final boolean myInclusive;
281
282     RangeSource(PsiExpression origin, PsiExpression bound, boolean inclusive) {
283       myOrigin = origin;
284       myBound = bound;
285       myInclusive = inclusive;
286     }
287
288     @Override
289     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
290       myOrigin = replaceVarReference(myOrigin, oldName, newName, context);
291       myBound = replaceVarReference(myBound, oldName, newName, context);
292     }
293
294     @Override
295     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
296       consumer.accept(myOrigin);
297       consumer.accept(myBound);
298     }
299
300     @Override
301     String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
302       String bound = myBound.getText();
303       if(needBound(context, bound)) {
304         bound = context.declare("bound", outVar.getType().getCanonicalText(), bound);
305       }
306       String loopVar = outVar.getName();
307       String reassign = "";
308       if (outVar.isFinal()) {
309         loopVar = context.registerVarName(Arrays.asList("i", "j", "idx"));
310         reassign = outVar.getDeclaration(loopVar);
311       }
312       return context.getLoopLabel() +
313              "for(" + outVar.getType().getCanonicalText() + " " + loopVar + " = " + myOrigin.getText() + ";" +
314              loopVar + (myInclusive ? "<=" : "<") + bound + ";" +
315              loopVar + "++) {\n" +
316              reassign +
317              code + "}\n";
318     }
319
320     private static boolean needBound(StreamToLoopReplacementContext context, String bound) {
321       PsiExpression expression = PsiUtil.skipParenthesizedExprDown(context.createExpression(bound));
322       while (expression instanceof PsiReferenceExpression) {
323         PsiElement ref = ((PsiReferenceExpression)expression).resolve();
324         if (!(ref instanceof PsiVariable) || !((PsiVariable)ref).hasModifierProperty(PsiModifier.FINAL)) {
325           break;
326         }
327         expression = ((PsiReferenceExpression)expression).getQualifierExpression();
328       }
329       return !ExpressionUtils.isSafelyRecomputableExpression(expression);
330     }
331   }
332
333   static class ArraySliceSource extends SourceOperation {
334     private @NotNull PsiExpression myArray;
335     private @NotNull PsiExpression myOrigin;
336     private @NotNull PsiExpression myBound;
337     private @NotNull final PsiType myArrayType;
338
339     ArraySliceSource(@NotNull PsiExpression array, @NotNull PsiExpression origin, @NotNull PsiExpression bound) {
340       myOrigin = origin;
341       myBound = bound;
342       myArray = array;
343       myArrayType = Objects.requireNonNull(myArray.getType());
344     }
345
346     @Override
347     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
348       myOrigin = replaceVarReference(myOrigin, oldName, newName, context);
349       myBound = replaceVarReference(myBound, oldName, newName, context);
350       myArray = replaceVarReference(myArray, oldName, newName, context);
351     }
352
353     @Override
354     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
355       consumer.accept(myOrigin);
356       consumer.accept(myBound);
357       consumer.accept(myArray);
358     }
359
360     @Override
361     String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
362       String bound = myBound.getText();
363       String array = myArray.getText();
364       if (!ExpressionUtils.isSafelyRecomputableExpression(context.createExpression(array))) {
365         array = context.declare("array", myArrayType.getCanonicalText(), array);
366       }
367       if (!ExpressionUtils.isSafelyRecomputableExpression(context.createExpression(bound))) {
368         bound = context.declare("bound", "int", bound);
369       }
370       String loopVar = context.registerVarName(Arrays.asList("i", "j", "idx"));
371       String element = outVar.getDeclaration(array + "[" + loopVar + "]");
372       return context.getLoopLabel() +
373              "for(" + "int " + loopVar + " = " + myOrigin.getText() + ";" +
374              loopVar + "<" + bound + ";" +
375              loopVar + "++) {\n" +
376              element +
377              code + "}\n";
378     }
379   }
380
381   private static class StreamIteratorSource extends SourceOperation {
382     private final String myElementType;
383     private PsiMethodCallExpression myCall;
384
385     StreamIteratorSource(PsiMethodCallExpression call, PsiType type) {
386       myCall = call;
387       myElementType = type.getCanonicalText();
388     }
389
390     @Override
391     void rename(String oldName, String newName, StreamToLoopReplacementContext context) {
392       myCall = replaceVarReference(myCall, oldName, newName, context);
393     }
394
395     @Override
396     public void registerReusedElements(Consumer<? super PsiElement> consumer) {
397       consumer.accept(myCall);
398     }
399
400     @Override
401     public void preprocessVariables(StreamToLoopReplacementContext context, StreamVariable inVar, StreamVariable outVar) {
402       String name = myCall.getMethodExpression().getReferenceName();
403       if (name != null) {
404         String unpluralized = StringUtil.unpluralize(name);
405         if (unpluralized != null && !unpluralized.equals(name)) {
406           outVar.addOtherNameCandidate(unpluralized);
407         }
408       }
409     }
410
411     static String getIteratorType(String type) {
412       switch(type) {
413         case "int":
414           return "java.util.PrimitiveIterator.OfInt";
415         case "long":
416           return "java.util.PrimitiveIterator.OfLong";
417         case "double":
418           return "java.util.PrimitiveIterator.OfDouble";
419         default:
420           return CommonClassNames.JAVA_UTIL_ITERATOR+"<"+type+">";
421       }
422     }
423
424     @Override
425     String wrap(StreamVariable outVar, String code, StreamToLoopReplacementContext context) {
426       String iterator = context.registerVarName(Arrays.asList("it", "iter", "iterator"));
427       String declaration = getIteratorType(myElementType) + " " + iterator + "=" + myCall.getText() + ".iterator()";
428       String condition = iterator + ".hasNext()";
429       return "for(" + declaration + ";" + condition + ";) {\n" +
430              outVar.getDeclaration(iterator + ".next()") +
431              code + "}\n";
432     }
433   }
434 }