Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Filter out step into forwarder and default getter methods. #224

Merged
merged 2 commits into from

3 participants

@dragos
Owner

A general reorganization of step filters was necessary:

  • made it a class instead of an object
  • split functionality between the filter class and a method classifier
  • split tests in unit tests (using the method classifier) and integration tests (testing the step-into inside a debugger instance)

A separate commit makes the Scala launch delegate behave according to the spec, throwing
CoreException on failure. Failure to do so was making tests hang indefinitely when a debug-test
project had errors, and therefore could not be launched.

Fixed #1001288.

@dragos dragos Throw an exception if the launcher fails.
The `MainMethodVerifier` may stop the Scala launcher if there is no main type,
or the project has errors. The Eclipse API requires that we throw `CoreException`,
but we used to fail silently (leading to the debug tests hanging indefinitely).
e4a73ee
...ls/eclipse/debug/model/MethodClassifierUnitTest.scala
((25 lines not shown))
+import scala.tools.eclipse.testsetup.TestProjectSetup
+
+object MethodClassifierUnitTest extends TestProjectSetup("constant-pool", bundleName = "org.scala-ide.sdt.debug.tests") {
+ @BeforeClass
+ def buildProject() {
+ project.underlying.build(IncrementalProjectBuilder.CLEAN_BUILD, null)
+ project.underlying.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, null)
+ }
+}
+
+class MethodClassifierUnitTest {
+
+ @Test
+ def cp_testConcreteClassExtendsTrait() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/ConcreteClass.class")
+ val pool = ConstantPool.fromFile(resource.getLocation().toFile())
@dotta Owner
dotta added a note

What are the tests checking? That the ConstantPool can parse the binary without throwing exception? Is the pool local value useful?

@dragos Owner
dragos added a note

They check that there are no crashes. Yeap, I can remove the pool value.

@dotta Owner
dotta added a note

I see. Would it make sense to add some assert using the pool? Or this is already covered by some other test?

@dragos Owner
dragos added a note

Not in this test, as long as there is no exception I am happy. The other tests will exercise methods on the pool when parsing full classfiles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dotta dotta commented on the diff
...ls/eclipse/debug/model/MethodClassifierUnitTest.scala
((94 lines not shown))
+ }
+
+ @Test
+ def testForwarder_pos_withDefault() {
+ assertForwarder("concreteTraitMethodWithDefault", true)
+ }
+
+ @Test
+ def testForwarder_neg_defaults() {
+ assertForwarder("abstractMethodWithDefault", false)
+ }
+
+ @Test
+ def testForwarder_neg_Java() {
+ // this is a call to a Java static method with the same name (but it's not inside an Impl class)
+ assertForwarder("console", false)
@dotta Owner
dotta added a note

IMO, it would be easier to read without having to pass the boolean argument. What do you think about having instead assertNoForwarder and assertHasForwarder

@dragos Owner
dragos added a note

I'm not sure the readability is that bad right now, and adding two more methods seems overkill.

@dotta Owner
dotta added a note

Fair enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dotta dotta commented on the diff
...ls/eclipse/debug/model/MethodClassifierUnitTest.scala
((175 lines not shown))
+ when(method.name()).thenReturn(name)
+ Assert.assertEquals("Default getter", expected, (new MethodClassifier).is(DefaultGetter, method))
+ }
+
+ def prepareGetterMock(name: String) = {
+ val method = mock(classOf[Method])
+ val refType = mock(classOf[ReferenceType])
+
+ when(method.name()).thenReturn(name)
+ when(method.declaringType()).thenReturn(refType)
+
+ when(refType.fieldByName(anyString())) thenAnswer { inv: InvocationOnMock =>
+ inv.getArguments()(0) match {
+ case name: String =>
+ val field = mock(classOf[Field])
+ parser.fields.get(name).map(_ => field).getOrElse(null)
@dotta Owner
dotta added a note

Let's make sure I understand what's happening here: you are returning a mocked field if a field with the passed name exists in parser.fields, null otherwise. Is that it?

@dragos Owner
dragos added a note

Exactly. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...cala/tools/eclipse/debug/classfile/ConstantPool.scala
((54 lines not shown))
+
+ bytes(idx) match {
+ case CONSTANT_Utf8 =>
+ idx += 3 + getU2(idx + 1)
+ case CONSTANT_Class | CONSTANT_String | CONSTANT_MethodType =>
+ idx += 3
+ case CONSTANT_MethodHandle =>
+ idx += 4
+ case CONSTANT_Fieldref | CONSTANT_Methodref | CONSTANT_InterfaceMethodref
+ | CONSTANT_NameAndType | CONSTANT_Integer | CONSTANT_Float | CONSTANT_InvokeDynamic =>
+ idx += 5
+ case CONSTANT_Long | CONSTANT_Double =>
+ idx += 9
+ i += 1 // long and double constants occupy *two* slots
+ case _ =>
+ assert(false, "Unknown entry at index %d: %d ".format(i, bytes(idx)))
@dotta Owner
dotta added a note

Considering assert can be elided, maybe it's better to throw a "real" exception, e.g., IllegalStateException.

@dragos Owner
dragos added a note

It's not an expected outcome, that should really not happen. I don't think we elide assertions even in compiler release builds.

I am not sure what to do: throwing an exception would not convey the underlying assumption of the code. On the other hand, maybe the calling code wants to be able to recover... hard choice.

@dotta Owner
dotta added a note

It's not an expected outcome, that should really not happen.

I know. And that's why you are using an assertion. I just think that an exception would be better in this case. And, I checked the doc for IllegalStateException, and figured it is not the right exception to throw in this case. I believe UnsupportedOperationException is the one.

I don't think we elide assertions even in compiler release builds.

What is true today may very well change tomorrow, and the less thing you need to remember the better.

I am not sure what to do: throwing an exception would not convey the underlying assumption of the code. On the other hand, maybe the calling code wants to be able to recover... hard choice.

What do you think of UnsupportedOperationException?

@dragos Owner
dragos added a note

I will throw a custom exception, I think it makes more sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...s/eclipse/debug/preferences/DebuggerPreferences.scala
@@ -29,6 +30,8 @@ Configured step filters:
addField(new BooleanFieldEditor(FILTER_SYNTHETIC, "Filter SYNTHETIC methods", parent))
addField(new BooleanFieldEditor(FILTER_GETTER, "Filter Scala getters", getFieldEditorParent))
addField(new BooleanFieldEditor(FILTER_SETTER, "Filter Scala setters", getFieldEditorParent))
+ addField(new BooleanFieldEditor(FILTER_DEFAULT_GETTER, "Filter default getters", getFieldEditorParent))
@dotta Owner
dotta added a note

I wonder if "Filter compiler generated getters" would be a better description.

@dragos Owner
dragos added a note

Default getters are getters for default parameters. Good point, I'll update the description accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dotta dotta commented on the diff
...cala/tools/eclipse/debug/model/MethodClassifier.scala
((12 lines not shown))
+/** Classifies `scalac` synthetic methods according to their purpose.
+ *
+ * May cache results, so do not reuse an instance of this class across debugging
+ * sessions (but *do* reuse it in the same session).
+ *
+ * TODO: Cache expensive operations (currently `Forwarder` is the most expensive).
+ */
+class MethodClassifier {
+
+ import MethodClassifier._
+
+ private val defaultR = """.*\$default\$\d+$""".r
+
+ /** Is the given method of the `kind` type? */
+ def is(kind: MethodClassifier.Value, method: Method): Boolean = {
+ kind match {
@dotta Owner
dotta added a note

Instead of using pattern matching, why not having proper (case) classes for the different MethodClassifiers. I find it would be easier to read.

@dragos Owner
dragos added a note

A method may have more than one classifier.. I am not proud of this code, but I spent some time thinking of better designs and didn't find something that I like. Any suggestions are welcome.

@dotta Owner
dotta added a note

The following is what I had in mind:

object MethodClassifier extends Enumeration {
  // This is defined in Enumeration but it cannot be accessed!?!
  private def nextNameOrNull = if (nextName != null && nextName.hasNext) nextName.next else null

  protected abstract class Classifier extends super.Val(nextId, nextNameOrNull) {
    def is(method: Method): Boolean
  }
  case object Synthetic extends Classifier {
    def is(method: Method): Boolean = method.isSynthetic()
  }
  case object Getter extends Classifier {
    def is(method: Method): Boolean = method.declaringType().fieldByName(method.name()) ne null
  }
  ...

  def allKindsOf(method: Method): Set[MethodClassifier.Value] = values.filter(_.is(method))
}

I know it's more verbose, but I like that it keeps data and behavior close, particularly for the Forwarder and DefaultGetter case.

@dragos Owner
dragos added a note

I like it (except the part where we depend on internals of Enumeration). Now what would be a better name for def is?

@dragos Owner
dragos added a note

Actually, it's not so good. Moving all the logic in the enumeration object makes it static and would prevent caching in the future (that is important for constant pool parsing in case of Forwarders)

@dragos Owner
dragos added a note

I'll add a comment about these alternatives and leave it as such for the moment. We can revisit this when we work more on step filters, like adding caching. Does that sound good?

@dotta Owner
dotta added a note

Yep.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dotta dotta commented on the diff
...cala/tools/eclipse/debug/classfile/ConstantPool.scala
((23 lines not shown))
+
+ // Taken from the JVM Spec
+ final val CONSTANT_Class = 7
+ final val CONSTANT_Fieldref = 9
+ final val CONSTANT_Methodref = 10
+ final val CONSTANT_InterfaceMethodref = 11
+ final val CONSTANT_String = 8
+ final val CONSTANT_Integer = 3
+ final val CONSTANT_Float = 4
+ final val CONSTANT_Long = 5
+ final val CONSTANT_Double = 6
+ final val CONSTANT_NameAndType = 12
+ final val CONSTANT_Utf8 = 1
+ final val CONSTANT_MethodHandle = 15
+ final val CONSTANT_MethodType = 16
+ final val CONSTANT_InvokeDynamic = 18
@dotta Owner
dotta added a note

Should be moved to the companion object.

@dragos Owner
dragos added a note

Good catch!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dragos dragos Filter out step into forwarder and default getter methods.
A general reorganization of step filters was necessary:

- made it a class instead of an object
- split functionality between the filter class and a method classifier
- split tests in unit tests (using the method classifier) and integration tests (testing the step-into
  inside a debugger instance)

A separate commit makes the Scala launch delegate behave according to the spec, throwing
`CoreException` on failure. Failure to do so was making tests hang indefinitely when a debug-test 
project had errors, and therefore could not be launched.

Fixed #1001288.
094a400
@dragos
Owner

I forced-push fixes for the comments you had. If the build goes through, I'll merge it.

@dragos
Owner

PLS REBUILD ALL

@dragos dragos merged commit cc81f37 into scala-ide:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 4, 2012
  1. @dragos

    Throw an exception if the launcher fails.

    dragos authored
    The `MainMethodVerifier` may stop the Scala launcher if there is no main type,
    or the project has errors. The Eclipse API requires that we throw `CoreException`,
    but we used to fail silently (leading to the debug tests hanging indefinitely).
Commits on Nov 6, 2012
  1. @dragos

    Filter out step into forwarder and default getter methods.

    dragos authored
    A general reorganization of step filters was necessary:
    
    - made it a class instead of an object
    - split functionality between the filter class and a method classifier
    - split tests in unit tests (using the method classifier) and integration tests (testing the step-into
      inside a debugger instance)
    
    A separate commit makes the Scala launch delegate behave according to the spec, throwing
    `CoreException` on failure. Failure to do so was making tests hang indefinitely when a debug-test 
    project had errors, and therefore could not be launched.
    
    Fixed #1001288.
This page is out of date. Refresh to see the latest.
Showing with 875 additions and 46 deletions.
  1. +11 −8 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/MainClassVerifier.scala
  2. +4 −2 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/ScalaLaunchDelegate.scala
  3. +34 −1 org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/ScalaDebugSteppingTest.scala
  4. +2 −1  org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/ScalaDebugTestSuite.scala
  5. +96 −0 org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/classfile/ClassfileParser.scala
  6. +210 −0 org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/model/MethodClassifierUnitTest.scala
  7. +7 −0 org.scala-ide.sdt.debug.tests/test-workspace/constant-pool/.classpath
  8. +18 −0 org.scala-ide.sdt.debug.tests/test-workspace/constant-pool/.project
  9. +66 −0 org.scala-ide.sdt.debug.tests/test-workspace/constant-pool/src/MethodClassifiers.scala
  10. +15 −0 org.scala-ide.sdt.debug.tests/test-workspace/debug/MethodClassifiers.launch
  11. +108 −0 org.scala-ide.sdt.debug.tests/test-workspace/debug/src/stepping/MethodClassifiers.scala
  12. +145 −0 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/classfile/ConstantPool.scala
  13. +2 −2 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/command/ScalaStepInto.scala
  14. +9 −9 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/command/ScalaStepOver.scala
  15. +1 −1  org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/command/ScalaStepReturn.scala
  16. +126 −0 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/model/MethodClassifier.scala
  17. +2 −0  org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/model/ScalaDebugTarget.scala
  18. +6 −17 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/model/StepFilters.scala
  19. +13 −5 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/preferences/DebuggerPreferences.scala
View
19 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/MainClassVerifier.scala
@@ -1,6 +1,9 @@
package scala.tools.eclipse.launching
import scala.tools.eclipse.ScalaProject
+import org.eclipse.core.runtime.IStatus
+import org.eclipse.core.runtime.Status
+import scala.tools.eclipse.ScalaPlugin
object MainClassVerifier {
trait ErrorReporter {
@@ -14,11 +17,10 @@ class MainClassVerifier(reporter: MainClassVerifier.type#ErrorReporter) {
* @project The scala project containing the main type.
* @typeName The fully-qualified main type name.
*/
- def execute(project: ScalaProject, mainTypeName: String): Boolean = {
+ def execute(project: ScalaProject, mainTypeName: String): IStatus = {
// 1. No binaries are produced if the project contains compilation errors.
if (project.buildManager.hasErrors) {
- projectHasBuildErrors(project.underlying.getName)
- return false
+ return projectHasBuildErrors(project.underlying.getName)
}
// Try to locate the type
@@ -27,23 +29,24 @@ class MainClassVerifier(reporter: MainClassVerifier.type#ErrorReporter) {
// 2. The main type won't be found if the provided ``mainTypeName`` (fully-qualified name) doesn't
// reference an existing type. (this is a workaround for #1000541).
if (element == null) {
- mainTypeCannotBeLocated(project.underlying.getName, mainTypeName)
- return false
+ return mainTypeCannotBeLocated(project.underlying.getName, mainTypeName)
}
- true
+ new Status(IStatus.OK, ScalaPlugin.plugin.pluginId, "")
}
- private def projectHasBuildErrors(projectName: String): Unit = {
+ private def projectHasBuildErrors(projectName: String): IStatus = {
val errMsg = "Project '%s' contains compilation errors (therefore, no binaries have been produced).".format(projectName)
reporter.report(errMsg)
+ new Status(IStatus.ERROR, ScalaPlugin.plugin.pluginId, errMsg)
}
- private def mainTypeCannotBeLocated(projectName: String, mainTypeName: String): Unit = {
+ private def mainTypeCannotBeLocated(projectName: String, mainTypeName: String): IStatus = {
val errMsg = ("Cannot locate main type '%s' in project '%s'. For this to work, the package name in " +
"the source needs to match the source's physical location.\n" +
"Hint: Move the source file containing the main type '%s' in folder '%s'."
).format(mainTypeName, projectName, mainTypeName, mainTypeName.split('.').init.mkString("/"))
reporter.report(errMsg)
+ new Status(IStatus.ERROR, ScalaPlugin.plugin.pluginId, errMsg)
}
}
View
6 org.scala-ide.sdt.core/src/scala/tools/eclipse/launching/ScalaLaunchDelegate.scala
@@ -15,6 +15,8 @@ import org.eclipse.jface.dialogs.MessageDialog
import scala.tools.eclipse.util.SWTUtils
import org.eclipse.core.runtime.IPath
import org.eclipse.jdt.core.IJavaProject
+import org.eclipse.core.runtime.Status
+import org.eclipse.core.runtime.IStatus
class ScalaLaunchDelegate extends AbstractJavaLaunchConfigurationDelegate {
def launch(configuration: ILaunchConfiguration, mode: String, launch: ILaunch, monitor0: IProgressMonitor) {
@@ -78,8 +80,8 @@ class ScalaLaunchDelegate extends AbstractJavaLaunchConfigurationDelegate {
val project = getJavaProject(configuration)
ScalaPlugin.plugin.asScalaProject(project.getProject).foreach { scalaProject =>
val mainClassVerifier = new MainClassVerifier(new UIErrorReporter)
- val mainClassFileOk = mainClassVerifier.execute(scalaProject, mainTypeName)
- if(!mainClassFileOk) return
+ val status = mainClassVerifier.execute(scalaProject, mainTypeName)
+ if (status.getCode() != IStatus.OK) throw new CoreException(status)
}
// done the verification phase
View
35 org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/ScalaDebugSteppingTest.scala
@@ -574,4 +574,37 @@ class ScalaDebugSteppingTest {
session.stepReturn()
}
-}
+
+ @Test
+ def StepIntoSkipsDefaultArgs() {
+ session = initDebugSession("MethodClassifiers")
+
+ session.runToLine("stepping.MethodClassifiers", 60)
+ session.stepInto()
+ session.checkStackFrame("stepping.Defaults", "methWithDefaults(Ljava/lang/String;)V", 6)
+ }
+
+ @Test
+ def StepIntoSkipsForwarder {
+ session = initDebugSession("MethodClassifiers")
+ session.runToLine("stepping.MethodClassifiers", 64)
+ session.stepInto
+ session.checkStackFrame("stepping.BaseTrait$class", "concreteTraitMethod1(Lstepping/BaseTrait;I)I", 12)
+ }
+
+ @Test
+ def StepIntoSkipsForwarderWithParams {
+ session = initDebugSession("MethodClassifiers")
+ session.runToLine("stepping.MethodClassifiers", 67)
+ session.stepInto
+ session.checkStackFrame("stepping.BaseTrait$class", "concreteTraitMethod4(Lstepping/BaseTrait;IDLjava/lang/String;Ljava/lang/Object;)V", 15)
+ }
+
+ @Test
+ def StepIntoSkipsForwarderWith22Params {
+ session = initDebugSession("MethodClassifiers")
+ session.runToLine("stepping.MethodClassifiers", 69)
+ session.stepInto
+ session.checkStackFrame("stepping.MaxArgs$class", "manyArgs(Lstepping/MaxArgs;DDDDDDDDDDDDDDDDDDDDDD)D", 105)
+ }
+}
View
3  org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/ScalaDebugTestSuite.scala
@@ -10,14 +10,15 @@ import scala.tools.eclipse.debug.model.ScalaValueTest
import scala.tools.eclipse.debug.model.ScalaDebugTargetTest
import scala.tools.eclipse.launching.LibraryJarInBootstrapTest
import scala.tools.eclipse.debug.model.DebugTargetTerminationTest
+import scala.tools.eclipse.debug.model.MethodClassifierUnitTest
/**
* Junit test suite for the Scala debugger.
*/
-
@RunWith(classOf[Suite])
@Suite.SuiteClasses(
Array(
+ classOf[MethodClassifierUnitTest],
classOf[ScalaDebugSteppingTest],
classOf[ScalaDebugResumeTest],
classOf[ScalaThreadTest],
View
96 org.scala-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/classfile/ClassfileParser.scala
@@ -0,0 +1,96 @@
+package scala.tools.eclipse.debug.classfile
+
+import java.io.File
+import scala.tools.nsc.io.AbstractFile
+import scala.tools.nsc.symtab.classfile.AbstractFileReader
+
+/** A one-off classfile parser used for testing.
+ *
+ * Once this class is initialized, it can return the constant pool, methods and field
+ * information. Used for testing only.
+ */
+class ClassfileParser(classFile: File) {
+ val file = AbstractFile.getFile(classFile)
+ val reader = new AbstractFileReader(file)
+
+ def parseHeader() {
+ if (!(reader.nextInt == 0xCAFEBABE))
+ throw new IllegalArgumentException("Invalid classfile: " + classFile.getName())
+ }
+
+ parseHeader()
+ reader.skip(4) // major/minor version
+
+ val count = reader.nextChar
+ private val poolStartIndex = reader.bp
+
+ val pool = new ConstantPool(reader.buf.slice(reader.bp, reader.buf.length), count)
+ reader.skip(pool.lastPoolIndex)
+
+ private val poolEndIndex = reader.bp
+
+ def constantPoolBytes: Array[Byte] = reader.buf.slice(poolStartIndex, poolEndIndex)
+
+ reader.skip(2) // access flags
+
+ val name: String = pool.getClassRef(reader.nextChar)
+ val superClass: String = pool.getClassRef(reader.nextChar)
+ val interfaces: Seq[String] = parseInterfaces()
+ val fields: Map[String, String] = parseFields()
+ val methods: Map[String, MethodDef] = parseMethods()
+
+ private def parseInterfaces(): Seq[String] = {
+ val count = reader.nextChar
+ for (i <- 0 until count) yield pool.getClassRef(reader.nextChar)
+ }
+
+ private def parseFields(): Map[String, String] = {
+ val count = reader.nextChar
+ (for (i <- 0 until count) yield {
+ reader.skip(2) // access flags dropped
+ val name = pool.getString(reader.nextChar)
+ val descr = pool.getString(reader.nextChar)
+ skipAttributes()
+ name -> descr
+ }) toMap
+ }
+
+ private def skipAttributes() {
+ val attrCount = reader.nextChar
+ for (i <- 0 until attrCount) {
+ reader.skip(2); reader.skip(reader.nextInt)
+ }
+ }
+
+ private def parseCodeAttribute(): Array[Byte] = {
+ val attrCount = reader.nextChar
+ var bytecode: Array[Byte] = null
+ for (i <- 0 until attrCount) yield {
+ val name = pool.getString(reader.nextChar)
+ val attrLen = reader.nextInt
+ val afterAttr = reader.bp + attrLen
+ if (name == "Code") {
+ reader.skip(2) // max_stack
+ reader.skip(2) // max_locals
+ val codeLen = reader.nextInt
+ bytecode = reader.nextBytes(codeLen)
+ // other things may follow, like exception handlers
+ }
+ reader.bp = afterAttr
+ }
+ bytecode
+ }
+
+ private def parseMethods(): Map[String, MethodDef] = {
+ val count = reader.nextChar
+ (for (i <- 0 until count) yield {
+ reader.skip(2) // access flags dropped
+ val name = pool.getString(reader.nextChar)
+ val descr = pool.getString(reader.nextChar)
+
+ name -> MethodDef(name, descr, parseCodeAttribute)
+ }) toMap
+ }
+}
+
+case class MethodDef(name: String, descriptor: String, bytecode: Array[Byte])
View
210 ...la-ide.sdt.debug.tests/src/scala/tools/eclipse/debug/model/MethodClassifierUnitTest.scala
@@ -0,0 +1,210 @@
+package scala.tools.eclipse.debug.model
+
+import org.eclipse.core.resources.IncrementalProjectBuilder
+import org.eclipse.core.runtime.IPath
+import org.eclipse.core.runtime.Path
+import org.junit.Assert
+import org.junit.BeforeClass
+import org.junit.Test
+import org.mockito.Matchers.anyString
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.when
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+
+import com.sun.jdi.Field
+import com.sun.jdi.Method
+import com.sun.jdi.ReferenceType
+
+import scala.tools.eclipse.debug.classfile.ClassfileParser
+import scala.tools.eclipse.debug.classfile.ConstantPool
+import scala.tools.eclipse.debug.model.MethodClassifier.DefaultGetter
+import scala.tools.eclipse.debug.model.MethodClassifier.Getter
+import scala.tools.eclipse.debug.model.MethodClassifier.Setter
+import scala.tools.eclipse.testsetup.SDTTestUtils
+import scala.tools.eclipse.testsetup.TestProjectSetup
+
+object MethodClassifierUnitTest extends TestProjectSetup("constant-pool", bundleName = "org.scala-ide.sdt.debug.tests") {
+ @BeforeClass
+ def buildProject() {
+ project.underlying.build(IncrementalProjectBuilder.CLEAN_BUILD, null)
+ project.underlying.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, null)
+ }
+}
+
+class MethodClassifierUnitTest {
+
+ @Test
+ def cp_testConcreteClassExtendsTrait() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/ConcreteClass.class")
+ ConstantPool.fromFile(resource.getLocation().toFile())
+ }
+
+ @Test
+ def cp_testDefaultsClass() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/Defaults.class")
+ ConstantPool.fromFile(resource.getLocation().toFile())
+ }
+
+ @Test
+ def cp_testDefaultsObjectClass() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/Defaults$.class")
+ ConstantPool.fromFile(resource.getLocation().toFile())
+ }
+
+ @Test
+ def cp_testBaseTrait() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/BaseTrait.class")
+ ConstantPool.fromFile(resource.getLocation().toFile())
+ }
+
+ @Test
+ def cp_testImplClass() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/BaseTrait$class.class")
+ ConstantPool.fromFile(resource.getLocation().toFile())
+ }
+
+ @Test
+ def cp_testMethodClassifier() {
+ val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/MethodClassifiers.class")
+ ConstantPool.fromFile(resource.getLocation().toFile())
+ }
+
+ lazy val resource = SDTTestUtils.workspace.getRoot().getFile("/constant-pool/bin/stepping/ConcreteClass.class")
+ lazy val parser = new ClassfileParser(resource.getLocation().toFile())
+
+ @Test
+ def testForwarder_pos_1() {
+ assertForwarder("concreteTraitMethod1", true)
+ }
+
+ @Test
+ def testForwarder_pos_2() {
+ assertForwarder("concreteTraitMethod2", true)
+ }
+
+ @Test
+ def testForwarder_pos_3() {
+ assertForwarder("concreteTraitMethod3", true)
+ }
+
+ @Test
+ def testForwarder_pos_4() {
+ assertForwarder("concreteTraitMethod4", true)
+ }
+
+ @Test
+ def testForwarder_pos_withDefault() {
+ assertForwarder("concreteTraitMethodWithDefault", true)
+ }
+
+ @Test
+ def testForwarder_neg_defaults() {
+ assertForwarder("abstractMethodWithDefault", false)
+ }
+
+ @Test
+ def testForwarder_neg_Java() {
+ // this is a call to a Java static method with the same name (but it's not inside an Impl class)
+ assertForwarder("console", false)
@dotta Owner
dotta added a note

IMO, it would be easier to read without having to pass the boolean argument. What do you think about having instead assertNoForwarder and assertHasForwarder

@dragos Owner
dragos added a note

I'm not sure the readability is that bad right now, and adding two more methods seems overkill.

@dotta Owner
dotta added a note

Fair enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+
+ @Test
+ def testDefaults_pos_1() {
+ assertDefaultGetter("init$default$1", true)
+ }
+
+ @Test
+ def testDefaults_pos_2() {
+ assertDefaultGetter("methWithDefaults2$default$4", true)
+ }
+
+ @Test
+ def testDefaults_pos_3() {
+ assertDefaultGetter("methWithDefaults2$default$2", true)
+ }
+
+ @Test
+ def testGetter_pos_concrete() {
+ assertGetter("concreteField1", true)
+ }
+
+ @Test
+ def testGetter_pos_concrete_mutable() {
+ assertGetter("concreteMField1", true)
+ }
+
+ @Test
+ def testGetter_pos_abs() {
+ assertGetter("abstractField1", true)
+ }
+
+ @Test
+ def testGetter_pos_abs_mutable() {
+ assertGetter("abstractMField1", true)
+ }
+
+ @Test
+ def testSetter_pos_concrete() {
+ assertSetter("concreteMField1_$eq", true)
+ }
+
+ @Test
+ def testSetter_pos_abs() {
+ assertSetter("abstractMField1_$eq", true)
+ }
+
+ @Test
+ def testGetter_nonPrivate() {
+ assertGetter("stepping$ConcreteClass$$fakePrivate", true)
+ }
+
+ @Test
+ def testSetter_nonPrivate() {
+ assertSetter("stepping$ConcreteClass$$fakePrivate_$eq", true)
+ }
+
+ def assertForwarder(method: String, forwarder: Boolean) {
+ val classifier = new MethodClassifier
+ Assert.assertEquals("Forwarder test failed", forwarder, classifier.isForwarderBytecode(parser.methods(method).bytecode, parser.constantPoolBytes, parser.pool.size, method))
+ }
+
+ def assertDefaultGetter(name: String, expected: Boolean) {
+ val method = mock(classOf[Method])
+
+ when(method.name()).thenReturn(name)
+ Assert.assertEquals("Default getter", expected, (new MethodClassifier).is(DefaultGetter, method))
+ }
+
+ def prepareGetterMock(name: String) = {
+ val method = mock(classOf[Method])
+ val refType = mock(classOf[ReferenceType])
+
+ when(method.name()).thenReturn(name)
+ when(method.declaringType()).thenReturn(refType)
+
+ when(refType.fieldByName(anyString())) thenAnswer { inv: InvocationOnMock =>
+ inv.getArguments()(0) match {
+ case name: String =>
+ val field = mock(classOf[Field])
+ parser.fields.get(name).map(_ => field).getOrElse(null)
@dotta Owner
dotta added a note

Let's make sure I understand what's happening here: you are returning a mocked field if a field with the passed name exists in parser.fields, null otherwise. Is that it?

@dragos Owner
dragos added a note

Exactly. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+ }
+ method
+ }
+
+ def assertGetter(name: String, expected: Boolean) {
+ Assert.assertEquals("Getter", expected, (new MethodClassifier).is(Getter, prepareGetterMock(name)))
+ }
+
+ def assertSetter(name: String, expected: Boolean) {
+ Assert.assertEquals("Setter", expected, (new MethodClassifier).is(Setter, prepareGetterMock(name)))
+ }
+
+ // implicits
+ implicit def toPath(str: String): IPath = new Path(str)
+
+ implicit def toAnswer[A](f: InvocationOnMock => A): Answer[A] = new Answer[A] {
+ def answer(inv: InvocationOnMock): A = f(inv)
+ }
+}
View
7 org.scala-ide.sdt.debug.tests/test-workspace/constant-pool/.classpath
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
View
18 org.scala-ide.sdt.debug.tests/test-workspace/constant-pool/.project
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>constant-pool</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.scala-ide.sdt.core.scalabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.scala-ide.sdt.core.scalanature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
View
66 org.scala-ide.sdt.debug.tests/test-workspace/constant-pool/src/MethodClassifiers.scala
@@ -0,0 +1,66 @@
+package stepping
+
+class Defaults(someArg: String = "a default String") {
+
+ def methWithDefaults(someArg1: String = "another default String") = {}
+ def methWithDefaults2(arg1: String, arg2: Int = 42)(barg1: String, barg2: Int = arg2 + 1) = {}
+}
+
+trait BaseTrait {
+ def concreteTraitMethod1(x: Int) = x
+ def concreteTraitMethod2(x: Int, y: Int): Boolean = false
+ def concreteTraitMethod3(x: Int, y: Long, z: String): Long = y
+ def concreteTraitMethod4(x: Int, y: Double, z: String, t: Object): Unit = ()
+
+ private def private_concreteTraitMethod1(x: Int) = x
+ private def private_concreteTraitMethod2(x: Int, y: Int): Boolean = false
+ private def private_concreteTraitMethod3(x: Int, y: Long, z: String): Long = y
+ private def private_concreteTraitMethod4(x: Int, y: Double, z: String, t: Object): Unit = ()
+
+
+ def concreteTraitMethodWithDefault(someArg2: String = "yet another default String") = {
+ someArg2
+ }
+
+ def abstractMethodWithDefault(someArg3: String = "last default String")
+
+ val concreteField1: Int = 42
+ val abstractField1: String
+ var concreteMField1: Int = 20
+ var abstractMField1: String
+
+}
+
+class ConcreteClass extends BaseTrait {
+ def abstractMethodWithDefault(someArg3: String) = ""
+
+ // static call to a Java method, but not a forwarder!
+ def console() {
+ System.console()
+ }
+
+ val abstractField1: String = "f1"
+ var abstractMField1: String = "f2"
+
+ private var fakePrivate: String = "fakePrivate"
+
+ class Inner {
+ fakePrivate
+ }
+}
+
+
+class MethodClassifiers {
+
+ def mainTest() {
+ val d = new Defaults()
+
+ d.methWithDefaults()
+
+ val c = new ConcreteClass
+
+ c.concreteTraitMethod1(42)
+ c.concreteTraitMethodWithDefault()
+ c.abstractMethodWithDefault()
+ }
+}
View
15 org.scala-ide.sdt.debug.tests/test-workspace/debug/MethodClassifiers.launch
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="scala.application">
+<stringAttribute key="bad_container_name" value="/debug/f"/>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/stepping/src/stepping/MethodClassifiers.scala"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<mapAttribute key="org.eclipse.debug.core.preferred_launchers">
+<mapEntry key="[debug]" value="scala.application.new"/>
+</mapAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="stepping.MethodClassifiers"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="debug"/>
+</launchConfiguration>
View
108 org.scala-ide.sdt.debug.tests/test-workspace/debug/src/stepping/MethodClassifiers.scala
@@ -0,0 +1,108 @@
+package stepping
+
+class Defaults(someArg: String = "a default String") {
+
+ def methWithDefaults(someArg1: String = "another default String") = {
+ println()
+ }
+ def methWithDefaults2(arg1: String, arg2: Int = 42)(barg1: String, barg2: Int = arg2 + 1) = {}
+}
+
+trait BaseTrait {
+ def concreteTraitMethod1(x: Int) = x
+ def concreteTraitMethod2(x: Int, y: Int): Boolean = false
+ def concreteTraitMethod3(x: Int, y: Long, z: String): Long = y
+ def concreteTraitMethod4(x: Int, y: Double, z: String, t: Object): Unit = ()
+
+ private def private_concreteTraitMethod1(x: Int) = x
+ private def private_concreteTraitMethod2(x: Int, y: Int): Boolean = false
+ private def private_concreteTraitMethod3(x: Int, y: Long, z: String): Long = y
+ private def private_concreteTraitMethod4(x: Int, y: Double, z: String, t: Object): Unit = ()
+
+
+ def concreteTraitMethodWithDefault(someArg2: String = "yet another default String") = {
+ someArg2
+ }
+
+ def abstractMethodWithDefault(someArg3: String = "last default String")
+
+ val concreteField1: Int = 42
+ val abstractField1: String
+ var concreteMField1: Int = 20
+ var abstractMField1: String
+
+}
+
+class ConcreteClass extends BaseTrait {
+ def abstractMethodWithDefault(someArg3: String) = ""
+
+ // static call to a Java method, but not a forwarder!
+ def console() {
+ System.console()
+ }
+
+ val abstractField1: String = "f1"
+ var abstractMField1: String = "f2"
+
+ private var fakePrivate: String = "fakePrivate"
+
+ class Inner {
+ fakePrivate
+ }
+}
+
+
+class MethodClassifiers {
+
+ def mainTest() {
+ val d = new Defaults()
+
+ d.methWithDefaults()
+
+ val c = new ConcreteClass
+
+ c.concreteTraitMethod1(42)
+ c.concreteTraitMethodWithDefault()
+ c.abstractMethodWithDefault()
+ c.concreteTraitMethod4(42, 42.0, "42", new Object)
+ val maxArgs = new MaxArgsC
+ maxArgs.manyArgs(42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0)
+ }
+}
+
+object MethodClassifiers {
+
+ def main(args: Array[String]) {
+ new MethodClassifiers().mainTest()
+ }
+
+}
+
+trait MaxArgs {
+ def manyArgs(
+ x1: Double,
+ x2: Double,
+ x3: Double,
+ x4: Double,
+ x5: Double,
+ x6: Double,
+ x7: Double,
+ x8: Double,
+ x9: Double,
+ x10: Double,
+ x11: Double,
+ x12: Double,
+ x13: Double,
+ x14: Double,
+ x15: Double,
+ x16: Double,
+ x17: Double,
+ x18: Double,
+ x19: Double,
+ x20: Double,
+ x21: Double,
+ x22: Double
+ ) = x1
+}
+
+class MaxArgsC extends MaxArgs
View
145 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/classfile/ConstantPool.scala
@@ -0,0 +1,145 @@
+package scala.tools.eclipse.debug.classfile
+
+import java.io.File
+import scala.tools.nsc.io.AbstractFile
+import scala.tools.nsc.symtab.classfile.AbstractFileReader
+
+/** A JVM constant pool. `bytes` are parsed when the object is instantiated,
+ * and then it's ready to retrieve values from the pool.
+ *
+ * Because of the way in which the classfile format is defined, there is
+ * no way to use the pool without parsing it first (each entry may have a
+ * different length, so there is no way to compute the offset of an entry
+ * based on its index).
+ *
+ * @param bytes The bytes constituting the constant pool
+ * @param length The number of constant pool entries. Any bytes not included in `length`
+ * are ignored
+ *
+ * @see http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
+ */
+class ConstantPool(bytes: Array[Byte], length: Int) {
+ import ConstantPool._
+
+ private val starts = new Array[Int](length)
+ private var cpLastIndex = -1
+
+ /** Return the number of entries in this constant pool */
+ def size: Int = length
+
+ // populate the constant pool
+ init()
+
+ def init() {
+ var i = 1 // according to the JVM Spec, the constant pool index starts at 1!
+ var idx = 0
+ while (i < length) {
+ starts(i) = idx
+ i += 1
+
+ bytes(idx) match {
+ case CONSTANT_Utf8 =>
+ idx += 3 + getU2(idx + 1)
+ case CONSTANT_Class | CONSTANT_String | CONSTANT_MethodType =>
+ idx += 3
+ case CONSTANT_MethodHandle =>
+ idx += 4
+ case CONSTANT_Fieldref | CONSTANT_Methodref | CONSTANT_InterfaceMethodref
+ | CONSTANT_NameAndType | CONSTANT_Integer | CONSTANT_Float | CONSTANT_InvokeDynamic =>
+ idx += 5
+ case CONSTANT_Long | CONSTANT_Double =>
+ idx += 9
+ i += 1 // long and double constants occupy *two* slots
+ case _ =>
+ throw IllegalConstantPoolEntry(i, bytes(idx), idx)
+ }
+ }
+ cpLastIndex = idx
+ }
+
+ /** The index in `bytes` of the last byte in the constant pool. */
+ def lastPoolIndex: Int = cpLastIndex
+
+ /** Return a String from the constant pool. */
+ def getString(i: Int): String = {
+ val idx = starts(i)
+ assert(bytes(idx) == CONSTANT_Utf8, bytes(idx))
+
+ new String(bytes, idx + 3, getU2(idx + 1))
+ }
+
+ /** Return a method reference from the constant pool. */
+ def getMethodRef(i: Int): MethodRef = {
+ val idx = starts(i)
+ assert(bytes(idx) == CONSTANT_Methodref || bytes(idx) == CONSTANT_InterfaceMethodref, bytes(idx))
+
+ val clsName = getClassRef(getU2(idx + 1))
+ val (method, signature) = getNameAndType(getU2(idx + 3))
+
+ MethodRef(clsName, method, signature)
+ }
+
+ /** Return a class reference from the constant pool. */
+ def getClassRef(i: Int): String = {
+ val idx = starts(i)
+ assert(bytes(idx) == CONSTANT_Class, bytes(idx))
+
+ getString(getU2(idx + 1))
+ }
+
+ /** Return a name and a type from the constant pool. */
+ def getNameAndType(i: Int): (String, String) = {
+ val idx = starts(i)
+ assert(bytes(idx) == CONSTANT_NameAndType, bytes(idx))
+
+ (getString(getU2(idx + 1)), getString(getU2(idx + 3)))
+ }
+
+ /** Return an unsigned 1-byte value as an Int. */
+ private def getU1(idx: Int): Int = {
+ bytes(idx) & 0xFF
+ }
+
+ /** Return an unsigned 2-byte value as a Char (they are unsigned on the JVM). */
+ private def getU2(idx: Int): Char =
+ ((getU1(idx) << 8) + getU1(idx + 1)).toChar
+}
+
+object ConstantPool {
+ case class MethodRef(className: String, methodName: String, signature: String)
+
+ case class IllegalConstantPoolEntry(idx: Int, value: Int, offset: Int)
+ extends RuntimeException("Illegal constant pool entry at index %d, value %d, offset %d".format(idx, value, offset))
+
+ // Taken from the JVM Spec
+ final val CONSTANT_Class = 7
+ final val CONSTANT_Fieldref = 9
+ final val CONSTANT_Methodref = 10
+ final val CONSTANT_InterfaceMethodref = 11
+ final val CONSTANT_String = 8
+ final val CONSTANT_Integer = 3
+ final val CONSTANT_Float = 4
+ final val CONSTANT_Long = 5
+ final val CONSTANT_Double = 6
+ final val CONSTANT_NameAndType = 12
+ final val CONSTANT_Utf8 = 1
+ final val CONSTANT_MethodHandle = 15
+ final val CONSTANT_MethodType = 16
+ final val CONSTANT_InvokeDynamic = 18
@dotta Owner
dotta added a note

Should be moved to the companion object.

@dragos Owner
dragos added a note

Good catch!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ /** Get the constant pool out of a Java class file. */
+ def fromFile(classFile: File): ConstantPool = {
+ val file = AbstractFile.getFile(classFile)
+ val reader = new AbstractFileReader(file)
+
+ def parseHeader() {
+ if (!(reader.nextInt == 0xCAFEBABE))
+ throw new IllegalArgumentException("Invalid classfile: " + classFile.getName())
+ }
+
+ parseHeader()
+ reader.skip(4) // major/minor version
+ val count = reader.nextChar
+ new ConstantPool(reader.buf.slice(reader.bp, reader.buf.length), count)
+ }
+}
View
4 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/command/ScalaStepInto.scala
@@ -64,14 +64,14 @@ private[command] abstract class ScalaStepIntoActor(debugTarget: ScalaDebugTarget
case stepEvent: StepEvent =>
reply(stepEvent.request.asInstanceOf[StepRequest].depth match {
case StepRequest.STEP_INTO =>
- if (StepFilters.isOpaqueLocation(stepEvent.location)) {
+ if (debugTarget.stepFilters.isOpaqueLocation(stepEvent.location)) {
// don't step deeper into constructor from 'hidden' entities
stepOutStackDepth = stepEvent.thread.frameCount
stepIntoRequest.disable()
stepOutRequest.enable()
false
} else {
- if (!StepFilters.isTransparentLocation(stepEvent.location) && stepEvent.location.lineNumber != stackLine) {
+ if (!debugTarget.stepFilters.isTransparentLocation(stepEvent.location) && stepEvent.location.lineNumber != stackLine) {
dispose()
thread.suspendedFromScala(DebugEvent.STEP_INTO)
true
View
18 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/command/ScalaStepOver.scala
@@ -44,11 +44,11 @@ object ScalaStepOver {
val range = Range(location.lineNumber, (location.method.declaringType.methods.asScala.flatten(methodToLines(_)).filter(_ > currentMethodLastLine) :+ Int.MaxValue).min)
// TODO: nestedTypes triggers a AllClasses request to the VM. Having the list of nested types managed and cached by the debug target should be more effective.
- val loadedAnonFunctionsInRange = location.method.declaringType.nestedTypes.asScala.flatMap(StepFilters.anonFunctionsInRange(_, range))
+ val loadedAnonFunctionsInRange = location.method.declaringType.nestedTypes.asScala.flatMap(scalaStackFrame.getDebugTarget.stepFilters.anonFunctionsInRange(_, range))
// if we are in an anonymous function, add the method
if (location.declaringType.name.contains("$$anonfun$")) {
- loadedAnonFunctionsInRange ++= StepFilters.findAnonFunction(location.declaringType)
+ loadedAnonFunctionsInRange ++= scalaStackFrame.getDebugTarget.stepFilters.findAnonFunction(location.declaringType)
}
requests ++= loadedAnonFunctionsInRange.map(JdiRequestFactory.createMethodEntryBreakpoint(_, scalaStackFrame.thread))
@@ -74,7 +74,7 @@ private class ScalaStepOver private (eventActor: ScalaStepOverActor) extends Bas
* Actor used to manage a Scala step over. It keeps track of the request needed to perform this step.
* This class is thread safe. Instances are not to be created outside of the ScalaStepOver object.
*/
-private[command] abstract class ScalaStepOverActor(target: ScalaDebugTarget, range: Range, thread: ScalaThread, requests: ListBuffer[EventRequest]) extends BaseDebuggerActor {
+private[command] abstract class ScalaStepOverActor(debugTarget: ScalaDebugTarget, range: Range, thread: ScalaThread, requests: ListBuffer[EventRequest]) extends BaseDebuggerActor {
protected[command] def scalaStep: ScalaStepOver
@@ -83,16 +83,16 @@ private[command] abstract class ScalaStepOverActor(target: ScalaDebugTarget, ran
override protected def behavior = {
// JDI event triggered when a class has been loaded
case classPrepareEvent: ClassPrepareEvent =>
- StepFilters.anonFunctionsInRange(classPrepareEvent.referenceType, range).foreach(method => {
+ debugTarget.stepFilters.anonFunctionsInRange(classPrepareEvent.referenceType, range).foreach(method => {
val breakpoint = JdiRequestFactory.createMethodEntryBreakpoint(method, thread)
requests += breakpoint
- target.eventDispatcher.setActorFor(this, breakpoint)
+ debugTarget.eventDispatcher.setActorFor(this, breakpoint)
breakpoint.enable()
})
reply(false)
// JDI event triggered when a step has been performed
case stepEvent: StepEvent =>
- reply(if (!StepFilters.isTransparentLocation(stepEvent.location)) {
+ reply(if (!debugTarget.stepFilters.isTransparentLocation(stepEvent.location)) {
dispose()
thread.suspendedFromScala(DebugEvent.STEP_OVER)
true
@@ -113,7 +113,7 @@ private[command] abstract class ScalaStepOverActor(target: ScalaDebugTarget, ran
}
private def step() {
- val eventDispatcher = target.eventDispatcher
+ val eventDispatcher = debugTarget.eventDispatcher
requests.foreach {
request =>
@@ -127,8 +127,8 @@ private[command] abstract class ScalaStepOverActor(target: ScalaDebugTarget, ran
private def dispose(): Unit = {
poison()
unlink(thread.eventActor)
- val eventDispatcher = target.eventDispatcher
- val eventRequestManager = target.virtualMachine.eventRequestManager
+ val eventDispatcher = debugTarget.eventDispatcher
+ val eventRequestManager = debugTarget.virtualMachine.eventRequestManager
for(request <- requests) {
request.disable()
View
2  org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/command/ScalaStepReturn.scala
@@ -45,7 +45,7 @@ private[command] abstract class ScalaStepReturnActor(debugTarget: ScalaDebugTarg
// JDI event triggered when a step has been performed
case stepEvent: StepEvent =>
reply {
- if (!StepFilters.isTransparentLocation(stepEvent.location)) {
+ if (!debugTarget.stepFilters.isTransparentLocation(stepEvent.location)) {
dispose()
thread.suspendedFromScala(DebugEvent.STEP_RETURN)
true
View
126 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/model/MethodClassifier.scala
@@ -0,0 +1,126 @@
+package scala.tools.eclipse.debug.model
+
+import com.sun.jdi.Method
+import scala.tools.eclipse.debug.classfile.ConstantPool
+import scala.tools.eclipse.util.Utils._
+
+object MethodClassifier extends Enumeration {
+ // more classifications may be added, but for the moment these are the ones that make sense in the debugger
+ val Synthetic, Getter, Setter, DefaultGetter, Bridge, TraitConstructor, Forwarder /*, DelayedInit, LazyCompute, LiftedException */ = Value
+}
+
+/** Classifies `scalac` synthetic methods according to their purpose.
+ *
+ * May cache results, so do not reuse an instance of this class across debugging
+ * sessions (but *do* reuse it in the same session).
+ *
+ * Note: Alternative designs have been explored. Making each flag in the enumeration
+ * a proper class does not play out because it would move detection strategies
+ * in the companion object, preventing caching (like the constant pool)
+ *
+ * TODO: Cache expensive operations (currently `Forwarder` is the most expensive).
+ */
+class MethodClassifier {
+
+ import MethodClassifier._
+
+ private val defaultR = """.*\$default\$\d+$""".r
+
+ /** Is the given method of the `kind` type? */
+ def is(kind: MethodClassifier.Value, method: Method): Boolean = {
+ kind match {
@dotta Owner
dotta added a note

Instead of using pattern matching, why not having proper (case) classes for the different MethodClassifiers. I find it would be easier to read.

@dragos Owner
dragos added a note

A method may have more than one classifier.. I am not proud of this code, but I spent some time thinking of better designs and didn't find something that I like. Any suggestions are welcome.

@dotta Owner
dotta added a note

The following is what I had in mind:

object MethodClassifier extends Enumeration {
  // This is defined in Enumeration but it cannot be accessed!?!
  private def nextNameOrNull = if (nextName != null && nextName.hasNext) nextName.next else null

  protected abstract class Classifier extends super.Val(nextId, nextNameOrNull) {
    def is(method: Method): Boolean
  }
  case object Synthetic extends Classifier {
    def is(method: Method): Boolean = method.isSynthetic()
  }
  case object Getter extends Classifier {
    def is(method: Method): Boolean = method.declaringType().fieldByName(method.name()) ne null
  }
  ...

  def allKindsOf(method: Method): Set[MethodClassifier.Value] = values.filter(_.is(method))
}

I know it's more verbose, but I like that it keeps data and behavior close, particularly for the Forwarder and DefaultGetter case.

@dragos Owner
dragos added a note

I like it (except the part where we depend on internals of Enumeration). Now what would be a better name for def is?

@dragos Owner
dragos added a note

Actually, it's not so good. Moving all the logic in the enumeration object makes it static and would prevent caching in the future (that is important for constant pool parsing in case of Forwarders)

@dragos Owner
dragos added a note

I'll add a comment about these alternatives and leave it as such for the moment. We can revisit this when we work more on step filters, like adding caching. Does that sound good?

@dotta Owner
dotta added a note

Yep.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ case Synthetic =>
+ method.isSynthetic()
+
+ case Getter =>
+ method.declaringType().fieldByName(method.name()) ne null
+
+ case Setter =>
+ val name = method.name()
+ (name.endsWith("_$eq")
+ && (method.declaringType().fieldByName(name.substring(0, name.length - 4)) ne null))
+
+ case DefaultGetter =>
+ defaultR.findFirstIn(method.name()).isDefined
+
+ case Forwarder => debugTimed("Testing flag Forwarder for %s".format(method)) {
+ (method.virtualMachine().canGetBytecodes()
+ && method.virtualMachine().canGetConstantPool()
+ && isForwarderBytecode(method))
+ }
+
+ case Bridge =>
+ method.isBridge()
+
+ case TraitConstructor =>
+ method.name() == "$init$"
+ }
+ }
+
+ /** Return all kinds that qualify this method. May be more than one (for instance, a method
+ * may be both `Synthetic` and `Bridge`, or `Synthetic` and `DefaultGetter`).
+ */
+ def allKindsOf(method: Method): Set[MethodClassifier.Value] = {
+ MethodClassifier.values.filter(is(_, method))
+ }
+
+ /** TODO: cache the constant pool.
+ */
+ private[debug] def isForwarderBytecode(method: Method): Boolean = {
+ val bytecode = method.bytecodes()
+ val cpool = method.declaringType().constantPool()
+ val cpoolSize = method.declaringType().constantPoolCount()
+
+ isForwarderBytecode(bytecode, cpool, cpoolSize, method.name())
+ }
+
+ /** Check that the `bytecode` is a forwarder call.
+ *
+ * Heuristic:
+ * - the bytecode ends with a `return` bytecode
+ * - `return` is immediately preceded by a static call to a method defined in a implementation class
+ * (name ends in `$class`)
+ * - the static method has the same name as the current method.
+ * - don't look at methods that are larger than a costant (MAX_CODE_SIZE). This saves parsing the constant
+ * pool when it's practically impossible to be a forwarder call.
+ *
+ * This might get expensive: the constant pool has to be fully parsed in order to retrieve the name of
+ * the class and method that is called.
+ *
+ * (private[debug] for testing).
+ */
+ private[debug] def isForwarderBytecode(bytecode: Array[Byte], cpool: Array[Byte], cpoolSize: Int, name: String): Boolean = {
+ import JVMOpcodes._
+ val MAX_CODE_SIZE = 50 // a method with 22 args is less than 50 bytes long in bytecode
+
+ def forwarderSequence(bytes: Array[Byte]): Boolean = bytes match {
+ case Array(`invoke_static`, idx1, idx2, ret) if returnOpcode(ret) =>
+ val idx = ((idx1 & 0xFF) << 8) + (idx2 & 0xFF) // mask needed for making Byte unsigned
+ val pool = new ConstantPool(cpool, cpoolSize)
+ val ConstantPool.MethodRef(clsName, methodName, _) = pool.getMethodRef(idx)
+ (clsName.endsWith("$class") && name == methodName)
+
+ case _ =>
+ false
+ }
+
+ ((bytecode.length < MAX_CODE_SIZE) // avoid the expensive constant pool check if the method is too long
+ && (bytecode.length > 4)
+ && forwarderSequence(bytecode.slice(bytecode.length - 4, bytecode.length)))
+ }
+}
+
+/** A partial list of JVM opcodes that are useful for the method classifier */
+object JVMOpcodes {
+ // return instructions
+ final val areturn = 0xB0.toByte
+ final val dreturn = 0xAF.toByte
+ final val freturn = 0xAE.toByte
+ final val ireturn = 0xAC.toByte
+ final val lreturn = 0xAD.toByte
+ final val _return = 0xB1.toByte
+ // invoke
+ final val invoke_static = 0xB8.toByte
+
+ final val returnOpcode = Set(areturn, dreturn, freturn, ireturn, lreturn, _return)
+}
View
2  org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/model/ScalaDebugTarget.scala
@@ -64,6 +64,8 @@ object ScalaDebugTarget extends HasLogger {
*/
abstract class ScalaDebugTarget private (val virtualMachine: VirtualMachine, launch: ILaunch, process: IProcess) extends ScalaDebugElement(null) with IDebugTarget {
+ val stepFilters = new StepFilters
+
// Members declared in org.eclipse.debug.core.IBreakpointListener
override def breakpointAdded(breakponit: IBreakpoint): Unit = ???
View
23 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/model/StepFilters.scala
@@ -10,7 +10,9 @@ import org.eclipse.core.internal.localstore.IsSynchronizedVisitor
/** Utility methods for deciding when a location should be filtered out from stepping into.
*/
-object StepFilters extends HasLogger {
+class StepFilters extends HasLogger {
+
+ val classifier = new MethodClassifier
private lazy val prefStore = ScalaDebugPlugin.plugin.getPreferenceStore()
@@ -20,23 +22,10 @@ object StepFilters extends HasLogger {
* Examples are: synthetic, bridges, getters, setters.
*/
private def isTransparentMethod(location: Location): Boolean = {
- import DebuggerPreferences._
-
- def isGetter = location.declaringType().fieldByName(location.method().name()) ne null
- def skipGetter = prefStore.getBoolean(FILTER_GETTER) && isGetter
-
- def isSetter = {
- val name = location.method().name()
- (name.endsWith("_$eq")
- && (location.declaringType().fieldByName(name.substring(0, name.length - 4)) ne null))
- }
- def skipSetter = prefStore.getBoolean(FILTER_SETTER) && isSetter
- def skipSynthetic = prefStore.getBoolean(FILTER_SYNTHETIC) && location.method.isSynthetic()
-
(location.method.isBridge()
- || skipSynthetic
- || skipGetter
- || skipSetter)
+ || MethodClassifier.values.exists { flag =>
+ prefStore.getBoolean(DebuggerPreferences.BASE_FILTER + flag.toString) && classifier.is(flag, location.method)
+ })
}
/** Return true if it is a filtered location. */
View
18 org.scala-ide.sdt.debug/src/scala/tools/eclipse/debug/preferences/DebuggerPreferences.scala
@@ -15,6 +15,7 @@ import org.eclipse.ui.dialogs.PreferencesUtil
import scala.tools.eclipse.debug.ScalaDebugger
import scala.tools.eclipse.debug.ScalaDebugPlugin
import org.eclipse.swt.widgets.Label
+import scala.tools.eclipse.debug.model.MethodClassifier
class DebuggerPreferences extends FieldEditorPreferencePage with IWorkbenchPreferencePage {
import DebuggerPreferences._
@@ -29,6 +30,8 @@ Configured step filters:
addField(new BooleanFieldEditor(FILTER_SYNTHETIC, "Filter SYNTHETIC methods", parent))
addField(new BooleanFieldEditor(FILTER_GETTER, "Filter Scala getters", getFieldEditorParent))
addField(new BooleanFieldEditor(FILTER_SETTER, "Filter Scala setters", getFieldEditorParent))
+ addField(new BooleanFieldEditor(FILTER_DEFAULT_GETTER, "Filter getters for default parameters", getFieldEditorParent))
+ addField(new BooleanFieldEditor(FILTER_FORWARDER, "Filter forwarder to trait methods", getFieldEditorParent))
}
def init(workbench: IWorkbench) {}
@@ -36,12 +39,15 @@ Configured step filters:
}
object DebuggerPreferences {
- val BASE = "org.scala-ide.sdt.debug."
- val FILTER_SYNTHETIC = BASE + "filerSynthetic"
- val FILTER_GETTER = BASE + "filterGetter"
- val FILTER_SETTER = BASE + "filterSetter"
-
+ import MethodClassifier._
+ val BASE = "org.scala-ide.sdt.debug."
+ val BASE_FILTER = BASE + "filter."
+ val FILTER_SYNTHETIC = BASE_FILTER + Synthetic
+ val FILTER_GETTER = BASE_FILTER + Getter
+ val FILTER_SETTER = BASE_FILTER + Setter
+ val FILTER_DEFAULT_GETTER = BASE_FILTER + DefaultGetter
+ val FILTER_FORWARDER = BASE_FILTER + Forwarder
}
class DebugerPreferencesInitializer extends AbstractPreferenceInitializer {
@@ -52,5 +58,7 @@ class DebugerPreferencesInitializer extends AbstractPreferenceInitializer {
store.setDefault(FILTER_SYNTHETIC, true)
store.setDefault(FILTER_GETTER, true)
store.setDefault(FILTER_SETTER, true)
+ store.setDefault(FILTER_DEFAULT_GETTER, true)
+ store.setDefault(FILTER_FORWARDER, true)
}
}
Something went wrong with that request. Please try again.