mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
OverwrittenKeyInspection: support Java 9 collection factories
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user