Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for lambda breakpoints #427

Merged
merged 5 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,109 @@
/*******************************************************************************
* 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) {
if (column > -1) {
int startPosition = node.getStartPosition();

int startColumn = this.compilationUnit.getColumnNumber(startPosition);
int endPosition = startPosition + node.getLength();
int endColumn = this.compilationUnit.getColumnNumber(endPosition);
int startLine = this.compilationUnit.getLineNumber(startPosition);
int endLine = this.compilationUnit.getLineNumber(endPosition);

// 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));
testforstephen marked this conversation as resolved.
Show resolved Hide resolved

if ((startLine == endLine && column >= startColumn && column <= endColumn && line == startLine)
|| (startLine != endLine && line >= startLine && line <= endLine
&& (column >= startColumn || column <= endColumn))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this condition only works for expression-style lambda with 2 lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested for

        Arrays.asList(1, 2, 3).stream().map(name -> name).forEach(🔴(item) -> {
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
        });

and

        Arrays.asList(1, 2, 3).stream().map(name -> name).forEach(
            (item) -> 🔴{
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
            System.out.println(item + 1 + 2 + 3 + 4 + 5);
        });

Its working, can you give a example snippet ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case 1:
image
This will hit lambda entry on line 21.

Case 2:
image
This will hit lambda entry on line 22.

(startLine != endLine && line >= startLine && line <= endLine
&& (column >= startColumn || column <= endColumn))

It is not accurate to compare the column number for multi-line lambda. It might be better to compare the offset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. But I couldn’t find a method to calculate offset by providing the line and column. But will check more to see if i have missed any such method. JDT should have such methods, not sure if they are part of the API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use this.compilationUnit#getPosition(line, column) to get the offset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I think i missed it. The problem with current condition is I didn’t intend to support blocks. But still if we only do boundry check and ignore middle lines it will work when we have multi lines. But position is much simpler. I will switch to that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the scenario 1 as user what you would expect ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the scenario 1 as user what you would expect ?

I expect adding a breakpoint on (Line 22, Column 24), and if Column 24 breakpoint is not supported, then fallback to a line breakpoint on Line 22.

If it's inside a block body, I'll treat it as a normal function body and not fall back to the Lambda entry breakpoint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LambdaExpression.getBody() will return either a Block or an Expression, which we can use to determine which lambda it is.

If it's an expression-style lambda, it's ok to enable Lambda breakpoint if the offset is within the whole LambdaExpression range.
If it's a block-style lambda, I would enable Lambda breakpoint only if the offset is in [LambdaStart, LambdaBodyStart).

Just my two cents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now keeping mind the future expansion of inline breakpoints for method invocations, I decided to restrict the lambda breakpoints by

  • only support for expressions
  • the inline breakpoint should be before the expression

This way we will not break any functionality nor complicate breakpoint logic in future when we introduce method invocation inline breakpoints. And for lambda blocks we can always use line breakpoints which is much clear and straight forward.

WDYT ?

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