From eb9e4bbebc620a65ad21e375385f72dc4e9235b8 Mon Sep 17 00:00:00 2001 From: Sven Keidel Date: Wed, 13 May 2026 07:36:35 +0200 Subject: [PATCH 1/4] Migrate from scala-reflect 2.13 to Java's reflection API --- .../cli/ProjectBasedCommandLineConfig.scala | 3 +-- .../main/scala/org/opalj/util/package.scala | 20 ++++++++++++++----- ...rationBasedConstructorEscapeAnalysis.scala | 15 ++++++++++---- project/Dependencies.scala | 4 +--- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/OPAL/br/src/main/scala/org/opalj/br/fpcf/cli/ProjectBasedCommandLineConfig.scala b/OPAL/br/src/main/scala/org/opalj/br/fpcf/cli/ProjectBasedCommandLineConfig.scala index 75fbf51304..46dafad0e7 100644 --- a/OPAL/br/src/main/scala/org/opalj/br/fpcf/cli/ProjectBasedCommandLineConfig.scala +++ b/OPAL/br/src/main/scala/org/opalj/br/fpcf/cli/ProjectBasedCommandLineConfig.scala @@ -5,7 +5,6 @@ package fpcf package cli import scala.language.postfixOps -import scala.reflect.internal.util.NoPosition.showError import java.io.File import java.net.URL @@ -102,7 +101,7 @@ trait ProjectBasedCommandLineConfig extends OPALCommandLineConfig { val cpFiles = resolveDirToCP(get(ProjectDirectoryArg), cp, cp) if (cpFiles.isEmpty) { - showError("Nothing to analyze.") + error("fatal", "Nothing to analyze.") printHelp() sys.exit(1) } diff --git a/OPAL/common/src/main/scala/org/opalj/util/package.scala b/OPAL/common/src/main/scala/org/opalj/util/package.scala index 4f7d8e9b5b..c37d02b232 100644 --- a/OPAL/common/src/main/scala/org/opalj/util/package.scala +++ b/OPAL/common/src/main/scala/org/opalj/util/package.scala @@ -124,15 +124,25 @@ package object util { def getObjectReflectively[A](fqn: String, source: AnyRef, category: String = "configuration")(implicit logContext: LogContext = GlobalLogContext ): Option[A] = { - import scala.reflect.runtime.universe._ try { - val mirror = runtimeMirror(source.getClass.getClassLoader) - val module = mirror.staticModule(fqn) - Some(mirror.reflectModule(module).instance.asInstanceOf[A]) + // Get the class loader from the source object + val classLoader = source.getClass.getClassLoader + + // Scala objects append a '$' to the full qualified name + val clazz = Class.forName(fqn+"$", true, classLoader) + + // Access the singleton instance field + val moduleField = clazz.getField("MODULE$") + + // Read the static field (passing null because it is a static field) + Some(moduleField.get(null).asInstanceOf[A]) } catch { - case sre: ScalaReflectionException => + case sre: ClassNotFoundException => error(category, s"Cannot find object $fqn", sre) None + case nsfe: NoSuchFieldException => + error(category, "Singleton instance field not found", nsfe) + None case cce: ClassCastException => error(category, "Reflected object is invalid", cce) None diff --git a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/escape/ConfigurationBasedConstructorEscapeAnalysis.scala b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/escape/ConfigurationBasedConstructorEscapeAnalysis.scala index f9de58a452..2b1aa937f6 100644 --- a/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/escape/ConfigurationBasedConstructorEscapeAnalysis.scala +++ b/OPAL/tac/src/main/scala/org/opalj/tac/fpcf/analyses/escape/ConfigurationBasedConstructorEscapeAnalysis.scala @@ -35,10 +35,17 @@ trait ConfigurationBasedConstructorEscapeAnalysis extends AbstractEscapeAnalysis */ private val predefinedConstructors: Map[ClassType, EscapeProperty] = { ConfigSource.fromConfig(project.config).at(ConfigKey).loadOrThrow[Seq[PredefinedResult]].map { r => - import scala.reflect.runtime.* - val rootMirror = universe.runtimeMirror(getClass.getClassLoader) - val module = rootMirror.staticModule(r.escapeOfThis) - val property = rootMirror.reflectModule(module).instance.asInstanceOf[EscapeProperty] + // 1. Get the ClassLoader + val classLoader = this.getClass.getClassLoader + + // 2. Load the companion object or module class + // Note: Scala modules (object) compiled to Java have a '$' suffix + val moduleClass = classLoader.loadClass(r.escapeOfThis + "$") + + // 3. Access the singleton instance via the MODULE$ field + val moduleField = moduleClass.getField("MODULE$") + val property = moduleField.get(null).asInstanceOf[EscapeProperty] + (ClassType(r.classType), property) }.toMap } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index de7b446be9..65875f6236 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -42,8 +42,6 @@ object Dependencies { case _ => throw new Exception("Unknown platform!") } - def reflect(scalaVersion: String): ModuleID = "org.scala-lang" % "scala-reflect" % "2.13.16" // scalaVersion No scala-reflect available for Scala 3 - val scalaxml = "org.scala-lang.modules" %% "scala-xml" % version.scalaxml val scalaparallelcollections = "org.scala-lang.modules" %% "scala-parallel-collections" % version.scalaparallelcollections @@ -72,7 +70,7 @@ object Dependencies { val testlibs: Seq[ModuleID] = Seq(junit, scalatest, scalatestjunit, scalacheck) def common(scalaVersion: String) = - Seq(reflect(scalaVersion), scalaparallelcollections, scalaxml, playjson, ficus, pureconfig, fastutil, scallop) + Seq(scalaparallelcollections, scalaxml, playjson, ficus, pureconfig, fastutil, scallop) val si = Seq(scalagraphcore, scalagraphdot) val bi = Seq(commonstext) From 922e92317231a610825637c85daec0b1ff6911b2 Mon Sep 17 00:00:00 2001 From: Sven Keidel Date: Wed, 13 May 2026 08:01:49 +0200 Subject: [PATCH 2/4] fix BytecodeAssembler tests --- .../scala/org/opalj/ba/FieldBuilderTest.scala | 15 +++++++-------- .../test/scala/org/opalj/ba/JumpLabelsTest.scala | 13 +++++-------- .../scala/org/opalj/ba/MethodBuilderTest.scala | 16 ++++++---------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/OPAL/ba/src/test/scala/org/opalj/ba/FieldBuilderTest.scala b/OPAL/ba/src/test/scala/org/opalj/ba/FieldBuilderTest.scala index 78e1b05c0f..e878a1c760 100644 --- a/OPAL/ba/src/test/scala/org/opalj/ba/FieldBuilderTest.scala +++ b/OPAL/ba/src/test/scala/org/opalj/ba/FieldBuilderTest.scala @@ -3,8 +3,6 @@ package org.opalj package ba import scala.language.postfixOps -import scala.reflect.classTag -import scala.reflect.runtime.universe.* import java.io.ByteArrayInputStream @@ -88,8 +86,8 @@ class FieldBuilderTest extends AnyFlatSpec { this.getClass.getClassLoader ) - val fieldInstance = loader.loadClass(javaClassName).getDeclaredConstructor().newInstance() - val mirror = runtimeMirror(loader).reflect(fieldInstance)(using classTag[AnyRef]) + val clazz = loader.loadClass(javaClassName) + val fieldInstance = clazz.getDeclaredConstructor().newInstance() val brClassFile = Java8Framework.ClassFile(() => new ByteArrayInputStream(rawClassFile)).head @@ -101,13 +99,14 @@ class FieldBuilderTest extends AnyFlatSpec { } "the field `FieldClass.privateField`" should "be initialized as true" in { - val field = mirror.symbol.typeSignature.member(TermName("privateField")).asTerm - assert(mirror.reflectField(field).get == true) + val field = clazz.getDeclaredField("privateField") + field.setAccessible(true) + assert(field.get(fieldInstance) == true) } "FieldClass.publicField" should "be initialized as 3" in { - val field = mirror.symbol.typeSignature.member(TermName("publicField")).asTerm - assert(mirror.reflectField(field).get == 3) + val field = clazz.getDeclaredField("publicField") + assert(field.get(fieldInstance) == 3) } } diff --git a/OPAL/ba/src/test/scala/org/opalj/ba/JumpLabelsTest.scala b/OPAL/ba/src/test/scala/org/opalj/ba/JumpLabelsTest.scala index 66990ce5ae..4cda94b6a7 100644 --- a/OPAL/ba/src/test/scala/org/opalj/ba/JumpLabelsTest.scala +++ b/OPAL/ba/src/test/scala/org/opalj/ba/JumpLabelsTest.scala @@ -3,8 +3,6 @@ package org.opalj package ba import scala.language.postfixOps -import scala.reflect.classTag -import scala.reflect.runtime.universe.* import java.io.ByteArrayInputStream @@ -83,13 +81,12 @@ class JumpLabelsTest extends AnyFlatSpec { def testClass(clazz: Class[?]): Unit = { val testJumpInstance = clazz.getDeclaredConstructor().newInstance() - val mirror = runtimeMirror(loader).reflect(testJumpInstance)(using classTag[AnyRef]) - val method = mirror.symbol.typeSignature.member(TermName("returnInt")).asMethod + val method = clazz.getMethod("returnInt", classOf[Int]) - assert(mirror.reflectMethod(method)(0) == 0) - assert(mirror.reflectMethod(method)(1) == 1) - assert(mirror.reflectMethod(method)(2) == 2) - assert(mirror.reflectMethod(method)(10) == 10) + assert(method.invoke(testJumpInstance, 0) == 0) + assert(method.invoke(testJumpInstance, 1) == 1) + assert(method.invoke(testJumpInstance, 2) == 2) + assert(method.invoke(testJumpInstance, 10) == 10) } testClass(loader.loadClass("TestJumpJava5")) diff --git a/OPAL/ba/src/test/scala/org/opalj/ba/MethodBuilderTest.scala b/OPAL/ba/src/test/scala/org/opalj/ba/MethodBuilderTest.scala index 6e7e0d7b7d..b4c6263fb5 100644 --- a/OPAL/ba/src/test/scala/org/opalj/ba/MethodBuilderTest.scala +++ b/OPAL/ba/src/test/scala/org/opalj/ba/MethodBuilderTest.scala @@ -3,8 +3,6 @@ package org.opalj package ba import scala.language.postfixOps -import scala.reflect.classTag -import scala.reflect.runtime.universe.* import java.io.ByteArrayInputStream import scala.collection.immutable.ArraySeq @@ -59,10 +57,9 @@ class MethodBuilderTest extends AnyFlatSpec { val simpleMethodClazz = loader.loadClass("SimpleMethodClass") val simpleMethodInstance = simpleMethodClazz.getDeclaredConstructor().newInstance() - val mirror = runtimeMirror(loader).reflect(simpleMethodInstance)(using classTag[AnyRef]) - val method = mirror.symbol.typeSignature.member(TermName("testMethod")).asMethod + val method = simpleMethodClazz.getMethod("testMethod", classOf[String]) - assert(mirror.reflectMethod(method)("test") == null) + assert(method.invoke(simpleMethodInstance, "test") == null) } val brClassFile = J8ClassFile(() => new java.io.ByteArrayInputStream(rawClassFile)).head @@ -205,11 +202,10 @@ class MethodBuilderTest extends AnyFlatSpec { try { val attributeMethodClass = loader.loadClass("AttributeMethodClass") val attributeTestInstance = attributeMethodClass.getDeclaredConstructor().newInstance() - val mirror = runtimeMirror(loader).reflect(attributeTestInstance)(using classTag[AnyRef]) - val method = mirror.symbol.typeSignature.member(TermName("tryCatchFinallyTest")).asMethod - assert(mirror.reflectMethod(method)(-1) == 0) - assert(mirror.reflectMethod(method)(0) == 1) - assert(mirror.reflectMethod(method)(1) == 2) + val method = attributeMethodClass.getDeclaredMethod("tryCatchFinallyTest", classOf[Int]) + assert(method.invoke(attributeTestInstance, -1) == 0) + assert(method.invoke(attributeTestInstance, 0) == 1) + assert(method.invoke(attributeTestInstance, 1) == 2) } catch { case t: Throwable => info( From 0be9a2e4e6df6f9ab7d6724111904cb3dce81c7d Mon Sep 17 00:00:00 2001 From: Sven Keidel Date: Wed, 13 May 2026 08:14:51 +0200 Subject: [PATCH 3/4] fix formatting --- OPAL/common/src/main/scala/org/opalj/util/package.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OPAL/common/src/main/scala/org/opalj/util/package.scala b/OPAL/common/src/main/scala/org/opalj/util/package.scala index c37d02b232..a867d66c34 100644 --- a/OPAL/common/src/main/scala/org/opalj/util/package.scala +++ b/OPAL/common/src/main/scala/org/opalj/util/package.scala @@ -129,7 +129,7 @@ package object util { val classLoader = source.getClass.getClassLoader // Scala objects append a '$' to the full qualified name - val clazz = Class.forName(fqn+"$", true, classLoader) + val clazz = Class.forName(fqn + "$", true, classLoader) // Access the singleton instance field val moduleField = clazz.getField("MODULE$") From af5c14cbc232c5be6769ffcbad269c1f168e55b2 Mon Sep 17 00:00:00 2001 From: Sven Keidel Date: Fri, 15 May 2026 07:49:37 +0200 Subject: [PATCH 4/4] append $ to object name if missing --- OPAL/common/src/main/scala/org/opalj/util/package.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/OPAL/common/src/main/scala/org/opalj/util/package.scala b/OPAL/common/src/main/scala/org/opalj/util/package.scala index a867d66c34..a14afa0c21 100644 --- a/OPAL/common/src/main/scala/org/opalj/util/package.scala +++ b/OPAL/common/src/main/scala/org/opalj/util/package.scala @@ -124,12 +124,15 @@ package object util { def getObjectReflectively[A](fqn: String, source: AnyRef, category: String = "configuration")(implicit logContext: LogContext = GlobalLogContext ): Option[A] = { + // Ensure fully qualified name of singleton object ends with dollar symbol + val fqnObject = if (fqn.endsWith("$")) fqn else fqn + "$" + try { // Get the class loader from the source object val classLoader = source.getClass.getClassLoader - // Scala objects append a '$' to the full qualified name - val clazz = Class.forName(fqn + "$", true, classLoader) + // Get class from class loader + val clazz = Class.forName(fqnObject, true, classLoader) // Access the singleton instance field val moduleField = clazz.getField("MODULE$") @@ -138,7 +141,7 @@ package object util { Some(moduleField.get(null).asInstanceOf[A]) } catch { case sre: ClassNotFoundException => - error(category, s"Cannot find object $fqn", sre) + error(category, s"Cannot find object $fqnObject", sre) None case nsfe: NoSuchFieldException => error(category, "Singleton instance field not found", nsfe)