OverwrittenKeyInspection: support Java 9 collection factories

This commit is contained in:
Tagir Valeev
2017-10-04 12:19:06 +07:00
parent daaa6e228e
commit 2b9670ef7b
4 changed files with 122 additions and 74 deletions
@@ -12,10 +12,13 @@ import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.VariableAccessUtils;
import one.util.streamex.IntStreamEx;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.Function;
import static com.intellij.util.ObjectUtils.tryCast;
@@ -24,88 +27,126 @@ public class OverwrittenKeyInspection extends BaseJavaBatchLocalInspectionTool {
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_SET, "add").parameterCount(1);
private static final CallMatcher MAP_PUT =
CallMatcher.instanceCall(CommonClassNames.JAVA_UTIL_MAP, "put").parameterCount(2);
private static final CallMatcher SET_OF =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_SET, "of");
private static final CallMatcher MAP_OF =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_MAP, "of");
private static final CallMatcher MAP_OF_ENTRIES =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_MAP, "ofEntries");
private static final CallMatcher MAP_ENTRY =
CallMatcher.staticCall(CommonClassNames.JAVA_UTIL_MAP, "entry");
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
Set<PsiMethodCallExpression> analyzed = new HashSet<>();
return new OverwrittenKeyVisitor(holder, isOnTheFly);
}
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
PsiExpressionStatement statement = tryCast(call.getParent(), PsiExpressionStatement.class);
if (statement == null) return;
CallMatcher myMatcher;
private static class OverwrittenKeyVisitor extends JavaElementVisitor {
private final ProblemsHolder myHolder;
private final boolean myIsOnTheFly;
private final Set<PsiMethodCallExpression> analyzed = new HashSet<>();
public OverwrittenKeyVisitor(ProblemsHolder holder, boolean isOnTheFly) {
myHolder = holder;
myIsOnTheFly = isOnTheFly;
}
@Override
public void visitMethodCallExpression(PsiMethodCallExpression call) {
PsiExpressionStatement statement = tryCast(call.getParent(), PsiExpressionStatement.class);
if (statement != null) {
if (SET_ADD.test(call)) {
myMatcher = SET_ADD;
processCallSequence(call, statement, SET_ADD, InspectionsBundle.message("inspection.overwritten.key.set.message"));
}
else if (MAP_PUT.test(call)) {
myMatcher = MAP_PUT;
}
else {
return;
}
if (!analyzed.add(call)) return;
Object key = getKey(call);
if (key == null) return;
PsiExpression qualifier = PsiUtil.skipParenthesizedExprDown(ExpressionUtils.getQualifierOrThis(call.getMethodExpression()));
if (qualifier == null) return;
PsiVariable qualifierVar =
qualifier instanceof PsiReferenceExpression ? tryCast(((PsiReferenceExpression)qualifier).resolve(), PsiVariable.class) : null;
Map<Object, List<PsiMethodCallExpression>> map = new HashMap<>();
map.computeIfAbsent(key, k -> new ArrayList<>()).add(call);
while (true) {
PsiExpressionStatement nextStatement =
tryCast(PsiTreeUtil.getNextSiblingOfType(statement, PsiStatement.class), PsiExpressionStatement.class);
if (nextStatement == null) break;
PsiMethodCallExpression nextCall = tryCast(nextStatement.getExpression(), PsiMethodCallExpression.class);
if (!myMatcher.test(nextCall)) break;
PsiExpression nextQualifier =
PsiUtil.skipParenthesizedExprDown(ExpressionUtils.getQualifierOrThis(nextCall.getMethodExpression()));
if (nextQualifier == null || !PsiEquivalenceUtil.areElementsEquivalent(qualifier, nextQualifier)) break;
analyzed.add(nextCall);
if (qualifierVar != null && VariableAccessUtils.variableIsUsed(qualifierVar, nextCall.getArgumentList())) break;
Object nextKey = getKey(nextCall);
if (nextKey != null) {
map.computeIfAbsent(nextKey, k -> new ArrayList<>()).add(nextCall);
}
statement = nextStatement;
}
for (List<PsiMethodCallExpression> calls : map.values()) {
if (calls.size() < 2) continue;
for (int i = 0; i < calls.size(); i++) {
PsiMethodCallExpression dup = calls.get(i);
PsiExpression arg = dup.getArgumentList().getExpressions()[0];
LocalQuickFix fix = null;
if (isOnTheFly) {
PsiExpression nextArg = calls.get((i + 1) % calls.size()).getArgumentList().getExpressions()[0];
fix = new NavigateToDuplicateFix(nextArg);
}
String message = myMatcher == SET_ADD ?
InspectionsBundle.message("inspection.overwritten.key.set.message") :
InspectionsBundle.message("inspection.overwritten.key.map.message");
holder.registerProblem(arg, message, fix);
}
processCallSequence(call, statement, MAP_PUT, InspectionsBundle.message("inspection.overwritten.key.map.message"));
}
}
private Object getKey(PsiMethodCallExpression call) {
PsiExpression key = call.getArgumentList().getExpressions()[0];
Object constant = ExpressionUtils.computeConstantExpression(key);
if (constant != null) {
return constant;
}
if (key instanceof PsiReferenceExpression) {
PsiField field = tryCast(((PsiReferenceExpression)key).resolve(), PsiField.class);
if (field instanceof PsiEnumConstant ||
field != null && field.hasModifierProperty(PsiModifier.FINAL) && field.hasModifierProperty(PsiModifier.STATIC)) {
return field;
}
}
return null;
if(SET_OF.test(call)) {
findDuplicates(call.getArgumentList().getExpressions(), InspectionsBundle.message("inspection.overwritten.key.set.message"));
}
};
else if (MAP_OF.test(call)) {
PsiExpression[] args = call.getArgumentList().getExpressions();
findDuplicates(IntStreamEx.range(0, args.length, 2).elements(args).toArray(PsiExpression[]::new),
InspectionsBundle.message("inspection.overwritten.key.map.message"));
}
else if (MAP_OF_ENTRIES.test(call)) {
PsiExpression[] keys = StreamEx.of(call.getArgumentList().getExpressions()).map(PsiUtil::skipParenthesizedExprDown)
.select(PsiMethodCallExpression.class).filter(MAP_ENTRY).map(entryCall -> entryCall.getArgumentList().getExpressions()[0])
.toArray(PsiExpression[]::new);
findDuplicates(keys, InspectionsBundle.message("inspection.overwritten.key.map.message"));
}
}
private void findDuplicates(PsiExpression[] expressions, String message) {
Map<Object, List<PsiExpression>> groups = StreamEx.of(expressions).mapToEntry(OverwrittenKeyVisitor::getKey, Function.identity())
.nonNullKeys().grouping();
registerDuplicates(message, groups);
}
private void processCallSequence(PsiMethodCallExpression call, PsiExpressionStatement statement, CallMatcher myMatcher, String message) {
if (!analyzed.add(call)) return;
PsiExpression arg = call.getArgumentList().getExpressions()[0];
Object key = getKey(arg);
if (key == null) return;
PsiExpression qualifier = PsiUtil.skipParenthesizedExprDown(ExpressionUtils.getQualifierOrThis(call.getMethodExpression()));
if (qualifier == null) return;
PsiVariable qualifierVar =
qualifier instanceof PsiReferenceExpression ? tryCast(((PsiReferenceExpression)qualifier).resolve(), PsiVariable.class) : null;
Map<Object, List<PsiExpression>> map = new HashMap<>();
map.computeIfAbsent(key, k -> new ArrayList<>()).add(arg);
while (true) {
PsiExpressionStatement nextStatement =
tryCast(PsiTreeUtil.getNextSiblingOfType(statement, PsiStatement.class), PsiExpressionStatement.class);
if (nextStatement == null) break;
PsiMethodCallExpression nextCall = tryCast(nextStatement.getExpression(), PsiMethodCallExpression.class);
if (!myMatcher.test(nextCall)) break;
PsiExpression nextQualifier =
PsiUtil.skipParenthesizedExprDown(ExpressionUtils.getQualifierOrThis(nextCall.getMethodExpression()));
if (nextQualifier == null || !PsiEquivalenceUtil.areElementsEquivalent(qualifier, nextQualifier)) break;
analyzed.add(nextCall);
if (qualifierVar != null && VariableAccessUtils.variableIsUsed(qualifierVar, nextCall.getArgumentList())) break;
PsiExpression nextArg = nextCall.getArgumentList().getExpressions()[0];
Object nextKey = getKey(nextArg);
if (nextKey != null) {
map.computeIfAbsent(nextKey, k -> new ArrayList<>()).add(nextArg);
}
statement = nextStatement;
}
registerDuplicates(message, map);
}
private void registerDuplicates(String message, Map<Object, List<PsiExpression>> map) {
for (List<PsiExpression> args : map.values()) {
if (args.size() < 2) continue;
for (int i = 0; i < args.size(); i++) {
PsiExpression arg = args.get(i);
LocalQuickFix fix = null;
if (myIsOnTheFly) {
PsiExpression nextArg = args.get((i + 1) % args.size());
fix = new NavigateToDuplicateFix(nextArg);
}
myHolder.registerProblem(arg, message, fix);
}
}
}
private static Object getKey(PsiExpression key) {
Object constant = ExpressionUtils.computeConstantExpression(key);
if (constant != null) {
return constant;
}
if (key instanceof PsiReferenceExpression) {
PsiField field = tryCast(((PsiReferenceExpression)key).resolve(), PsiField.class);
if (field instanceof PsiEnumConstant ||
field != null && field.hasModifierProperty(PsiModifier.FINAL) && field.hasModifierProperty(PsiModifier.STATIC)) {
return field;
}
}
return null;
}
}
private static class NavigateToDuplicateFix implements LocalQuickFix {
@@ -39,4 +39,11 @@ class OverwrittenKey {
put(<warning descr="Duplicating Map key">Test.C</warning>, "d");
put(Test.E, "e");
}};
void java9() {
Set<String> set = Set.of(<warning descr="Duplicating Set element">"a"</warning>, "b", "c", <warning descr="Duplicating Set element">"a"</warning>);
Map<String, String> map = Map.of("a", "a", <warning descr="Duplicating Map key">"b"</warning>, "b", "c", "b", <warning descr="Duplicating Map key">"b"</warning>, "d");
Map<String, String> map2 = Map.ofEntries(Map.entry("a", "a"), Map.entry(<warning descr="Duplicating Map key">"b"</warning>, "b"),
Map.entry("c", "b"), Map.entry(<warning descr="Duplicating Map key">"b"</warning>, "d"));
}
}
@@ -28,6 +28,6 @@ public class OverwrittenKeyInspectionTest extends LightInspectionTestCase {
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_8;
return JAVA_9;
}
}
@@ -1,7 +1,7 @@
<html>
<body>
Warns if <code>Map</code> key or <code>Set</code> element was overwritten in the sequence of add/put calls. This usually
occurs due to copy-paste error. Example:
Warns if <code>Map</code> key or <code>Set</code> element was overwritten in the sequence of add/put calls or using a Java 9 factory method
like <code>Set.of</code> (which will result in runtime exception). This usually occurs due to copy-paste error. Example:
<pre>
map.put("A", 1);
map.put("B", 2);