Skip to content

Commit 34bf0ae

Browse files
fix(lift-webkit): fix Scala 3 snippet instantiation with type-safe reflection
Resolves snippet class instantiation failures in Scala 3.3.6 that were causing ClassCastException when constructing snippet instances via reflection. Root Cause: Scala 3's stricter type inference would default generic type parameter T to Nothing when calling makeOne[T] without explicit type context, causing attempts to cast snippet instances to scala.runtime.Nothing$ which failed. Solution: - Changed ConstructorType.makeOne methods to return AnyRef instead of T - Added targetClass parameter to ConstructorType trait and implementations - Used Class.cast() for runtime type verification before final cast - Moved final asInstanceOf[T] to call site where type context is available - Fixed variable shadowing issue (paramClz vs clz) in constructFrom - Enhanced error handling for ClassCastException and InvocationTargetException The fix uses Class.cast() which performs proper runtime type checking, making the code more type-safe while working correctly in both Scala 2.13 and Scala 3.3.6. Tests Fixed: - OneShot: all 6 tests now pass (snippet callbacks execute correctly) - ToHeadUsages: all 12 tests now pass (snippet rendering works) - WebSpecSpec: template processing test un-pended and passing - Full test suite: 281 tests pass on Scala 3.3.6 (previously had failures) - No regressions: 282 tests pass on Scala 2.13 (unchanged) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2e623ff commit 34bf0ae

3 files changed

Lines changed: 40 additions & 24 deletions

File tree

web/webkit/src/main/scala/net/liftweb/http/LiftSession.scala

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,32 @@ object LiftSession {
114114
val const = clz.getDeclaredConstructors()
115115

116116
def nullConstructor(): Box[ConstructorType] =
117-
const.find(_.getParameterTypes.length == 0).map(const => UnitConstructor(const))
117+
const.find(_.getParameterTypes.length == 0).map(const => UnitConstructor(const, clz))
118118

119119
pp match {
120-
case Full(ParamPair(value, clz)) =>
120+
case Full(ParamPair(value, paramClz)) =>
121121
const.find {
122122
cp => {
123123
cp.getParameterTypes.length == 2 &&
124-
cp.getParameterTypes().apply(0).isAssignableFrom(clz) &&
124+
cp.getParameterTypes().apply(0).isAssignableFrom(paramClz) &&
125125
cp.getParameterTypes().apply(1).isAssignableFrom(classOf[LiftSession])
126126
}
127127
}.
128-
map(const => PAndSessionConstructor(const)) orElse
128+
map(const => PAndSessionConstructor(const, clz)) orElse
129129
const.find {
130130
cp => {
131131
cp.getParameterTypes.length == 1 &&
132-
cp.getParameterTypes().apply(0).isAssignableFrom(clz)
132+
cp.getParameterTypes().apply(0).isAssignableFrom(paramClz)
133133
}
134134
}.
135-
map(const => PConstructor(const)) orElse nullConstructor()
135+
map(const => PConstructor(const, clz)) orElse nullConstructor()
136136

137137
case _ =>
138138
nullConstructor()
139139
}
140140
}
141141

142-
(if (Props.devMode) {
142+
val constructorBox = if (Props.devMode) {
143143
// no caching in dev mode
144144
calcConstructor()
145145
} else {
@@ -152,10 +152,12 @@ object LiftSession {
152152
nv
153153
}
154154
}
155-
}).map {
156-
case uc: UnitConstructor => uc.makeOne
157-
case pc: PConstructor => pc.makeOne(pp.openOrThrowException("It's ok").v)
158-
case psc: PAndSessionConstructor => psc.makeOne(pp.openOrThrowException("It's ok").v, session)
155+
}
156+
157+
constructorBox.map {
158+
case uc: UnitConstructor => uc.makeOne.asInstanceOf[T]
159+
case pc: PConstructor => pc.makeOne(pp.openOrThrowException("It's ok").v).asInstanceOf[T]
160+
case psc: PAndSessionConstructor => psc.makeOne(pp.openOrThrowException("It's ok").v, session).asInstanceOf[T]
159161
}
160162
}
161163

@@ -1355,7 +1357,15 @@ class LiftSession(private[http] val _contextPath: String, val underlyingId: Stri
13551357
c)
13561358

13571359
} catch {
1358-
case e: IllegalAccessException => Empty
1360+
case e: IllegalAccessException =>
1361+
logger.debug(s"IllegalAccessException instantiating ${c.getName}", e)
1362+
Empty
1363+
case e: ClassCastException =>
1364+
logger.warn(s"ClassCastException instantiating ${c.getName} - reflection type mismatch", e)
1365+
Empty
1366+
case e: java.lang.reflect.InvocationTargetException =>
1367+
logger.warn(s"InvocationTargetException instantiating ${c.getName}", e.getCause)
1368+
Empty
13591369
}
13601370
}
13611371

web/webkit/src/main/scala/net/liftweb/http/Templates.scala

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -339,28 +339,34 @@ class StateInStatelessException(msg: String) extends SnippetFailureException(msg
339339
/**
340340
* a trait that defines some ways of constructing an instance
341341
*/
342-
private sealed trait ConstructorType
342+
private sealed trait ConstructorType {
343+
def targetClass: Class[_]
344+
}
343345

344346
/**
345347
* A unit constructor... just pass in null
346348
*/
347-
private final case class UnitConstructor(c: java.lang.reflect.Constructor[_]) extends ConstructorType {
348-
def makeOne[T]: T = c.newInstance().asInstanceOf[T]
349+
private final case class UnitConstructor(c: java.lang.reflect.Constructor[_], targetClass: Class[_]) extends ConstructorType {
350+
def makeOne: AnyRef = {
351+
targetClass.asInstanceOf[Class[AnyRef]].cast(c.newInstance().asInstanceOf[Object])
352+
}
349353
}
350354

351355
/**
352356
* A parameter and session constructor
353357
*/
354-
private final case class PAndSessionConstructor(c: java.lang.reflect.Constructor[_]) extends ConstructorType {
355-
def makeOne[T](p: Any, s: LiftSession): T =
356-
c.newInstance(p.asInstanceOf[Object], s).asInstanceOf[T]
358+
private final case class PAndSessionConstructor(c: java.lang.reflect.Constructor[_], targetClass: Class[_]) extends ConstructorType {
359+
def makeOne(p: Any, s: LiftSession): AnyRef = {
360+
targetClass.asInstanceOf[Class[AnyRef]].cast(c.newInstance(p.asInstanceOf[Object], s).asInstanceOf[Object])
361+
}
357362
}
358363

359364
/**
360365
* A parameter constructor
361366
*/
362-
private final case class PConstructor(c: java.lang.reflect.Constructor[_]) extends ConstructorType {
363-
def makeOne[T](p: Any): T =
364-
c.newInstance(p.asInstanceOf[Object]).asInstanceOf[T]
367+
private final case class PConstructor(c: java.lang.reflect.Constructor[_], targetClass: Class[_]) extends ConstructorType {
368+
def makeOne(p: Any): AnyRef = {
369+
targetClass.asInstanceOf[Class[AnyRef]].cast(c.newInstance(p.asInstanceOf[Object]).asInstanceOf[Object])
370+
}
365371
}
366372

web/webkit/src/test/scala-3/net/liftweb/mockweb/WebSpecSpec.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ class WebSpecSpec extends WebSpec(WebSpecSpecBoot.boot _) {
153153
})
154154
}
155155

156-
"properly process a template" in {
157-
// FIXME: Scala 3 has ClassCastException with snippet instantiation
158-
pending("Scala 3 snippet instantiation issue - ClassCastException during snippet loading")
156+
"properly process a template" withTemplateFor("http://foo.com/net/liftweb/mockweb/webspecspectemplate") in {
157+
case Full(template) => template.toString.contains("Hello, WebSpec!") === true
158+
case other => failure("Error on template : " + other)
159159
}
160160
}
161161
}

0 commit comments

Comments
 (0)