DFA: support partial streams and ::isInstance method reference

Fixes:
IDEA-182519 Support non-terminated Stream API chains in data flow
IDEA-182520 Support Xyz.class::isInstance in stream/optional chains
This commit is contained in:
Tagir Valeev
2017-11-22 14:23:00 +07:00
parent 7c9f5ce2c9
commit 80e63e20fc
14 changed files with 185 additions and 14 deletions
@@ -446,6 +446,9 @@ public class GuessManagerImpl extends GuessManager {
@Override
public DfaInstructionState[] visitInstanceof(InstanceofInstruction instruction, DataFlowRunner runner, DfaMemoryState memState) {
if (instruction.getLeft() == null) {
return super.visitInstanceof(instruction, runner, memState);
}
DfaValue type = memState.pop();
DfaValue operand = memState.pop();
DfaValue relation = runner.getFactory().createCondition(operand, DfaRelationValue.RelationType.IS, type);
@@ -498,6 +498,7 @@ public class CFGBuilder {
JavaResolveResult resolveResult = methodRef.advancedResolve(false);
PsiMethod method = ObjectUtils.tryCast(resolveResult.getElement(), PsiMethod.class);
if (method != null && !method.isVarArgs()) {
if (processKnownMethodReference(argCount, methodRef, method)) return this;
int expectedArgCount = method.getParameterList().getParametersCount();
boolean pushQualifier = true;
if (!method.hasModifierProperty(PsiModifier.STATIC) && !method.isConstructor()) {
@@ -548,6 +549,17 @@ public class CFGBuilder {
return this;
}
private boolean processKnownMethodReference(int argCount, PsiMethodReferenceExpression methodRef, PsiMethod method) {
if (argCount != 1 || !method.getName().equals("isInstance")) return false;
PsiClassObjectAccessExpression qualifier = ObjectUtils.tryCast(PsiUtil.skipParenthesizedExprDown(methodRef.getQualifierExpression()),
PsiClassObjectAccessExpression.class);
if (qualifier == null) return false;
PsiType type = qualifier.getOperand().getType();
push(getFactory().createTypeValue(type, Nullness.NOT_NULL));
myAnalyzer.addInstruction(new InstanceofInstruction(methodRef, methodRef.getProject(), null, type));
return true;
}
/**
* Generate instructions to move top stack value to the specified depth
* <p>
@@ -616,6 +616,10 @@ public class DataFlowInspectionBase extends AbstractBaseJavaLocalInspectionTool
InspectionsBundle.message("dataflow.message.loop.on.empty.array") :
InspectionsBundle.message("dataflow.message.loop.on.empty.collection"));
}
else if (psiAnchor instanceof PsiMethodReferenceExpression) {
holder.registerProblem(psiAnchor, InspectionsBundle.message("dataflow.message.constant.method.reference", evaluatesToTrue),
createReplaceWithTrivialLambdaFix(evaluatesToTrue));
}
else {
boolean isAssertion = isAssertionEffectively(psiAnchor, evaluatesToTrue);
if (!DONT_REPORT_TRUE_ASSERT_STATEMENTS || !isAssertion) {
@@ -19,10 +19,8 @@ import com.intellij.codeInspection.InspectionsBundle;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiInstanceOfExpression;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import org.jetbrains.annotations.NotNull;
/**
@@ -38,10 +36,17 @@ public class RedundantInstanceofFix implements LocalQuickFix {
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement psiElement = descriptor.getPsiElement();
String replacement;
if (psiElement instanceof PsiInstanceOfExpression) {
PsiExpression compareToNull = JavaPsiFacade.getInstance(psiElement.getProject()).getElementFactory().
createExpressionFromText(((PsiInstanceOfExpression)psiElement).getOperand().getText() + " != null", psiElement.getParent());
psiElement.replace(compareToNull);
replacement = ((PsiInstanceOfExpression)psiElement).getOperand().getText() + " != null";
}
else if (psiElement instanceof PsiMethodReferenceExpression) {
replacement = CommonClassNames.JAVA_UTIL_OBJECTS + "::nonNull";
}
else {
return;
}
PsiExpression compareToNull = JavaPsiFacade.getElementFactory(project).createExpressionFromText(replacement, psiElement.getParent());
JavaCodeStyleManager.getInstance(project).shortenClassReferences(psiElement.replace(compareToNull));
}
}
@@ -103,12 +103,29 @@ public class StreamChainInliner implements CallInliner {
.register(MIN_MAX_TERMINAL, MinMaxTerminalStep::new)
.register(OPTIONAL_TERMINAL, OptionalTerminalStep::new);
private static final Step NULL_TERMINAL_STEP = new Step(null, null, null) {
@Override
void before(CFGBuilder builder) {
builder.flushFields();
}
@Override
void iteration(CFGBuilder builder) {
builder.pop();
}
@Override
void pushResult(CFGBuilder builder) {
builder.pushUnknown();
}
};
static abstract class Step {
final Step myNext;
final @NotNull PsiMethodCallExpression myCall;
final PsiMethodCallExpression myCall;
final PsiExpression myFunction;
Step(@NotNull PsiMethodCallExpression call, Step next, PsiExpression function) {
Step(PsiMethodCallExpression call, Step next, PsiExpression function) {
myNext = next;
myCall = call;
myFunction = function;
@@ -480,9 +497,30 @@ public class StreamChainInliner implements CallInliner {
@Override
public boolean tryInlineCall(@NotNull CFGBuilder builder, @NotNull PsiMethodCallExpression call) {
if (!TERMINAL_CALL.test(call)) {
if (TERMINAL_CALL.test(call)) {
return inlineCompleteStream(builder, call);
}
else {
return inlinePartialStream(builder, call);
}
}
private static boolean inlinePartialStream(@NotNull CFGBuilder builder, @NotNull PsiMethodCallExpression call) {
Step firstStep = buildChain(call, NULL_TERMINAL_STEP);
if (firstStep == NULL_TERMINAL_STEP) {
return false;
}
PsiExpression originalQualifier = firstStep.myCall.getMethodExpression().getQualifierExpression();
if (originalQualifier == null) return false;
builder.pushUnknown()
.ifConditionIs(true)
.chain(b -> buildStreamCFG(b, firstStep, originalQualifier))
.endIf()
.push(builder.getFactory().createTypeValue(call.getType(), Nullness.NOT_NULL));
return true;
}
private static boolean inlineCompleteStream(@NotNull CFGBuilder builder, @NotNull PsiMethodCallExpression call) {
PsiMethodCallExpression qualifierCall = MethodCallUtils.getQualifierMethodCall(call);
Step terminalStep = createTerminalStep(call);
Step firstStep = buildChain(qualifierCall, terminalStep);
@@ -25,15 +25,16 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author peter
*/
public class InstanceofInstruction extends BinopInstruction {
@NotNull private final PsiExpression myLeft;
@Nullable private final PsiExpression myLeft;
@NotNull private final PsiType myCastType;
public InstanceofInstruction(PsiElement psiAnchor, @NotNull Project project, @NotNull PsiExpression left, @NotNull PsiType castType) {
public InstanceofInstruction(PsiElement psiAnchor, @NotNull Project project, @Nullable PsiExpression left, @NotNull PsiType castType) {
super(JavaTokenType.INSTANCEOF_KEYWORD, psiAnchor, project);
myLeft = left;
myCastType = castType;
@@ -44,7 +45,11 @@ public class InstanceofInstruction extends BinopInstruction {
return visitor.visitInstanceof(this, runner, stateBefore);
}
@NotNull
/**
* @return instanceof operand or null if it's not applicable
* (e.g. instruction is emitted when inlining Xyz.class::isInstance method reference)
*/
@Nullable
public PsiExpression getLeft() {
return myLeft;
}
@@ -0,0 +1,9 @@
// "Replace with a null check" "true"
class Test {
void test(String s) {
Object obj = s;
if(obj != null) {
System.out.println("always");
}
}
}
@@ -0,0 +1,10 @@
// "Replace with a null check" "true"
import java.util.Objects;
import java.util.stream.Stream;
class Test {
void test(Stream<String> s) {
s.filter(Objects::nonNull)
.forEach(System.out::println);
}
}
@@ -0,0 +1,9 @@
// "Replace with a null check" "true"
class Test {
void test(String s) {
Object obj = s;
if(obj instanceof <caret>String) {
System.out.println("always");
}
}
}
@@ -0,0 +1,9 @@
// "Replace with a null check" "true"
import java.util.stream.Stream;
class Test {
void test(Stream<String> s) {
s.filter(String.class::<caret>isInstance)
.forEach(System.out::println);
}
}
@@ -57,6 +57,40 @@ public class StreamInlining {
.forEach(System.out::println);
}
void testIsInstanceIncomplete(List<?> objects) {
IntStream is = objects.stream()
.filter(String.class::isInstance)
.mapToInt(x -> (<warning descr="Casting 'x' to 'Integer' may produce 'java.lang.ClassCastException'">Integer</warning>)x);
objects.stream()
.filter(String.class::isInstance)
.filter(<warning descr="Method reference result is always 'false'">Number.class::isInstance</warning>);
objects.stream()
.filter(x -> x instanceof String)
.filter(<warning descr="Method reference result is always 'false'">Number.class::isInstance</warning>);
objects.stream()
.filter(String.class::isInstance)
.filter(<warning descr="Method reference result is always 'true'">String.class::isInstance</warning>);
}
Stream<String> testInstanceOfMap(List<?> objects) {
return objects.stream().filter(it -> it instanceof String)
.map(entry -> {
if (<warning descr="Condition 'entry instanceof String' is always 'true'">entry instanceof String</warning>) {
return (String)entry;
}
return null;
})
.filter(<warning descr="Method reference result is always 'true'">Objects::nonNull</warning>);
}
void test(Stream<String> stream, Optional<String> opt) {
stream.filter(<warning descr="Condition 'String.class::isInstance' is redundant and can be replaced with '!= null'">String.class::isInstance</warning>).forEach(System.out::println);
opt.filter(<warning descr="Method reference result is always 'true'">String.class::isInstance</warning>).ifPresent(System.out::println);
}
// IDEA-152871
static class A {
@@ -0,0 +1,32 @@
// Copyright 2000-2017 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.
package com.intellij.java.codeInsight.daemon.quickFix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.dataFlow.DataFlowInspection;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class RedundantInstanceofFixTest extends LightQuickFixParameterizedTestCase {
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new DataFlowInspection()};
}
public void test() {
doAllTests();
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return LightCodeInsightFixtureTestCase.JAVA_8;
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/redundantInstanceOf";
}
}
@@ -59,6 +59,7 @@ public class DataFlowInspectionTestSuite {
suite.addTestSuite(ReplaceWithTrivialLambdaFixTest.class);
suite.addTestSuite(UnwrapIfStatementFixTest.class);
suite.addTestSuite(StreamFilterNotNullFixTest.class);
suite.addTestSuite(RedundantInstanceofFixTest.class);
return suite;
}
}
@@ -48,7 +48,7 @@ inspection.data.flow.turn.off.true.asserts.quickfix=Don't report always true ass
inspection.data.flow.turn.off.constant.references.quickfix=Don't report values which are guaranteed to be constant
inspection.data.flow.turn.off.nullable.returning.notnull.quickfix=Don't report nullable methods which always return not-null value
inspection.data.flow.turn.off.unchecked.optional.get.quickfix=Don't report Optional.get() calls without previous isPresent check
inspection.data.flow.redundant.instanceof.quickfix=Replace with != null
inspection.data.flow.redundant.instanceof.quickfix=Replace with a null check
inspection.data.flow.simplify.boolean.expression.quickfix=Simplify boolean expression
inspection.data.flow.simplify.to.assignment.quickfix.name=Simplify to normal assignment
inspection.data.flow.filter.notnull.quickfix=Insert 'filter(Objects::nonNull)' step