mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-07 05:09:37 +07:00
IDEA-226191 Use Java dataflow to assist the step-by-step debugging: draft implementation
For now switched off by default (use debugger.show.values.from.dfa) GitOrigin-RevId: a983ef0b73ddec7e4987a2c162035e74c72a7b73
This commit is contained in:
committed by
intellij-monorepo-bot
parent
dbb3aea632
commit
1731245617
@@ -4,6 +4,7 @@ package com.intellij.debugger.engine;
|
|||||||
import com.intellij.debugger.DebuggerBundle;
|
import com.intellij.debugger.DebuggerBundle;
|
||||||
import com.intellij.debugger.actions.DebuggerActions;
|
import com.intellij.debugger.actions.DebuggerActions;
|
||||||
import com.intellij.debugger.actions.JvmSmartStepIntoActionHandler;
|
import com.intellij.debugger.actions.JvmSmartStepIntoActionHandler;
|
||||||
|
import com.intellij.debugger.engine.dfaassist.DfaAssist;
|
||||||
import com.intellij.debugger.engine.evaluation.EvaluationContext;
|
import com.intellij.debugger.engine.evaluation.EvaluationContext;
|
||||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl;
|
import com.intellij.debugger.engine.events.SuspendContextCommandImpl;
|
||||||
import com.intellij.debugger.impl.*;
|
import com.intellij.debugger.impl.*;
|
||||||
@@ -186,6 +187,9 @@ public class JavaDebugProcess extends XDebugProcess {
|
|||||||
if (Registry.is("debugger.show.values.between.lines") && session instanceof XDebugSessionImpl) {
|
if (Registry.is("debugger.show.values.between.lines") && session instanceof XDebugSessionImpl) {
|
||||||
((XDebugSessionImpl)session).getSessionData().putUserData(XDebuggerInlayUtil.HELPER_KEY, new JavaDebuggerInlayUtil.Helper());
|
((XDebugSessionImpl)session).getSessionData().putUserData(XDebuggerInlayUtil.HELPER_KEY, new JavaDebuggerInlayUtil.Helper());
|
||||||
}
|
}
|
||||||
|
if (Registry.is("debugger.show.values.from.dfa")) {
|
||||||
|
DfaAssist.installDfaAssist(myJavaSession);
|
||||||
|
}
|
||||||
|
|
||||||
mySmartStepIntoActionHandler = new JvmSmartStepIntoActionHandler(javaSession);
|
mySmartStepIntoActionHandler = new JvmSmartStepIntoActionHandler(javaSession);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
// Copyright 2000-2019 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.debugger.engine.dfaassist;
|
||||||
|
|
||||||
|
import com.intellij.codeInspection.dataFlow.*;
|
||||||
|
import com.intellij.codeInspection.dataFlow.value.*;
|
||||||
|
import com.intellij.openapi.project.Project;
|
||||||
|
import com.intellij.psi.*;
|
||||||
|
import com.intellij.psi.util.PsiTreeUtil;
|
||||||
|
import com.intellij.psi.util.TypeConversionUtil;
|
||||||
|
import com.intellij.util.ObjectUtils;
|
||||||
|
import com.intellij.util.containers.ContainerUtil;
|
||||||
|
import com.sun.jdi.*;
|
||||||
|
import one.util.streamex.EntryStream;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.intellij.codeInspection.dataFlow.value.DfaRelationValue.RelationType.EQ;
|
||||||
|
|
||||||
|
class DebuggerDfaRunner extends DataFlowRunner {
|
||||||
|
private static final Value NullConst = new Value() {
|
||||||
|
@Override
|
||||||
|
public VirtualMachine virtualMachine() { return null; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type type() { return null; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() { return "null"; }
|
||||||
|
};
|
||||||
|
private static final Set<String> COLLECTIONS_WITH_SIZE_FIELD =
|
||||||
|
ContainerUtil.immutableSet(CommonClassNames.JAVA_UTIL_ARRAY_LIST,
|
||||||
|
"java.util.LinkedList",
|
||||||
|
CommonClassNames.JAVA_UTIL_HASH_MAP,
|
||||||
|
"java.util.TreeMap");
|
||||||
|
private static final Map<String, Integer> COLLECTIONS_WITH_KNOWN_SIZE =
|
||||||
|
EntryStream.of("java.util.Collections$EmptyList", 0,
|
||||||
|
"java.util.Collections$EmptySet", 0,
|
||||||
|
"java.util.Collections$EmptyMap", 0,
|
||||||
|
"java.util.Collections$Singleton", 1,
|
||||||
|
"java.util.Collections$SingletonList", 1,
|
||||||
|
"java.util.Collections$SingletonMap", 1).toImmutableMap();
|
||||||
|
private final PsiCodeBlock myBody;
|
||||||
|
private final PsiStatement myStatement;
|
||||||
|
private final Project myProject;
|
||||||
|
private final StackFrame myFrame;
|
||||||
|
|
||||||
|
DebuggerDfaRunner(@NotNull PsiCodeBlock body, @NotNull PsiStatement statement, @NotNull StackFrame frame) {
|
||||||
|
super(false, body);
|
||||||
|
myBody = body;
|
||||||
|
myStatement = statement;
|
||||||
|
myProject = body.getProject();
|
||||||
|
myFrame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
protected List<DfaInstructionState> createInitialInstructionStates(@NotNull PsiElement psiBlock,
|
||||||
|
@NotNull Collection<? extends DfaMemoryState> memStates,
|
||||||
|
@NotNull ControlFlow flow) {
|
||||||
|
if (psiBlock == myBody) {
|
||||||
|
int offset = flow.getStartOffset(myStatement).getInstructionOffset();
|
||||||
|
if (offset >= 0) {
|
||||||
|
boolean changed = false;
|
||||||
|
DfaMemoryState state = super.createMemoryState();
|
||||||
|
PsiElementFactory psiFactory = JavaPsiFacade.getElementFactory(myProject);
|
||||||
|
DfaValueFactory factory = getFactory();
|
||||||
|
for (DfaValue dfaValue : factory.getValues().toArray(new DfaValue[0])) {
|
||||||
|
if (dfaValue instanceof DfaVariableValue) {
|
||||||
|
DfaVariableValue var = (DfaVariableValue)dfaValue;
|
||||||
|
Value jdiValue = findJdiValue(var);
|
||||||
|
if (jdiValue != null) {
|
||||||
|
addToState(psiFactory, factory, state, var, jdiValue);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
return Collections.singletonList(new DfaInstructionState(flow.getInstruction(offset), state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyList(); // Cancel analysis: no reason to continue
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Value findJdiValue(@NotNull DfaVariableValue var) {
|
||||||
|
if (var.getQualifier() != null) {
|
||||||
|
VariableDescriptor descriptor = var.getDescriptor();
|
||||||
|
if (descriptor instanceof SpecialField) {
|
||||||
|
// Special fields facts are applied from qualifiers
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Value qualifierValue = findJdiValue(var.getQualifier());
|
||||||
|
if (qualifierValue == null) return null;
|
||||||
|
PsiModifierListOwner element = descriptor.getPsiElement();
|
||||||
|
if (element instanceof PsiField && qualifierValue instanceof ObjectReference) {
|
||||||
|
ReferenceType type = ((ObjectReference)qualifierValue).referenceType();
|
||||||
|
PsiClass psiClass = ((PsiField)element).getContainingClass();
|
||||||
|
if (psiClass != null && type.name().equals(psiClass.getQualifiedName())) {
|
||||||
|
Field field = type.fieldByName(((PsiField)element).getName());
|
||||||
|
if (field != null) {
|
||||||
|
return wrap(((ObjectReference)qualifierValue).getValue(field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (descriptor instanceof DfaExpressionFactory.ArrayElementDescriptor && qualifierValue instanceof ArrayReference) {
|
||||||
|
int index = ((DfaExpressionFactory.ArrayElementDescriptor)descriptor).getIndex();
|
||||||
|
int length = ((ArrayReference)qualifierValue).length();
|
||||||
|
if (index >= 0 && index < length) {
|
||||||
|
return wrap(((ArrayReference)qualifierValue).getValue(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PsiModifierListOwner psi = var.getPsiVariable();
|
||||||
|
if (psi instanceof PsiClass) {
|
||||||
|
// this
|
||||||
|
if (PsiTreeUtil.getParentOfType(myBody, PsiClass.class) == psi) {
|
||||||
|
return myFrame.thisObject();
|
||||||
|
}
|
||||||
|
// TODO: support references to outer classes
|
||||||
|
}
|
||||||
|
if (psi instanceof PsiLocalVariable || psi instanceof PsiParameter) {
|
||||||
|
try {
|
||||||
|
LocalVariable variable = myFrame.visibleVariableByName(((PsiVariable)psi).getName());
|
||||||
|
if (variable != null) {
|
||||||
|
return wrap(myFrame.getValue(variable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (AbsentInformationException ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addToState(PsiElementFactory psiFactory,
|
||||||
|
DfaValueFactory factory,
|
||||||
|
DfaMemoryState state, DfaVariableValue var,
|
||||||
|
Value jdiValue) {
|
||||||
|
DfaConstValue val = getConstantValue(psiFactory, factory, jdiValue);
|
||||||
|
if (val != null) {
|
||||||
|
state.applyCondition(factory.createCondition(var, EQ, val));
|
||||||
|
}
|
||||||
|
if (jdiValue instanceof ObjectReference) {
|
||||||
|
ObjectReference ref = (ObjectReference)jdiValue;
|
||||||
|
ReferenceType type = ref.referenceType();
|
||||||
|
PsiType psiType = getPsiReferenceType(psiFactory, type);
|
||||||
|
if (psiType == null) return;
|
||||||
|
TypeConstraint exactType = TypeConstraint.exact(factory.createDfaType(psiType));
|
||||||
|
String name = type.name();
|
||||||
|
state.applyFact(var, DfaFactType.NULLABILITY, DfaNullability.NOT_NULL);
|
||||||
|
state.applyFact(var, DfaFactType.TYPE_CONSTRAINT, exactType);
|
||||||
|
if (jdiValue instanceof ArrayReference) {
|
||||||
|
DfaValue dfaLength = SpecialField.ARRAY_LENGTH.createValue(factory, var);
|
||||||
|
int jdiLength = ((ArrayReference)jdiValue).length();
|
||||||
|
state.applyCondition(factory.createCondition(dfaLength, EQ, factory.getInt(jdiLength)));
|
||||||
|
}
|
||||||
|
else if (TypeConversionUtil.isPrimitiveWrapper(name)) {
|
||||||
|
setSpecialField(psiFactory, factory, state, var, ref, type, "value", SpecialField.UNBOX);
|
||||||
|
}
|
||||||
|
else if (COLLECTIONS_WITH_SIZE_FIELD.contains(name)) {
|
||||||
|
setSpecialField(psiFactory, factory, state, var, ref, type, "size", SpecialField.COLLECTION_SIZE);
|
||||||
|
}
|
||||||
|
else if (name.startsWith("java.util.Collections$Empty")) {
|
||||||
|
state.applyCondition(factory.createCondition(
|
||||||
|
SpecialField.COLLECTION_SIZE.createValue(factory, var), EQ, factory.getInt(0)));
|
||||||
|
}
|
||||||
|
else if (name.startsWith("java.util.Collections$Singleton")) {
|
||||||
|
state.applyCondition(factory.createCondition(
|
||||||
|
SpecialField.COLLECTION_SIZE.createValue(factory, var), EQ, factory.getInt(1)));
|
||||||
|
}
|
||||||
|
else if (CommonClassNames.JAVA_UTIL_OPTIONAL.equals(name) && !(var.getDescriptor() instanceof SpecialField)) {
|
||||||
|
setSpecialField(psiFactory, factory, state, var, ref, type, "value", SpecialField.OPTIONAL_VALUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private PsiType getPsiReferenceType(PsiElementFactory psiFactory, ReferenceType jdiType) {
|
||||||
|
Type componentType = jdiType;
|
||||||
|
int depth = 0;
|
||||||
|
while (componentType instanceof ArrayType) {
|
||||||
|
try {
|
||||||
|
componentType = ((ArrayType)componentType).componentType();
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
catch (ClassNotLoadedException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PsiType psiType = psiFactory.createTypeByFQClassName(componentType.name(), myBody.getResolveScope());
|
||||||
|
while (depth > 0) {
|
||||||
|
psiType = psiType.createArrayType();
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
return psiType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSpecialField(PsiElementFactory psiFactory,
|
||||||
|
DfaValueFactory factory,
|
||||||
|
DfaMemoryState state,
|
||||||
|
DfaVariableValue dfaQualifier,
|
||||||
|
ObjectReference jdiQualifier,
|
||||||
|
ReferenceType type,
|
||||||
|
String fieldName,
|
||||||
|
SpecialField specialField) {
|
||||||
|
Field value = type.fieldByName(fieldName);
|
||||||
|
if (value != null) {
|
||||||
|
DfaVariableValue dfaUnboxed = ObjectUtils.tryCast(specialField.createValue(factory, dfaQualifier), DfaVariableValue.class);
|
||||||
|
Value jdiUnboxed = jdiQualifier.getValue(value);
|
||||||
|
if (jdiUnboxed != null && dfaUnboxed != null) {
|
||||||
|
addToState(psiFactory, factory, state, dfaUnboxed, jdiUnboxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private DfaConstValue getConstantValue(PsiElementFactory psiFactory, DfaValueFactory factory, Value jdiValue) {
|
||||||
|
if (jdiValue == NullConst) {
|
||||||
|
return factory.getConstFactory().getNull();
|
||||||
|
}
|
||||||
|
if (jdiValue instanceof BooleanValue) {
|
||||||
|
return factory.getBoolean(((BooleanValue)jdiValue).value());
|
||||||
|
}
|
||||||
|
if (jdiValue instanceof ShortValue || jdiValue instanceof CharValue ||
|
||||||
|
jdiValue instanceof ByteValue || jdiValue instanceof IntegerValue) {
|
||||||
|
return factory.getConstFactory().createFromValue(((PrimitiveValue)jdiValue).intValue(), PsiType.LONG);
|
||||||
|
}
|
||||||
|
if (jdiValue instanceof FloatValue) {
|
||||||
|
return factory.getConstFactory().createFromValue(((FloatValue)jdiValue).floatValue(), PsiType.FLOAT);
|
||||||
|
}
|
||||||
|
if (jdiValue instanceof DoubleValue) {
|
||||||
|
return factory.getConstFactory().createFromValue(((DoubleValue)jdiValue).doubleValue(), PsiType.DOUBLE);
|
||||||
|
}
|
||||||
|
if (jdiValue instanceof StringReference) {
|
||||||
|
return factory.getConstFactory().createFromValue(
|
||||||
|
((StringReference)jdiValue).value(), psiFactory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_STRING,
|
||||||
|
myBody.getResolveScope()));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Value wrap(Value value) {
|
||||||
|
return value == null ? NullConst : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2000-2019 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.debugger.engine.dfaassist;
|
||||||
|
|
||||||
|
import com.intellij.codeInspection.dataFlow.CommonDataflow;
|
||||||
|
import com.intellij.codeInspection.dataFlow.DfaMemoryState;
|
||||||
|
import com.intellij.codeInspection.dataFlow.NullabilityProblemKind;
|
||||||
|
import com.intellij.codeInspection.dataFlow.StandardInstructionVisitor;
|
||||||
|
import com.intellij.codeInspection.dataFlow.value.DfaConstValue;
|
||||||
|
import com.intellij.codeInspection.dataFlow.value.DfaValue;
|
||||||
|
import com.intellij.openapi.util.TextRange;
|
||||||
|
import com.intellij.psi.*;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class DebuggerInstructionVisitor extends StandardInstructionVisitor {
|
||||||
|
private final Map<PsiExpression, DfaHint> myHints = new HashMap<>();
|
||||||
|
|
||||||
|
DebuggerInstructionVisitor() {
|
||||||
|
super(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHint(@NotNull PsiExpression expression, @Nullable DfaHint hint) {
|
||||||
|
if (hint != null) {
|
||||||
|
myHints.merge(expression, hint, DfaHint::merge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeExpressionPush(@NotNull DfaValue value,
|
||||||
|
@NotNull PsiExpression expression,
|
||||||
|
@Nullable TextRange range,
|
||||||
|
@NotNull DfaMemoryState state) {
|
||||||
|
if (range != null) return;
|
||||||
|
DfaHint hint = DfaHint.ANY_VALUE;
|
||||||
|
if (value instanceof DfaConstValue) {
|
||||||
|
Object constVal = ((DfaConstValue)value).getValue();
|
||||||
|
if (Boolean.TRUE.equals(constVal)) {
|
||||||
|
hint = DfaHint.TRUE;
|
||||||
|
}
|
||||||
|
else if (Boolean.FALSE.equals(constVal)) {
|
||||||
|
hint = DfaHint.FALSE;
|
||||||
|
}
|
||||||
|
else if (DfaConstValue.isContractFail(value)) {
|
||||||
|
hint = DfaHint.FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addHint(expression, hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTypeCast(PsiTypeCastExpression castExpression, DfaMemoryState state, boolean castPossible) {
|
||||||
|
if (!castPossible) {
|
||||||
|
addHint(castExpression, DfaHint.CCE);
|
||||||
|
}
|
||||||
|
super.onTypeCast(castExpression, state, castPossible);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void processArrayAccess(PsiArrayAccessExpression expression, boolean alwaysOutOfBounds) {
|
||||||
|
if (alwaysOutOfBounds) {
|
||||||
|
addHint(expression, DfaHint.AIOOBE);
|
||||||
|
}
|
||||||
|
super.processArrayAccess(expression, alwaysOutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void processArrayStoreTypeMismatch(PsiAssignmentExpression assignmentExpression, PsiType fromType, PsiType toType) {
|
||||||
|
addHint(assignmentExpression.getLExpression(), DfaHint.ASE);
|
||||||
|
super.processArrayStoreTypeMismatch(assignmentExpression, fromType, toType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean checkNotNullable(DfaMemoryState state,
|
||||||
|
DfaValue value,
|
||||||
|
@Nullable NullabilityProblemKind.NullabilityProblem<?> problem) {
|
||||||
|
if (problem != null) {
|
||||||
|
PsiExpression expression = problem.getDereferencedExpression();
|
||||||
|
if (expression != null && problem.thrownException() != null) {
|
||||||
|
if (state.isNull(value)) {
|
||||||
|
DfaHint hint = problem.thrownException().equals(CommonClassNames.JAVA_LANG_NULL_POINTER_EXCEPTION)
|
||||||
|
? DfaHint.NPE
|
||||||
|
: DfaHint.NULL_AS_NOT_NULL;
|
||||||
|
addHint(expression, hint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.checkNotNullable(state, value, problem);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup() {
|
||||||
|
myHints.values().removeIf(h -> h.getTitle() == null);
|
||||||
|
myHints.keySet().removeIf(expr -> {
|
||||||
|
CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(expr);
|
||||||
|
return result != null && result.getExpressionValues(expr).size() == 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Map<PsiExpression, DfaHint> getHints() {
|
||||||
|
return myHints;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright 2000-2019 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.debugger.engine.dfaassist;
|
||||||
|
|
||||||
|
import com.intellij.codeInsight.hints.presentation.MenuOnClickPresentation;
|
||||||
|
import com.intellij.codeInsight.hints.presentation.PresentationFactory;
|
||||||
|
import com.intellij.codeInsight.hints.presentation.PresentationRenderer;
|
||||||
|
import com.intellij.codeInspection.dataFlow.RunnerResult;
|
||||||
|
import com.intellij.debugger.SourcePosition;
|
||||||
|
import com.intellij.debugger.engine.DebugProcessImpl;
|
||||||
|
import com.intellij.debugger.engine.JavaDebugProcess;
|
||||||
|
import com.intellij.debugger.engine.SuspendContextImpl;
|
||||||
|
import com.intellij.debugger.engine.events.SuspendContextCommandImpl;
|
||||||
|
import com.intellij.debugger.impl.DebuggerContextImpl;
|
||||||
|
import com.intellij.debugger.impl.DebuggerContextListener;
|
||||||
|
import com.intellij.debugger.impl.DebuggerSession;
|
||||||
|
import com.intellij.debugger.impl.DebuggerStateManager;
|
||||||
|
import com.intellij.debugger.jdi.StackFrameProxyImpl;
|
||||||
|
import com.intellij.icons.AllIcons;
|
||||||
|
import com.intellij.openapi.actionSystem.AnAction;
|
||||||
|
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||||
|
import com.intellij.openapi.application.ApplicationManager;
|
||||||
|
import com.intellij.openapi.application.ReadAction;
|
||||||
|
import com.intellij.openapi.editor.Inlay;
|
||||||
|
import com.intellij.openapi.editor.InlayModel;
|
||||||
|
import com.intellij.openapi.editor.impl.EditorImpl;
|
||||||
|
import com.intellij.openapi.project.Project;
|
||||||
|
import com.intellij.openapi.util.Disposer;
|
||||||
|
import com.intellij.openapi.util.TextRange;
|
||||||
|
import com.intellij.psi.*;
|
||||||
|
import com.intellij.psi.util.PsiTreeUtil;
|
||||||
|
import com.intellij.util.ObjectUtils;
|
||||||
|
import com.sun.jdi.AbsentInformationException;
|
||||||
|
import com.sun.jdi.Method;
|
||||||
|
import com.sun.jdi.StackFrame;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public class DfaAssist implements DebuggerContextListener {
|
||||||
|
private final Project myProject;
|
||||||
|
private final Queue<Inlay<?>> myInlays = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
private DfaAssist(Project project) {
|
||||||
|
myProject = project;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changeEvent(@NotNull DebuggerContextImpl newContext, DebuggerSession.Event event) {
|
||||||
|
if (event == DebuggerSession.Event.DETACHED || event == DebuggerSession.Event.DISPOSE) {
|
||||||
|
disposeInlays();
|
||||||
|
}
|
||||||
|
if (event != DebuggerSession.Event.PAUSE) return;
|
||||||
|
SourcePosition sourcePosition = newContext.getSourcePosition();
|
||||||
|
if (sourcePosition == null) {
|
||||||
|
disposeInlays();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PsiJavaFile file = ObjectUtils.tryCast(sourcePosition.getFile(), PsiJavaFile.class);
|
||||||
|
DebugProcessImpl debugProcess = JavaDebugProcess.getCurrentDebugProcess(myProject);
|
||||||
|
if (debugProcess == null || file == null) {
|
||||||
|
disposeInlays();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PsiElement element = sourcePosition.getElementAt();
|
||||||
|
debugProcess.getManagerThread().schedule(new SuspendContextCommandImpl(newContext.getSuspendContext()) {
|
||||||
|
@Override
|
||||||
|
public void contextAction(@NotNull SuspendContextImpl suspendContext) throws Exception {
|
||||||
|
StackFrameProxyImpl proxy = suspendContext.getFrameProxy();
|
||||||
|
if (proxy == null) {
|
||||||
|
disposeInlays();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StackFrame frame = proxy.getStackFrame();
|
||||||
|
Map<PsiExpression, DfaHint> hints = ReadAction.compute(() -> computeHints(frame, element));
|
||||||
|
displayInlays(hints, sourcePosition);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disposeInlays() {
|
||||||
|
ApplicationManager.getApplication().invokeLater(this::doDisposeInlays);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doDisposeInlays() {
|
||||||
|
while (true) {
|
||||||
|
Inlay<?> inlay = myInlays.poll();
|
||||||
|
if (inlay == null) break;
|
||||||
|
Disposer.dispose(inlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayInlays(Map<PsiExpression, DfaHint> hints, SourcePosition sourcePosition) {
|
||||||
|
if (hints.isEmpty()) {
|
||||||
|
disposeInlays();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApplicationManager.getApplication().invokeLater(
|
||||||
|
() -> {
|
||||||
|
doDisposeInlays();
|
||||||
|
EditorImpl editor = ObjectUtils.tryCast(sourcePosition.openEditor(true), EditorImpl.class);
|
||||||
|
if (editor == null) return;
|
||||||
|
InlayModel model = editor.getInlayModel();
|
||||||
|
List<Inlay<?>> newInlays = new ArrayList<>();
|
||||||
|
AnAction turnOffDfaProcessor = new TurnOffDfaProcessorAction();
|
||||||
|
hints.forEach((expression, hint) -> {
|
||||||
|
TextRange range = expression.getTextRange();
|
||||||
|
PresentationFactory factory = new PresentationFactory(editor);
|
||||||
|
MenuOnClickPresentation presentation = new MenuOnClickPresentation(
|
||||||
|
factory.roundWithBackground(factory.smallText(hint.getTitle())), myProject,
|
||||||
|
() -> Collections.singletonList(turnOffDfaProcessor));
|
||||||
|
newInlays.add(model.addInlineElement(range.getEndOffset(), new PresentationRenderer(presentation)));
|
||||||
|
});
|
||||||
|
myInlays.addAll(newInlays);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
static Map<PsiExpression, DfaHint> computeHints(StackFrame frame, PsiElement element) throws AbsentInformationException {
|
||||||
|
Method method = frame.location().method();
|
||||||
|
if (!element.isValid()) return Collections.emptyMap();
|
||||||
|
|
||||||
|
PsiMethod psiMethod = PsiTreeUtil.getParentOfType(element, PsiMethod.class);
|
||||||
|
if (psiMethod == null || !psiMethod.getName().equals(method.name()) ||
|
||||||
|
psiMethod.getParameterList().getParametersCount() != method.arguments().size()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
PsiStatement statement = getAnchorStatement(element);
|
||||||
|
if (statement == null) return Collections.emptyMap();
|
||||||
|
// TODO: support class initializers
|
||||||
|
// TODO: check/improve lambdas support
|
||||||
|
PsiCodeBlock body = getCodeBlock(statement);
|
||||||
|
if (body == null) return Collections.emptyMap();
|
||||||
|
DebuggerInstructionVisitor visitor = new DebuggerInstructionVisitor();
|
||||||
|
// TODO: read assertion status
|
||||||
|
RunnerResult result = new DebuggerDfaRunner(body, statement, frame).analyzeMethod(body, visitor);
|
||||||
|
if (result != RunnerResult.OK) return Collections.emptyMap();
|
||||||
|
visitor.cleanup();
|
||||||
|
return visitor.getHints();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static PsiStatement getAnchorStatement(@NotNull PsiElement element) {
|
||||||
|
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class, false, PsiMethod.class);
|
||||||
|
if (statement instanceof PsiBlockStatement && ((PsiBlockStatement)statement).getCodeBlock().getRBrace() == element) {
|
||||||
|
statement = PsiTreeUtil.getNextSiblingOfType(statement, PsiStatement.class);
|
||||||
|
}
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static PsiCodeBlock getCodeBlock(@NotNull PsiStatement statement) {
|
||||||
|
PsiElement e = statement;
|
||||||
|
while (e != null && !(e instanceof PsiClass) && !(e instanceof PsiFileSystemItem)) {
|
||||||
|
e = e.getParent();
|
||||||
|
if (e instanceof PsiCodeBlock) {
|
||||||
|
if (e.getParent() instanceof PsiMethod ||
|
||||||
|
e.getParent() instanceof PsiBlockStatement && e.getParent().getParent() instanceof PsiLoopStatement) {
|
||||||
|
return (PsiCodeBlock)e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TurnOffDfaProcessorAction extends AnAction {
|
||||||
|
private TurnOffDfaProcessorAction() {
|
||||||
|
super("Turn Off Dataflow Assist", "Switch off dataflow aided debugging for this session", AllIcons.Actions.Cancel);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(@NotNull AnActionEvent evt) {
|
||||||
|
DebugProcessImpl process = JavaDebugProcess.getCurrentDebugProcess(myProject);
|
||||||
|
if (process != null) {
|
||||||
|
process.getSession().getContextManager().removeListener(DfaAssist.this);
|
||||||
|
disposeInlays();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install dataflow assistant to the specified debugging session
|
||||||
|
* @param javaSession JVM debugger session to install an assistant to
|
||||||
|
*/
|
||||||
|
public static void installDfaAssist(@NotNull DebuggerSession javaSession) {
|
||||||
|
DebuggerStateManager manager = javaSession.getContextManager();
|
||||||
|
manager.addListener(new DfaAssist(manager.getContext().getProject()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2000-2019 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.debugger.engine.dfaassist;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
enum DfaHint {
|
||||||
|
NONE(null), ANY_VALUE(null, true), TRUE("= true", true), FALSE("= false", true),
|
||||||
|
NPE("[NullPointerException]"), NULL_AS_NOT_NULL("[Null passed where not-null expected]"), CCE("[ClassCastException]"),
|
||||||
|
ASE("[ArrayStoreException]"), AIOOBE("[ArrayIndexOutOfBoundsException]"), FAIL("[Method will fail]", true);
|
||||||
|
|
||||||
|
private final String myTitle;
|
||||||
|
private final boolean myValue;
|
||||||
|
|
||||||
|
DfaHint(String title) {
|
||||||
|
this(title, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
DfaHint(String title, boolean value) {
|
||||||
|
myTitle = title;
|
||||||
|
myValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getTitle() {
|
||||||
|
return myTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
DfaHint merge(@NotNull DfaHint other) {
|
||||||
|
if (other == this) return this;
|
||||||
|
if (this.myValue && other.myValue) return ANY_VALUE;
|
||||||
|
if (this.myValue) return other;
|
||||||
|
if (other.myValue) return this;
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -305,9 +305,9 @@ public class DataFlowRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private List<DfaInstructionState> createInitialInstructionStates(@NotNull PsiElement psiBlock,
|
protected List<DfaInstructionState> createInitialInstructionStates(@NotNull PsiElement psiBlock,
|
||||||
@NotNull Collection<? extends DfaMemoryState> memStates,
|
@NotNull Collection<? extends DfaMemoryState> memStates,
|
||||||
@NotNull ControlFlow flow) {
|
@NotNull ControlFlow flow) {
|
||||||
initializeVariables(psiBlock, memStates, flow);
|
initializeVariables(psiBlock, memStates, flow);
|
||||||
return ContainerUtil.map(memStates, s -> new DfaInstructionState(flow.getInstruction(0), s));
|
return ContainerUtil.map(memStates, s -> new DfaInstructionState(flow.getInstruction(0), s));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class StandardInstructionVisitor extends InstructionVisitor {
|
|||||||
myStopAnalysisOnNpe = false;
|
myStopAnalysisOnNpe = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
StandardInstructionVisitor(boolean stopAnalysisOnNpe) {
|
protected StandardInstructionVisitor(boolean stopAnalysisOnNpe) {
|
||||||
myStopAnalysisOnNpe = stopAnalysisOnNpe;
|
myStopAnalysisOnNpe = stopAnalysisOnNpe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -425,13 +425,17 @@ public class DfaExpressionFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class ArrayElementDescriptor implements VariableDescriptor {
|
public static final class ArrayElementDescriptor implements VariableDescriptor {
|
||||||
private final int myIndex;
|
private final int myIndex;
|
||||||
|
|
||||||
ArrayElementDescriptor(int index) {
|
ArrayElementDescriptor(int index) {
|
||||||
myIndex = index;
|
myIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return myIndex;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public PsiType getType(@Nullable DfaVariableValue qualifier) {
|
public PsiType getType(@Nullable DfaVariableValue qualifier) {
|
||||||
|
|||||||
@@ -1482,6 +1482,8 @@ debugger.show.values.inplace=false
|
|||||||
debugger.show.values.inplace.description=Show primitive values near variables inside current execution line
|
debugger.show.values.inplace.description=Show primitive values near variables inside current execution line
|
||||||
debugger.show.values.between.lines=false
|
debugger.show.values.between.lines=false
|
||||||
debugger.show.values.between.lines.description=Show variable values below their definition place
|
debugger.show.values.between.lines.description=Show variable values below their definition place
|
||||||
|
debugger.show.values.from.dfa=false
|
||||||
|
debugger.show.values.from.dfa.description=Enrich debug session with dataflow output (Java only)
|
||||||
|
|
||||||
ide.projectView.ProjectViewPaneTreeStructure.BuildChildrenInBackground=false
|
ide.projectView.ProjectViewPaneTreeStructure.BuildChildrenInBackground=false
|
||||||
ide.projectView.ProjectViewPaneTreeStructure.BuildChildrenInBackground.description=Temporary ability to control a tree building for the Project View pane
|
ide.projectView.ProjectViewPaneTreeStructure.BuildChildrenInBackground.description=Temporary ability to control a tree building for the Project View pane
|
||||||
|
|||||||
Reference in New Issue
Block a user