Skip to content

Commit

Permalink
Add support for lambda breakpoints (#427)
Browse files Browse the repository at this point in the history
* Add support for lambda breakpoints

The support for method header breakpoints was extended to support
lambda breakpoints using the vscode inline breakpoint feature.

* Optimize lambda search

Only search for lambda when the breakpoing is an inline breakpoint.
Add support for any column position within lambda expression.

* Improve for multi line and multi lambdas

* Improve lambda breakpoints

- Only support for expressions.
- The inline breakpoint should be before the expression start.

* Remove the unnecessary nodeType check

Co-authored-by: Jinbo Wang <[email protected]>
  • Loading branch information
gayanper and testforstephen authored Aug 4, 2022
1 parent 8c0e87d commit fe77989
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,19 @@ public String getCondition() {

@Override
public boolean equals(Object obj) {
if (!(obj instanceof IBreakpoint)) {
if (!(obj instanceof Breakpoint)) {
return super.equals(obj);
}

IBreakpoint breakpoint = (IBreakpoint) obj;
return Objects.equals(this.className(), breakpoint.className()) && this.getLineNumber() == breakpoint.getLineNumber();
Breakpoint breakpoint = (Breakpoint) obj;
return Objects.equals(this.className(), breakpoint.className())
&& this.getLineNumber() == breakpoint.getLineNumber()
&& Objects.equals(this.methodSignature, breakpoint.methodSignature);
}

@Override
public int hashCode() {
return Objects.hash(this.className, this.lineNumber, this.methodSignature);
}

@Override
Expand Down Expand Up @@ -298,7 +305,7 @@ private List<BreakpointRequest> createBreakpointRequests(List<ReferenceType> ref
request.addCountFilter(hitCount);
}
request.enable();
request.putProperty(IBreakpoint.REQUEST_TYPE_FUNCTIONAL, Boolean.valueOf(this.methodSignature != null));
request.putProperty(IBreakpoint.REQUEST_TYPE, computeRequestType());
newRequests.add(request);
} catch (VMDisconnectedException ex) {
// enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be
Expand All @@ -310,6 +317,18 @@ private List<BreakpointRequest> createBreakpointRequests(List<ReferenceType> ref
return newRequests;
}

private Object computeRequestType() {
if (this.methodSignature == null) {
return IBreakpoint.REQUEST_TYPE_LINE;
}

if (this.methodSignature.startsWith("lambda$")) {
return IBreakpoint.REQUEST_TYPE_LAMBDA;
} else {
return IBreakpoint.REQUEST_TYPE_METHOD;
}
}

@Override
public void putProperty(Object key, Object value) {
propertyMap.put(key, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@

package com.microsoft.java.debug.core;

import org.apache.commons.lang3.StringUtils;

import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

import com.sun.jdi.event.ThreadDeathEvent;
import org.apache.commons.lang3.StringUtils;

import com.sun.jdi.ThreadReference;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.event.ThreadDeathEvent;

import io.reactivex.disposables.Disposable;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@

public interface IBreakpoint extends IDebugResource {

String REQUEST_TYPE_FUNCTIONAL = "functional";
String REQUEST_TYPE = "request_type";

int REQUEST_TYPE_LINE = 0;

int REQUEST_TYPE_METHOD = 1;

int REQUEST_TYPE_LAMBDA = 2;

String className();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ public static int convertLineNumber(int line, boolean sourceLinesStartAt1, boole
}
}

/**
* Convert the source platform's column number to the target platform's column
* number.
*
* @param column
* the column number from the source platform
* @param sourceColumnsStartAt1
* the source platform's column starts at 1 or not
* @return the new column number
*/
public static int convertColumnNumber(int column, boolean sourceColumnsStartAt1) {
if (sourceColumnsStartAt1) {
return column - 1;
} else {
return column;
}
}

/**
* Convert the source platform's path format to the target platform's path format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ public IBreakpoint[] setBreakpoints(String source, IBreakpoint[] breakpoints, bo

// Compute the breakpoints that are newly added.
List<IBreakpoint> toAdd = new ArrayList<>();
List<Integer> visitedLineNumbers = new ArrayList<>();
List<Integer> visitedBreakpoints = new ArrayList<>();
for (IBreakpoint breakpoint : breakpoints) {
IBreakpoint existed = breakpointMap.get(String.valueOf(breakpoint.getLineNumber()));
IBreakpoint existed = breakpointMap.get(String.valueOf(breakpoint.hashCode()));
if (existed != null) {
result.add(existed);
visitedLineNumbers.add(existed.getLineNumber());
visitedBreakpoints.add(existed.hashCode());
continue;
} else {
result.add(breakpoint);
Expand All @@ -95,7 +95,7 @@ public IBreakpoint[] setBreakpoints(String source, IBreakpoint[] breakpoints, bo
// Compute the breakpoints that are no longer listed.
List<IBreakpoint> toRemove = new ArrayList<>();
for (IBreakpoint breakpoint : breakpointMap.values()) {
if (!visitedLineNumbers.contains(breakpoint.getLineNumber())) {
if (!visitedBreakpoints.contains(breakpoint.hashCode())) {
toRemove.add(breakpoint);
}
}
Expand All @@ -113,7 +113,7 @@ private void addBreakpointsInternally(String source, IBreakpoint[] breakpoints)
for (IBreakpoint breakpoint : breakpoints) {
breakpoint.putProperty("id", this.nextBreakpointId.getAndIncrement());
this.breakpoints.add(breakpoint);
breakpointMap.put(String.valueOf(breakpoint.getLineNumber()), breakpoint);
breakpointMap.put(String.valueOf(breakpoint.hashCode()), breakpoint);
}
}
}
Expand All @@ -133,7 +133,7 @@ private void removeBreakpointsInternally(String source, IBreakpoint[] breakpoint
// Destroy the breakpoint on the debugee VM.
breakpoint.close();
this.breakpoints.remove(breakpoint);
breakpointMap.remove(String.valueOf(breakpoint.getLineNumber()));
breakpointMap.remove(String.valueOf(breakpoint.hashCode()));
} catch (Exception e) {
logger.log(Level.SEVERE, String.format("Remove breakpoint exception: %s", e.toString()), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.StepEvent;
import com.sun.jdi.request.EventRequest;

public class SetBreakpointsRequestHandler implements IDebugRequestHandler {

Expand Down Expand Up @@ -193,7 +194,8 @@ private void registerBreakpointHandler(IDebugAdapterContext context) {

// find the breakpoint related to this breakpoint event
IBreakpoint expressionBP = getAssociatedEvaluatableBreakpoint(context, (BreakpointEvent) event);
boolean functional = (boolean) event.request().getProperty(IBreakpoint.REQUEST_TYPE_FUNCTIONAL);
String breakpointName = computeBreakpointName(event.request());

if (expressionBP != null) {
CompletableFuture.runAsync(() -> {
engine.evaluateForBreakpoint((IEvaluatableBreakpoint) expressionBP, bpThread).whenComplete((value, ex) -> {
Expand All @@ -205,20 +207,31 @@ private void registerBreakpointHandler(IDebugAdapterContext context) {
debugEvent.eventSet.resume();
} else {
context.getProtocolServer().sendEvent(new Events.StoppedEvent(
functional ? "function breakpoint" : "breakpoint", bpThread.uniqueID()));
breakpointName, bpThread.uniqueID()));
}
});
});
} else {
context.getProtocolServer().sendEvent(new Events.StoppedEvent(
functional ? "function breakpoint" : "breakpoint", bpThread.uniqueID()));
breakpointName, bpThread.uniqueID()));
}
debugEvent.shouldResume = false;
}
});
}
}

private String computeBreakpointName(EventRequest request) {
switch ((int) request.getProperty(IBreakpoint.REQUEST_TYPE)) {
case IBreakpoint.REQUEST_TYPE_LAMBDA:
return "lambda breakpoint";
case IBreakpoint.REQUEST_TYPE_METHOD:
return "function breakpoint";
default:
return "breakpoint";
}
}

/**
* Check whether the condition expression is satisfied, and return a boolean value to determine to resume the thread or not.
*/
Expand Down Expand Up @@ -287,8 +300,13 @@ private IBreakpoint[] convertClientBreakpointsToDebugger(String sourceFile, Type
int[] lines = Arrays.asList(sourceBreakpoints).stream().map(sourceBreakpoint -> {
return AdapterUtils.convertLineNumber(sourceBreakpoint.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1());
}).mapToInt(line -> line).toArray();

int[] columns = Arrays.asList(sourceBreakpoints).stream().map(b -> {
return AdapterUtils.convertColumnNumber(b.column, context.isClientColumnsStartAt1());
}).mapToInt(b -> b).toArray();

ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class);
String[] fqns = sourceProvider.getFullyQualifiedName(sourceFile, lines, null);
String[] fqns = sourceProvider.getFullyQualifiedName(sourceFile, lines, columns);
IBreakpoint[] breakpoints = new IBreakpoint[lines.length];
for (int i = 0; i < lines.length; i++) {
int hitCount = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public Breakpoint(int id, boolean verified, int line, String message) {

public static class SourceBreakpoint {
public int line;
public int column;
public String hitCondition;
public String condition;
public String logMessage;
Expand All @@ -217,6 +218,16 @@ public SourceBreakpoint(int line, String condition, String hitCondition) {
this.condition = condition;
this.hitCondition = hitCondition;
}

/**
* Constructor.
*/
public SourceBreakpoint(int line, String condition, String hitCondition, int column) {
this.line = line;
this.column = column;
this.condition = condition;
this.hitCondition = hitCondition;
}
}

public static class FunctionBreakpoint {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public class BreakpointLocationLocator

private IMethodBinding methodBinding;

public BreakpointLocationLocator(CompilationUnit compilationUnit, int lineNumber, boolean bindingsResolved,
public BreakpointLocationLocator(CompilationUnit compilationUnit, int lineNumber,
boolean bindingsResolved,
boolean bestMatch) {
super(compilationUnit, lineNumber, bindingsResolved, bestMatch);
}
Expand All @@ -45,7 +46,7 @@ public String getMethodSignature() {
if (this.methodBinding == null) {
return null;
}
return toSignature(this.methodBinding);
return toSignature(this.methodBinding, getMethodName());
}

/**
Expand All @@ -70,13 +71,12 @@ public String getFullyQualifiedTypeName() {
return super.getFullyQualifiedTypeName();
}

private String toSignature(IMethodBinding binding) {
static String toSignature(IMethodBinding binding, String name) {
// use key for now until JDT core provides a public API for this.
// "Ljava/util/Arrays;.asList<T:Ljava/lang/Object;>([TT;)Ljava/util/List<TT;>;"
// "([Ljava/lang/String;)V|Ljava/lang/InterruptedException;"
String signatureString = binding.getKey();
if (signatureString != null) {
String name = binding.getName();
int index = signatureString.indexOf(name);
if (index > -1) {
int exceptionIndex = signatureString.indexOf("|", signatureString.lastIndexOf(")"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*******************************************************************************
* Copyright (c) 2022 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:
* Gayan Perera ([email protected]) - initial API and implementation
*******************************************************************************/
package com.microsoft.java.debug;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.LambdaExpression;

public class LambdaExpressionLocator extends ASTVisitor {
private CompilationUnit compilationUnit;
private int line;
private int column;
private boolean found;

private IMethodBinding lambdaMethodBinding;
private LambdaExpression lambdaExpression;

public LambdaExpressionLocator(CompilationUnit compilationUnit, int line, int column) {
this.compilationUnit = compilationUnit;
this.line = line;
this.column = column;
}

@Override
public boolean visit(LambdaExpression node) {
// we only support inline breakpoints which are added before the expression part of the
// lambda. And we don't support lambda blocks since they can be debugged using line
// breakpoints.
if (column > -1) {
int startPosition = node.getStartPosition();
int endPosition = node.getBody().getStartPosition();
int offset = this.compilationUnit.getPosition(line, column);
// lambda on same line:
// list.stream().map(i -> i + 1);
//
// lambda on multiple lines:
// list.stream().map(user
// -> user.isSystem() ? new SystemUser(user) : new EndUser(user));

if (offset >= startPosition && offset <= endPosition) {
this.lambdaMethodBinding = node.resolveMethodBinding();
this.found = true;
this.lambdaExpression = node;
return false;
}
}
return super.visit(node);
}

/**
* Returns <code>true</code> if a lambda is found at given location.
*/
public boolean isFound() {
return found;
}

/**
* Returns the signature of lambda method otherwise return null.
*/
public String getMethodSignature() {
if (!this.found) {
return null;
}
return BreakpointLocationLocator.toSignature(this.lambdaMethodBinding, getMethodName());
}

/**
* Returns the name of lambda method otherwise return null.
*/
public String getMethodName() {
if (!this.found) {
return null;
}
String key = this.lambdaMethodBinding.getKey();
return key.substring(key.indexOf('.') + 1, key.indexOf('('));
}

/**
* Returns the name of the type which the lambda method is found.
*/
public String getFullyQualifiedTypeName() {
if (this.found) {
ASTNode parent = lambdaExpression.getParent();
while (parent != null) {
if (parent instanceof AbstractTypeDeclaration) {
AbstractTypeDeclaration declaration = (AbstractTypeDeclaration) parent;
return declaration.resolveBinding().getBinaryName();
}
parent = parent.getParent();
}
}
return null;
}
}
Loading

0 comments on commit fe77989

Please sign in to comment.