IDEA-167942 Migrate to simplified collection factories (JEP 269): initial implementation (supported scenarios 2, 3, 4 for sets and similar for lists)

This commit is contained in:
Tagir Valeev
2017-02-27 10:35:34 +07:00
parent 671c9cee20
commit 2dda8819cc
22 changed files with 465 additions and 26 deletions
@@ -0,0 +1,220 @@
/*
* Copyright 2000-2017 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.java19api;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.ex.BaseLocalInspectionTool;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.callMatcher.CallMapper;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.ClassUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.MethodCallUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.intellij.util.ObjectUtils.tryCast;
/**
* @author Tagir Valeev
*/
public class Java9CollectionFactoryInspection extends BaseLocalInspectionTool {
private static final CallMatcher UNMODIFIABLE_SET =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_COLLECTIONS, "unmodifiableSet").parameterCount(1);
private static final CallMatcher UNMODIFIABLE_LIST =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_COLLECTIONS, "unmodifiableList").parameterCount(1);
private static final CallMatcher ARRAYS_AS_LIST =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_ARRAYS, "asList");
private static final CallMatcher COLLECTION_ADD =
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_COLLECTION, "add").parameterCount(1);
private static final CallMatcher STREAM_COLLECT =
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_STREAM_STREAM, "collect").parameterCount(1);
private static final CallMatcher STREAM_OF =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_STREAM, "of");
private static final CallMatcher COLLECTORS_TO_SET =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, "toSet").parameterCount(0);
private static final CallMatcher COLLECTORS_TO_LIST =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_STREAM_COLLECTORS, "toList").parameterCount(0);
private static final CallMapper<PrepopulatedCollectionModel> MAPPER = new CallMapper<PrepopulatedCollectionModel>()
.register(UNMODIFIABLE_SET, call -> PrepopulatedCollectionModel.fromSet(call.getArgumentList().getExpressions()[0]))
.register(UNMODIFIABLE_LIST, call -> PrepopulatedCollectionModel.fromList(call.getArgumentList().getExpressions()[0]));
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!PsiUtil.isLanguageLevel9OrHigher(holder.getFile())) {
return PsiElementVisitor.EMPTY_VISITOR;
}
return new JavaElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
PrepopulatedCollectionModel model = MAPPER.mapFirst(call);
if(model != null) {
PsiElement element = call.getMethodExpression().getReferenceNameElement();
if(element != null) {
holder.registerProblem(element, "Can be replaced with '"+model.myType+".of' call",
new ReplaceWithCollectionFactoryFix(model.myType));
}
}
}
};
}
static class PrepopulatedCollectionModel {
final List<PsiExpression> myContent;
final List<PsiStatement> myStatementsToDelete;
final String myType;
PrepopulatedCollectionModel(List<PsiExpression> content, List<PsiStatement> delete, String type) {
myContent = content;
myStatementsToDelete = delete;
myType = type;
}
public static PrepopulatedCollectionModel fromList(PsiExpression listDefinition) {
listDefinition = PsiUtil.skipParenthesizedExprDown(listDefinition);
if(listDefinition instanceof PsiMethodCallExpression) {
PsiMethodCallExpression call = (PsiMethodCallExpression)listDefinition;
if (ARRAYS_AS_LIST.test(call)) {
return new PrepopulatedCollectionModel(Arrays.asList(call.getArgumentList().getExpressions()), Collections.emptyList(), "List");
}
if(STREAM_COLLECT.test(call) && COLLECTORS_TO_LIST.matches(call.getArgumentList().getExpressions()[0])) {
PsiMethodCallExpression qualifier = MethodCallUtils.getQualifierMethodCall(call);
if(STREAM_OF.matches(qualifier)) {
return new PrepopulatedCollectionModel(Arrays.asList(qualifier.getArgumentList().getExpressions()), Collections.emptyList(),
"List");
}
}
}
if(listDefinition instanceof PsiNewExpression) {
return fromNewExpression((PsiNewExpression)listDefinition, "List", CommonClassNames.JAVA_UTIL_ARRAY_LIST);
}
return null;
}
public static PrepopulatedCollectionModel fromSet(PsiExpression setDefinition) {
setDefinition = PsiUtil.skipParenthesizedExprDown(setDefinition);
if(setDefinition instanceof PsiNewExpression) {
return fromNewExpression((PsiNewExpression)setDefinition, "Set", CommonClassNames.JAVA_UTIL_HASH_SET);
}
if(setDefinition instanceof PsiMethodCallExpression) {
PsiMethodCallExpression call = (PsiMethodCallExpression)setDefinition;
if(STREAM_COLLECT.test(call) && COLLECTORS_TO_SET.matches(call.getArgumentList().getExpressions()[0])) {
PsiMethodCallExpression qualifier = MethodCallUtils.getQualifierMethodCall(call);
if(STREAM_OF.matches(qualifier)) {
return new PrepopulatedCollectionModel(Arrays.asList(qualifier.getArgumentList().getExpressions()), Collections.emptyList(),
"Set");
}
}
}
return null;
}
@Nullable
private static PrepopulatedCollectionModel fromNewExpression(PsiNewExpression newExpression, String type, String className) {
PsiExpressionList argumentList = newExpression.getArgumentList();
if (argumentList != null) {
PsiExpression[] args = argumentList.getExpressions();
PsiJavaCodeReferenceElement classReference = newExpression.getClassReference();
if (classReference != null && className.equals(classReference.getQualifiedName())) {
return fromArraysAsList(args, type);
}
PsiAnonymousClass anonymousClass = newExpression.getAnonymousClass();
if (anonymousClass != null && args.length == 0) {
PsiJavaCodeReferenceElement baseClassReference = anonymousClass.getBaseClassReference();
if (className.equals(baseClassReference.getQualifiedName())) {
return fromInitializer(anonymousClass, type);
}
}
}
return null;
}
@Nullable
private static PrepopulatedCollectionModel fromArraysAsList(PsiExpression[] args, String type) {
if (args.length == 1) {
PsiMethodCallExpression arg = tryCast(PsiUtil.skipParenthesizedExprDown(args[0]), PsiMethodCallExpression.class);
if (ARRAYS_AS_LIST.test(arg)) {
return new PrepopulatedCollectionModel(Arrays.asList(arg.getArgumentList().getExpressions()), Collections.emptyList(), type);
}
}
return null;
}
@Nullable
private static PrepopulatedCollectionModel fromInitializer(PsiAnonymousClass anonymousClass, String type) {
PsiClassInitializer initializer = ClassUtils.getDoubleBraceInitializer(anonymousClass);
if(initializer != null) {
List<PsiExpression> contents = new ArrayList<>();
for(PsiStatement statement : initializer.getBody().getStatements()) {
if(!(statement instanceof PsiExpressionStatement)) return null;
PsiMethodCallExpression call = tryCast(((PsiExpressionStatement)statement).getExpression(), PsiMethodCallExpression.class);
if(!COLLECTION_ADD.test(call)) return null;
PsiExpression qualifier = call.getMethodExpression().getQualifierExpression();
if(qualifier != null && !qualifier.getText().equals("this")) return null;
contents.add(call.getArgumentList().getExpressions()[0]);
}
return new PrepopulatedCollectionModel(contents, Collections.emptyList(), type);
}
return null;
}
}
private static class ReplaceWithCollectionFactoryFix implements LocalQuickFix {
private String myType;
public ReplaceWithCollectionFactoryFix(String type) {myType = type;}
@Nls
@NotNull
@Override
public String getName() {
return "Replace with '"+myType+".of' call";
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return "Replace with collection factory call";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiMethodCallExpression call = PsiTreeUtil.getParentOfType(descriptor.getStartElement(), PsiMethodCallExpression.class);
if(call == null) return;
PrepopulatedCollectionModel model = MAPPER.mapFirst(call);
if(model == null) return;
CommentTracker ct = new CommentTracker();
model.myStatementsToDelete.forEach(ct::delete);
ct.replaceAndRestoreComments(call, StreamEx.of(model.myContent).map(ct::text)
.joining(",", "java.util." + model.myType + ".of(", ")"));
}
}
}
@@ -0,0 +1,9 @@
// "Replace with 'List.of' call" "true"
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Test {
public static final List<Number> EVEN = List.of(2, 4, 6, 8, 10);
}
@@ -0,0 +1,8 @@
// "Replace with 'List.of' call" "true"
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test {
public static final List<Number> EVEN = List.of(0, 2, 4, 6, 8);
}
@@ -0,0 +1,8 @@
// "Replace with 'List.of' call" "true"
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Test {
public static final List<Number> EVEN = List.of(2, 4, 6, 8, 10);
}
@@ -0,0 +1,9 @@
// "Replace with 'Set.of' call" "true"
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Test {
public static final Set<String> MY_SET = Set.of("a", "b", "c");
}
@@ -0,0 +1,8 @@
// "Replace with 'Set.of' call" "true"
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Test {
public static final Set<String> MY_SET = Set.of("a", "b", "c".toUpperCase());
}
@@ -0,0 +1,9 @@
// "Replace with 'List.of' call" "true"
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Test {
public static final List<Class<?>> MY_LIST = List.of(String.class, int.class, Object.class);
}
@@ -0,0 +1,9 @@
// "Replace with 'Set.of' call" "true"
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Test {
public static final Set<Class<?>> MY_SET = Set.of(String.class, int.class, Object.class);
}
@@ -0,0 +1,9 @@
// "Replace with 'List.of' call" "true"
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Test {
public static final List<Number> EVEN = Collections.unmo<caret>difiableList(new ArrayList<>(Arrays.asList(2,4,6,8,10)));
}
@@ -0,0 +1,14 @@
// "Replace with 'List.of' call" "true"
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test {
public static final List<Number> EVEN = Collections.unmodif<caret>iableList(new ArrayList<>() {{
this.add(0);
this.add(2);
this.add(4);
this.add(6);
this.add(8);
}});
}
@@ -0,0 +1,8 @@
// "Replace with 'List.of' call" "true"
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Test {
public static final List<Number> EVEN = Collections.unmodi<caret>fiableList(Arrays.asList(2,4,6,8,10));
}
@@ -0,0 +1,9 @@
// "Replace with 'Set.of' call" "true"
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Test {
public static final Set<String> MY_SET = Collections.unm<caret>odifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));
}
@@ -0,0 +1,12 @@
// "Replace with 'Set.of' call" "true"
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Test {
public static final Set<String> MY_SET = Collections.unmodi<caret>fiableSet(new HashSet<String>() {{
add("a");
add("b");
add("c".toUpperCase());
}});
}
@@ -0,0 +1,14 @@
// "Replace with 'Set.of' call" "false"
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class Test {
static Set<String> WRONG_SET = new HashSet<>();
public static final Set<String> MY_SET = Collections.unmod<caret>ifiableSet(new HashSet<String>() {{
add("a");
add("b");
WRONG_SET.add("c".toUpperCase());
}});
}
@@ -0,0 +1,10 @@
// "Replace with 'List.of' call" "true"
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Test {
public static final List<Class<?>> MY_LIST = Collections
.unmo<caret>difiableList(Stream.of(String.class, int.class, Object.class).collect(Collectors.toList()));
}
@@ -0,0 +1,10 @@
// "Replace with 'Set.of' call" "true"
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Test {
public static final Set<Class<?>> MY_SET = Collections
.un<caret>modifiableSet(Stream.of(String.class, int.class, Object.class).collect(Collectors.toSet()));
}
@@ -0,0 +1,46 @@
/*
* Copyright 2000-2017 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.java19api;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.pom.java.LanguageLevel;
import org.jetbrains.annotations.NotNull;
/**
* @author Tagir Valeev
*/
public class Java9CollectionFactoryInspectionTest extends LightQuickFixParameterizedTestCase {
@Override
protected LanguageLevel getLanguageLevel() {
return LanguageLevel.JDK_1_9;
}
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new Java9CollectionFactoryInspection()};
}
public void test() throws Exception {
doAllTests();
}
@Override
protected String getBasePath() {
return "/inspection/java9CollectionFactory";
}
}
@@ -17,6 +17,7 @@ package com.siyeh.ig.callMatcher;
import com.intellij.psi.*;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.MethodCallUtils;
@@ -44,6 +45,18 @@ public interface CallMatcher extends Predicate<PsiMethodCallExpression> {
@Contract("null -> false")
boolean test(@Nullable PsiMethodCallExpression call);
/**
* Returns true if the supplied expression is (possibly parenthesized) method call which matches this matcher
*
* @param expression expression to test
* @return true if the supplied expression matches this matcher
*/
@Contract("null -> false")
default boolean matches(@Nullable PsiExpression expression) {
expression = PsiUtil.skipParenthesizedExprDown(expression);
return expression instanceof PsiMethodCallExpression && test((PsiMethodCallExpression)expression);
}
/**
* Returns a new matcher which will return true if any of supplied matchers return true
*
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2017 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.
@@ -24,6 +24,7 @@ import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.InspectionGadgetsFix;
import com.siyeh.ig.psiutils.ClassUtils;
import com.siyeh.ig.psiutils.ParenthesesUtils;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -180,31 +181,7 @@ public class DoubleBraceInitializationInspection extends BaseInspection {
@Override
public void visitAnonymousClass(PsiAnonymousClass aClass) {
super.visitAnonymousClass(aClass);
final PsiClassInitializer[] initializers = aClass.getInitializers();
if (initializers.length != 1) {
return;
}
final PsiClassInitializer initializer = initializers[0];
if (initializer.hasModifierProperty(PsiModifier.STATIC)) {
// don't warn on broken code
return;
}
final PsiField[] fields = aClass.getFields();
if (fields.length != 0) {
return;
}
final PsiMethod[] methods = aClass.getMethods();
if (methods.length != 0) {
return;
}
final PsiClass[] innerClasses = aClass.getInnerClasses();
if (innerClasses.length != 0) {
return;
}
final PsiJavaCodeReferenceElement reference = aClass.getBaseClassReference();
if (reference.resolve() == null) {
return;
}
if (ClassUtils.getDoubleBraceInitializer(aClass) == null) return;
registerClassError(aClass, aClass);
}
}
@@ -223,4 +223,27 @@ public class ClassUtils {
final PsiClass parentClass = (PsiClass)parent;
return !parentClass.isInterface();
}
/**
* Returns "double brace" initialization for given anonymous class.
*
* @param aClass anonymous class to extract the "double brace" initializer from
* @return "double brace" initializer or null if the class does not follow double brace initialization anti-pattern
*/
@Nullable
public static PsiClassInitializer getDoubleBraceInitializer(PsiAnonymousClass aClass) {
final PsiClassInitializer[] initializers = aClass.getInitializers();
if (initializers.length != 1) return null;
final PsiClassInitializer initializer = initializers[0];
if (initializer.hasModifierProperty(PsiModifier.STATIC)) return null;
final PsiField[] fields = aClass.getFields();
if (fields.length != 0) return null;
final PsiMethod[] methods = aClass.getMethods();
if (methods.length != 0) return null;
final PsiClass[] innerClasses = aClass.getInnerClasses();
if (innerClasses.length != 0) return null;
final PsiJavaCodeReferenceElement reference = aClass.getBaseClassReference();
if (reference.resolve() == null) return null;
return initializer;
}
}
@@ -0,0 +1,9 @@
<html>
<body>
This inspection helps to convert unmodifiable collections created before Java 9 to new collection factory methods
like <code>List.of</code> or <code>Set.of</code>.
<!-- tooltip end -->
<p>This inspection is available since Java 9 only.</p>
<small>New in 2017.2</small>
</body>
</html>
+5
View File
@@ -857,6 +857,11 @@
groupKey="group.names.language.level.specific.issues.and.migration.aids8" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.java18api.Java8MapForEachInspection"
displayName="Replace with Map.forEach"/>
<localInspection groupPath="Java" language="JAVA" shortName="Java9CollectionFactory"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.code.style.issues" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.java19api.Java9CollectionFactoryInspection"
displayName="Immutable collection creation can be replaced with collection factory call"/>
<localInspection groupPath="Java" language="JAVA" shortName="ExcessiveLambdaUsage"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.verbose.or.redundant.code.constructs" enabledByDefault="true" level="WARNING"