com.microsoft.java.debug.plugin.internal.InlineValueHandler Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2021 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/
package com.microsoft.java.debug.plugin.internal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.TypeDeclarationStatement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ModuleDeclaration;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.manipulation.CoreASTProvider;
import org.eclipse.jdt.ls.core.internal.JDTUtils;
import org.eclipse.jdt.ls.core.internal.handlers.JsonRpcHelpers;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
public class InlineValueHandler {
/**
* Find the valid inline variables belonging to the visible view port.
*/
public static InlineVariable[] resolveInlineVariables(InlineParams params, IProgressMonitor monitor) {
ITypeRoot root = JDTUtils.resolveTypeRoot(params.uri);
try {
if (root == null || root.getBuffer() == null) {
return new InlineVariable[0];
}
Position stoppedLocation = params.stoppedLocation.getStart();
int stoppedOffset = JsonRpcHelpers.toOffset(root.getBuffer(), stoppedLocation.getLine(), stoppedLocation.getCharacter());
IMethod enclosingMethod = findEnclosingMethod(root, stoppedOffset);
if (enclosingMethod == null) {
return new InlineVariable[0];
}
Position startLocation = getPosition(root.getBuffer(), enclosingMethod.getSourceRange().getOffset());
Range stoppedRange = new Range(startLocation, stoppedLocation);
if (params.viewPort != null
&& (params.viewPort.getEnd().getLine() < startLocation.getLine() || params.viewPort.getStart().getLine() > stoppedLocation.getLine())) {
return new InlineVariable[0];
}
CompilationUnit astRoot = CoreASTProvider.getInstance().getAST(root, CoreASTProvider.WAIT_YES, monitor);
VariableVisitor visitor = new VariableVisitor(astRoot, stoppedRange, params.viewPort, Flags.isStatic(enclosingMethod.getFlags()));
astRoot.accept(visitor);
InlineVariable[] result = visitor.getInlineVariables();
return result;
} catch (JavaModelException e) {
return new InlineVariable[0];
}
}
private static IMethod findEnclosingMethod(ITypeRoot root, int stoppedOffset) throws JavaModelException {
IType enclosingType = null;
if (root instanceof ICompilationUnit) {
IType[] types = ((ICompilationUnit) root).getAllTypes();
for (IType type : types) {
if (isEnclosed(type, stoppedOffset)) {
enclosingType = type;
}
}
} else if (root instanceof IClassFile) {
enclosingType = ((IClassFile) root).getType();
}
if (enclosingType == null) {
return null;
}
IMethod enclosingMethod = null;
for (IMethod method : enclosingType.getMethods()) {
if (isEnclosed(method, stoppedOffset)) {
enclosingMethod = method;
break;
}
}
if (enclosingMethod == null) {
return null;
}
// Deal with the scenario that the stopped location is inside the local types defined in method.
return findMethodInLocalTypes(enclosingMethod, stoppedOffset);
}
private static boolean isEnclosed(ISourceReference sourceReference, int offset) throws JavaModelException {
ISourceRange sourceRange = sourceReference.getSourceRange();
return sourceRange != null && offset >= sourceRange.getOffset()
&& offset < sourceRange.getOffset() + sourceRange.getLength();
}
private static IMethod findMethodInLocalTypes(IMethod enclosingMethod, int stoppedOffset) throws JavaModelException {
if (enclosingMethod == null) {
return null;
}
for (IJavaElement element : enclosingMethod.getChildren()) {
if (element instanceof IType) {
if (isEnclosed((IType) element, stoppedOffset)) {
for (IMethod method : ((IType) element).getMethods()) {
if (isEnclosed(method, stoppedOffset)) {
IMethod nearerMethod = findMethodInLocalTypes(method, stoppedOffset);
return nearerMethod == null ? enclosingMethod : nearerMethod;
}
}
break;
}
}
}
return enclosingMethod;
}
/**
* Returns the zero based line and column number.
*/
private static Position getPosition(IBuffer buffer, int offset) {
int[] result = JsonRpcHelpers.toLine(buffer, offset);
if (result == null || result.length < 1) {
return new Position(-1, -1);
}
return new Position(result[0], result[1]);
}
static class VariableVisitor extends ASTVisitor {
private CompilationUnit unit = null;
private Range stoppedSourceRange;
private Range viewPort;
private boolean isStoppingAtStaticMethod;
private int baseLine;
private Set[] tokens;
private List localVarDecls = new ArrayList<>();
private List localVarDeclPositions = new ArrayList<>();
private boolean isStoppingAtLambda = false;
private Set varDeclsAtLastLine = new HashSet<>();
private Range visibleInlineRange = null;
public VariableVisitor(CompilationUnit unit, Range stoppedSourceRange, Range viewPort, boolean stopAtStaticMethod) {
this.unit = unit;
this.stoppedSourceRange = stoppedSourceRange;
this.viewPort = viewPort;
this.isStoppingAtStaticMethod = stopAtStaticMethod;
this.baseLine = stoppedSourceRange.getStart().getLine();
this.tokens = new Set[stoppedSourceRange.getEnd().getLine() - stoppedSourceRange.getStart().getLine() + 1];
updateVisibleRange();
}
private void updateVisibleRange() {
if (viewPort == null) {
visibleInlineRange = stoppedSourceRange;
} else if (compare(viewPort.getStart(), stoppedSourceRange.getEnd()) > 0
|| compare(viewPort.getEnd(), stoppedSourceRange.getStart()) < 0) {
visibleInlineRange = null;
} else {
Position start = compare(viewPort.getStart(), stoppedSourceRange.getStart()) >= 0 ? viewPort.getStart() : stoppedSourceRange.getStart();
Position end = compare(viewPort.getEnd(), stoppedSourceRange.getEnd()) <= 0 ? viewPort.getEnd() : stoppedSourceRange.getEnd();
visibleInlineRange = new Range(start, end);
}
}
/**
* Handle the variables in the visible source ranges.
*/
@Override
public boolean visit(SimpleName node) {
if (visibleInlineRange == null) {
return false;
}
Position startPosition = getStartPosition(node);
boolean isAtLastLine = isAtStopLocation(startPosition);
if (isEnclosed(visibleInlineRange, startPosition) || isAtLastLine) {
IBinding binding = node.resolveBinding();
if (!(binding instanceof IVariableBinding)) {
return false;
} else if (isAtLastLine && this.varDeclsAtLastLine.contains(binding.getKey())) {
return false;
}
String declaringClass = null;
if (((IVariableBinding) binding).isField()) {
ITypeBinding typeBinding = ((IVariableBinding) binding).getDeclaringClass();
if (typeBinding == null) {
return false;
}
declaringClass = typeBinding.getBinaryName();
}
Token token = new Token(node.getIdentifier(), startPosition, declaringClass);
int index = startPosition.getLine() - baseLine;
if (tokens[index] == null) {
tokens[index] = new LinkedHashSet<>();
}
if (!tokens[index].contains(token)) {
tokens[index].add(token);
}
}
return false;
}
/**
* Handle local variable declarations happening in current method.
*/
@Override
public boolean visit(VariableDeclarationFragment node) {
SimpleName name = node.getName();
Position startPosition = getStartPosition(name);
if (isEnclosed(stoppedSourceRange, startPosition)) {
this.localVarDecls.add(name.getIdentifier());
this.localVarDeclPositions.add(startPosition);
}
if (isAtStopLocation(startPosition)) {
IVariableBinding binding = node.resolveBinding();
if (binding != null) {
this.varDeclsAtLastLine.add(binding.getKey());
}
}
return true;
}
/**
* Handle formal parameter declarations happening in current method.
*/
@Override
public boolean visit(SingleVariableDeclaration node) {
SimpleName name = node.getName();
Position startPosition = getStartPosition(name);
if (isEnclosed(stoppedSourceRange, startPosition)) {
this.localVarDecls.add(name.getIdentifier());
this.localVarDeclPositions.add(startPosition);
}
return false;
}
/**
* Handle the lambda expression containing the stopped location.
* If the execution instruction stops on a lambda expression, then
* crop the visible source ranges to the lambda expression body.
*/
@Override
public boolean visit(LambdaExpression node) {
Position startPosition = getStartPosition(node);
Position endPosition = getEndPosition(node);
if (compare(startPosition, stoppedSourceRange.getStart()) >= 0
&& isEnclosed(new Range(startPosition, endPosition), stoppedSourceRange.getEnd())) {
stoppedSourceRange.setStart(startPosition);
updateVisibleRange();
isStoppingAtLambda = true;
localVarDecls.clear();
localVarDeclPositions.clear();
return true;
}
return super.visit(node);
}
/**
* Handle the method containing the stopped location.
*/
@Override
public boolean visit(MethodDeclaration node) {
Position startPosition = getStartPosition(node);
Position endPosition = getEndPosition(node);
if (compare(startPosition, stoppedSourceRange.getStart()) <= 0 && compare(endPosition, stoppedSourceRange.getEnd()) >= 0) {
return true;
}
return false;
}
@Override
public boolean visit(Block node) {
if (isUnreachableNode(node)) {
return false;
}
return true;
}
@Override
public boolean visit(DoStatement node) {
if (isUnreachableNode(node) && !isAtStopLocation(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(ForStatement node) {
if (isUnreachableNode(node) && !isAtStopLocation(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(IfStatement node) {
if (isUnreachableNode(node) && !isAtStopLocation(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(SwitchStatement node) {
if (isUnreachableNode(node) && !isAtStopLocation(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(WhileStatement node) {
if (isUnreachableNode(node) && !isAtStopLocation(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(AnnotationTypeDeclaration node) {
if (isUnreachableNode(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(AnonymousClassDeclaration node) {
if (isUnreachableNode(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(TypeDeclarationStatement node) {
if (isUnreachableNode(node)) {
return false;
}
return super.visit(node);
}
@Override
public boolean visit(ImportDeclaration node) {
return false;
}
@Override
public boolean visit(ModuleDeclaration node) {
return false;
}
@Override
public boolean visit(PackageDeclaration node) {
return false;
}
@Override
public boolean visit(QualifiedName node) {
return Objects.equals("length", node.getName().getIdentifier());
}
@Override
public boolean visit(QualifiedType node) {
return false;
}
/**
* Return the valid inline variables in the visible source ranges.
*
* There are four typical kinds of variable:
* - Local variables declared in method body.
* - Formal parameters declared in method declaration.
* - Field variables.
* - Captured variables from outer scope. This includes local type is accessing
* variables of enclosing method, and lambda expression body is accessing to
* variables of enclosing method.
*
* For the first two kinds such as local variables and formal parameters,
* we're going to return them with VariableLookup kind since their values are
* expanded by Variables View by default.
*
* For the last two kinds, we're going to return them with Evaluation kind
* since it requires additional evaluation to get its values.
*/
public InlineVariable[] getInlineVariables() {
if (visibleInlineRange == null) {
return new InlineVariable[0];
}
// Adding the local variable declarations to the token list.
for (int i = 0; i < localVarDecls.size(); i++) {
String name = localVarDecls.get(i);
Position position = localVarDeclPositions.get(i);
if (isEnclosed(visibleInlineRange, position)) {
int index = position.getLine() - baseLine;
if (tokens[index] == null) {
tokens[index] = new LinkedHashSet<>();
}
Token token = new Token(name, position, null);
if (!tokens[index].contains(token)) {
tokens[index].add(token);
}
}
}
// For lambda expression in non static method, the captured variable 'arg$1'
// points to 'this' object of the enclosing method, and the index of other
// captured variables starts with 2.
int capturedArgIndexInLambda = isStoppingAtStaticMethod ? 1 : 2;
Map capturedVarsInLambda = new HashMap<>();
List result = new ArrayList<>();
for (int i = 0; i < tokens.length; i++) {
int line = baseLine + i;
if (tokens[i] == null || line < visibleInlineRange.getStart().getLine()) {
continue;
}
for (Token token : tokens[i]) {
if (!isEnclosed(visibleInlineRange, token.position) && !isAtLastVisibleLine(token.position)) {
continue;
}
// Local Variables
if (token.declaringClass == null && localVarDecls.contains(token.name)) {
int declIndex = localVarDecls.lastIndexOf(token.name);
Position declPosition = localVarDeclPositions.get(declIndex);
if (compare(token.position, declPosition) >= 0) {
result.add(new InlineVariable(new Range(token.position, token.position), token.name, InlineKind.VariableLookup));
continue;
}
}
InlineVariable value = new InlineVariable(
new Range(token.position, token.position), token.name, InlineKind.Evaluation, token.declaringClass);
// Captured variables by lambda expression
if (isStoppingAtLambda && token.declaringClass == null) {
/**
* When the lambda body accesses variables from its "outer" scope such as
* its enclosing method, these variables will be captured as properties of
* 'this' object of a synthetic lambda instance by Java runtime. However,
* when the compiler parses the lambda expression, it erases the specific
* variable name but keeps the captured variable names with format like
* 'arg$'. In order to evaluate the correct value from Java runtime,
* we have to encode the variable name using the same rule 'arg$' as
* the compiler.
*/
if (capturedVarsInLambda.containsKey(token.name)) {
value.expression = capturedVarsInLambda.get(token.name);
} else {
value.expression = "arg$" + capturedArgIndexInLambda++;
capturedVarsInLambda.put(token.name, value.expression);
}
}
result.add(value);
}
}
return result.toArray(new InlineVariable[0]);
}
private Position getStartPosition(ASTNode node) {
// Line number returned by AST unit is one based, converts it to zero based.
int lineNumber = unit.getLineNumber(node.getStartPosition()) - 1;
int columnNumber = unit.getColumnNumber(node.getStartPosition());
return new Position(lineNumber, columnNumber);
}
private Position getEndPosition(ASTNode node) {
// Line number returned by AST unit is one based, converts it to zero based.
int lineNumber = unit.getLineNumber(node.getStartPosition() + node.getLength() - 1) - 1;
int columnNumber = unit.getColumnNumber(node.getStartPosition() + node.getLength() - 1);
return new Position(lineNumber, columnNumber);
}
private boolean isUnreachableNode(ASTNode node) {
Position startPosition = getStartPosition(node);
Position endPosition = getEndPosition(node);
return compare(startPosition, stoppedSourceRange.getEnd()) > 0
|| compare(endPosition, stoppedSourceRange.getEnd()) < 0;
}
private boolean isEnclosed(Range range, Position position) {
return compare(range.getStart(), position) <= 0 && compare(range.getEnd(), position) >= 0;
}
private int compare(Position p1, Position p2) {
if (p1.getLine() < p2.getLine()) {
return -1;
} else if (p1.getLine() == p2.getLine()) {
return p1.getCharacter() - p2.getCharacter();
}
return 1;
}
private boolean isAtStopLocation(Position position) {
return position.getLine() == stoppedSourceRange.getEnd().getLine();
}
private boolean isAtStopLocation(ASTNode node) {
Position startPosition = getStartPosition(node);
return isAtStopLocation(startPosition);
}
private boolean isAtLastVisibleLine(Position position) {
return visibleInlineRange != null && visibleInlineRange.getEnd().getLine() == position.getLine();
}
}
static class InlineVariable {
Range range;
String name;
InlineKind kind;
String expression;
String declaringClass;
public InlineVariable(Range range, String name, InlineKind kind) {
this.range = range;
this.name = name;
this.kind = kind;
}
public InlineVariable(Range range, String name, InlineKind kind, String declaringClass) {
this.range = range;
this.name = name;
this.kind = kind;
this.declaringClass = declaringClass;
}
}
static enum InlineKind {
VariableLookup,
Evaluation
}
static class InlineParams {
String uri;
Range viewPort;
Range stoppedLocation;
}
static class Token {
String name;
Position position;
String declaringClass = null;
public Token(String name, Position position) {
this.name = name;
this.position = position;
}
public Token(String name, Position position, String declaringClass) {
this.name = name;
this.position = position;
this.declaringClass = declaringClass;
}
@Override
public int hashCode() {
return Objects.hash(declaringClass, name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Token)) {
return false;
}
Token other = (Token) obj;
return Objects.equals(declaringClass, other.declaringClass) && Objects.equals(name, other.name);
}
}
}