Skip to content

Commit 585d612

Browse files
committed
Fix breakpoint in local object
1 parent 72200ef commit 585d612

File tree

5 files changed

+97
-39
lines changed

5 files changed

+97
-39
lines changed

modules/core/src/main/scala/ch/epfl/scala/debugadapter/internal/stepfilter/ScalaStepFilter.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ abstract class ScalaStepFilter(scalaVersion: ScalaVersion) extends StepFilter {
8888
private def isAnonClass(tpe: ReferenceType): Boolean =
8989
tpe.name.contains("$anon$")
9090

91+
/** is local class or local object */
9192
private def isLocalClass(tpe: ReferenceType): Boolean =
92-
tpe.name.matches(".+\\$\\d+")
93+
tpe.name.matches(".+\\$\\d+\\$?")
9394

9495
private def isNestedClass(tpe: ReferenceType): Boolean =
9596
tpe.name.matches(".+\\$\\.+")

modules/scala-3-step-filter/src/main/scala/ch/epfl/scala/debugadapter/internal/jdi/Method.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ class Method(val obj: Any) extends JavaReflection(obj, "com.sun.jdi.Method"):
1919
case e: InvocationTargetException if e.getCause.getClass.getName == "com.sun.jdi.ClassNotLoadedException" =>
2020
None
2121

22+
def isExtensionMethod: Boolean =
23+
name.endsWith("$extension")
24+
2225
override def toString: String = invokeMethod("toString")

modules/scala-3-step-filter/src/main/scala/ch/epfl/scala/debugadapter/internal/stepfilter/ScalaStepFilterBridge.scala

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package ch.epfl.scala.debugadapter.internal.stepfilter
22

3-
import tastyquery.Contexts.Context
3+
import ch.epfl.scala.debugadapter.internal.jdi
44
import tastyquery.Contexts
5-
import tastyquery.jdk.ClasspathLoaders
6-
import tastyquery.jdk.ClasspathLoaders.FileKind
5+
import tastyquery.Contexts.Context
6+
import tastyquery.Flags
77
import tastyquery.Names.*
8+
import tastyquery.Signatures.*
89
import tastyquery.Symbols.*
9-
import java.util.function.Consumer
10+
import tastyquery.Types.*
11+
import tastyquery.jdk.ClasspathLoaders
12+
import tastyquery.jdk.ClasspathLoaders.FileKind
13+
1014
import java.nio.file.Path
11-
import ch.epfl.scala.debugadapter.internal.jdi
12-
import tastyquery.Flags
13-
import scala.util.matching.Regex
15+
import java.util.function.Consumer
16+
import scala.util.Failure
17+
import scala.util.Success
1418
import scala.util.Try
15-
import tastyquery.Types.*
16-
import tastyquery.Signatures.*
19+
import scala.util.matching.Regex
1720

1821
class ScalaStepFilterBridge(
1922
classpaths: Array[Path],
@@ -25,36 +28,40 @@ class ScalaStepFilterBridge(
2528

2629
private def warn(msg: String): Unit = warnLogger.accept(msg)
2730

28-
private def throwOrWarn(msg: String): Unit =
29-
if (testMode) throw new Exception(msg)
30-
else warn(msg)
31+
private def throwOrWarn(exception: Throwable): Unit =
32+
if (testMode) throw exception
33+
else exception.getMessage
3134

3235
def skipMethod(obj: Any): Boolean =
33-
findSymbol(obj).forall(skip)
36+
findSymbolImpl(obj) match
37+
case Failure(exception) => throwOrWarn(exception); false
38+
case Success(Some(symbol)) => skip(symbol)
39+
case Success(None) => true
3440

3541
private[stepfilter] def findSymbol(obj: Any): Option[TermSymbol] =
42+
findSymbolImpl(obj).get
43+
44+
private def findSymbolImpl(obj: Any): Try[Option[TermSymbol]] =
3645
val method = jdi.Method(obj)
37-
val isExtensionMethod = method.name.endsWith("$extension")
3846
val fqcn = method.declaringType.name
39-
findDeclaringType(fqcn, isExtensionMethod) match
47+
findDeclaringType(fqcn, method.isExtensionMethod) match
4048
case None =>
41-
throwOrWarn(s"Cannot find Scala symbol of $fqcn")
42-
None
49+
Failure(Exception(s"Cannot find Scala symbol of $fqcn"))
4350
case Some(declaringType) =>
4451
val matchingSymbols =
4552
declaringType.declarations
4653
.collect { case sym: TermSymbol if sym.isTerm => sym }
47-
.filter(matchSymbol(method, _, isExtensionMethod))
54+
.filter(matchSymbol(method, _))
4855

4956
if matchingSymbols.size > 1 then
5057
val builder = new java.lang.StringBuilder
5158
builder.append(
5259
s"Found ${matchingSymbols.size} matching symbols for $method:"
5360
)
5461
matchingSymbols.foreach(sym => builder.append(s"\n$sym"))
55-
throwOrWarn(builder.toString)
62+
Failure(Exception(builder.toString))
5663

57-
matchingSymbols.headOption
64+
Success(matchingSymbols.headOption)
5865

5966
private[stepfilter] def extractScalaTerms(
6067
fqcn: String,
@@ -94,34 +101,25 @@ class ScalaStepFilterBridge(
94101
case _ => None
95102
}
96103

97-
private def matchSymbol(method: jdi.Method, symbol: TermSymbol, isExtensionMethod: Boolean): Boolean =
98-
matchTargetName(method, symbol, isExtensionMethod) &&
99-
matchSignature(method, symbol, isExtensionMethod)
104+
private def matchSymbol(method: jdi.Method, symbol: TermSymbol): Boolean =
105+
matchTargetName(method, symbol) && matchSignature(method, symbol)
100106

101-
def matchTargetName(
102-
method: jdi.Method,
103-
symbol: TermSymbol,
104-
isExtensionMethod: Boolean
105-
): Boolean =
107+
def matchTargetName(method: jdi.Method, symbol: TermSymbol): Boolean =
106108
val javaPrefix = method.declaringType.name.replace('.', '$') + "$$"
107109
// if an inner accesses a private method, the backend makes the method public
108110
// and prefixes its name with the full class name.
109111
// Example: method foo in class example.Inner becomes example$Inner$$foo
110112
val expectedName = method.name.stripPrefix(javaPrefix)
111113
val encodedScalaName = NameTransformer.encode(symbol.targetName.toString)
112-
if isExtensionMethod then encodedScalaName == expectedName.stripSuffix("$extension")
114+
if method.isExtensionMethod then encodedScalaName == expectedName.stripSuffix("$extension")
113115
else encodedScalaName == expectedName
114116

115-
def matchSignature(
116-
method: jdi.Method,
117-
symbol: TermSymbol,
118-
isExtensionMethod: Boolean
119-
): Boolean =
117+
def matchSignature(method: jdi.Method, symbol: TermSymbol): Boolean =
120118
try {
121119
symbol.signedName match
122120
case SignedName(_, sig, _) =>
123121
val javaArgs = method.arguments.headOption.map(_.name) match
124-
case Some("$this") if isExtensionMethod => method.arguments.tail
122+
case Some("$this") if method.isExtensionMethod => method.arguments.tail
125123
case _ => method.arguments
126124
matchArguments(sig.paramsSig, javaArgs) &&
127125
method.returnType.forall(matchType(sig.resSig, _))

modules/scala-3-step-filter/src/test/scala/ch/epfl/scala/debugadapter/internal/stepfilter/ScalaStepFilterBridgeTests.scala

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,34 @@ abstract class ScalaStepFilterBridgeTests(scalaVersion: ScalaVersion) extends Fu
7070
assert(findM("example.F$").isEmpty)
7171
assert(findM("example.Main$G").isDefined)
7272
assert(findM("example.Main$H").isEmpty)
73-
assert(findM("example.Main$$anon$1").isEmpty)
73+
intercept[Exception](findM("example.Main$$anon$1"))
7474
// TODO fix: we could find it by traversing the tree of `Main`
75-
assert(findM("example.Main$$anon$2").isEmpty)
75+
intercept[Exception](findM("example.Main$$anon$2"))
76+
}
77+
78+
test("local classes or local objects") {
79+
val source =
80+
"""|package example
81+
|
82+
|object Main {
83+
| def main(args: Array[String]) = {
84+
| class A {
85+
| def m(): Unit = {
86+
| println("A.m")
87+
| }
88+
| }
89+
| object B {
90+
| def m(): Unit = {
91+
| println("B.m")
92+
| }
93+
| }
94+
| }
95+
|}
96+
|""".stripMargin
97+
val debuggee = TestingDebuggee.mainClass(source, "example.Main", scalaVersion)
98+
val stepFilter = getStepFilter(debuggee)
99+
intercept[Exception](stepFilter.findSymbol(FakeJdiMethod("example.Main$A$1", "m")()("void")))
100+
intercept[Exception](stepFilter.findSymbol(FakeJdiMethod("example.Main$B$2$", "m")()("void")))
76101
}
77102

78103
test("getters and setters") {
@@ -268,7 +293,7 @@ abstract class ScalaStepFilterBridgeTests(scalaVersion: ScalaVersion) extends Fu
268293
|""".stripMargin
269294
val debuggee = TestingDebuggee.mainClass(source, "example.Main", scalaVersion)
270295
val stepFilter = getStepFilter(debuggee)
271-
val anonymousfun = FakeJdiMethod("Main$", "$anonfun$1")("x" -> "int")("int")
296+
val anonymousfun = FakeJdiMethod("example.Main$", "$anonfun$1")("x" -> "int")("int")
272297
// TODO fix: it should find the symbol f by traversing the tree of object Main
273298
assert(stepFilter.findSymbol(anonymousfun).isEmpty)
274299
}

modules/tests/src/test/scala/ch/epfl/scala/debugadapter/StepFilterTests.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,37 @@ abstract class StepFilterTests(protected val scalaVersion: ScalaVersion) extends
137137
)
138138
}
139139

140+
test("step into local class or local object") {
141+
val source =
142+
"""|package example
143+
|
144+
|object Main {
145+
| def main(args: Array[String]) = {
146+
| class A {
147+
| def m(): Unit = {
148+
| println("A.m")
149+
| }
150+
| }
151+
| object B {
152+
| def m(): Unit = {
153+
| println("B.m")
154+
| }
155+
| }
156+
| val a = new A
157+
| a.m()
158+
| B.m()
159+
| }
160+
|}
161+
|""".stripMargin
162+
implicit val debuggee: TestingDebuggee = TestingDebuggee.mainClass(source, "example.Main", scalaVersion)
163+
check(
164+
Breakpoint(16),
165+
StepIn.line(7),
166+
// object B becomes a lazy ref at runtime, to avoid all the steps, it just breaks in B.m()
167+
Breakpoint(12)
168+
)
169+
}
170+
140171
test("should not step into getters") {
141172
val source =
142173
"""|package example

0 commit comments

Comments
 (0)