Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

web: javascript: Fix bugs in function literals that contain statements,

JsStub#hashCode etc.
  • Loading branch information...
commit 469f98a3a241eb9409461d73d34c8916cf13849c 1 parent 9cc9d04
@nafg authored
View
87 reactive-web/src/main/scala/reactive/web/javascript/JsExp.scala
@@ -3,7 +3,8 @@ package web
package javascript
import net.liftweb.json._
-import java.lang.reflect.Proxy
+
+import java.lang.reflect.{ InvocationHandler, Proxy, Method }
/**
* Contains types that model javascript types.
@@ -167,18 +168,30 @@ class Func1Lit[-P <: JsAny, +R <: JsAny](f: JsExp[P] => JsExp[R]) extends JsLite
lazy val (exp, statements) = JsStatement.inScope {
f(JsIdent('arg))
}
- def render = "(function(arg){return "+JsExp.render(exp)+"})"
+ def render = {
+ // if the last statement is a return statement, disregard the expression value
+ object MIH {
+ def unapply(x: JsExp[_]) =
+ if (!Proxy.isProxyClass(x.getClass)) None
+ else Some(Proxy.getInvocationHandler(x)).collect{ case mih: MethodInvocationHandler[_] => mih }
+ }
+ "(function(arg)"+JsStatement.renderBlock(
+ (statements.lastOption, exp) match {
+ case (Some(_: Return[_]), _) => statements
+ case (Some(s), e) if s eq e => statements.dropRight(1) ++ JsStatement.inScope(Return(exp))._2
+ case (Some(s), MIH(mih)) if mih.apm eq s => statements.dropRight(1) ++ JsStatement.inScope(Return(exp))._2
+ case _ => statements ++ JsStatement.inScope(Return(exp))._2
+ }
+ )+")"
+ }
}
trait ToJsLow { // make sure Map has a higher priority than a regular function
implicit def func1[P <: JsAny, R <: JsAny]: ToJsLit[JsExp[P] => JsExp[R], JsFunction1[P, R]] =
new ToJsLit[JsExp[P] => JsExp[R], JsFunction1[P, R]]((f: JsExp[P] => JsExp[R]) => new Func1Lit(f))
}
trait ToJsMedium extends ToJsLow {
- implicit def voidFunc1[P <: JsAny]: ToJsLit[JsExp[P] => JsStatement, P =|> JsVoid] = new ToJsLit[JsExp[P] => JsStatement, P =|> JsVoid]({ f: (JsExp[P] => JsStatement) =>
- "("+JsStatement.render(new Function[P](x => f(x)) {
- override val ident = Symbol("")
- })+")"
- })
+ implicit def voidFunc1[P <: JsAny]: ToJsLit[JsExp[P] => JsStatement, P =|> JsVoid] =
+ new ToJsLit[JsExp[P] => JsStatement, P =|> JsVoid]((f: JsExp[P] => JsStatement) => new Func1Lit({ x: JsExp[P] => f(x); JsRaw("") }))
}
/**
* Contains implicit conversions from scala values to javascript literals.
@@ -327,9 +340,7 @@ class CanOrder[-L <: JsAny, -R <: JsAny, +T <: JsAny](f: String => $[L] => $[R]
* Traits that extends JsStub can have proxy instances vended
* whose methods result in calls to javascript methods
*/
-trait JsStub extends NamedIdent[JsObj] {
- override def hashCode() = System.identityHashCode(this)
-}
+trait JsStub extends NamedIdent[JsObj]
/**
* A function that converts one JsStub type to another. Preserves JsStatement stack info.
@@ -350,3 +361,59 @@ class Extend[Old <: JsExp[_], New <: JsStub: ClassManifest] extends (Old => New)
}
)
}
+
+private[javascript] class StubInvocationHandler[T <: JsStub: ClassManifest](val ident: String, val toReplace: List[JsStatement] = Nil) extends InvocationHandler {
+ def invoke(proxy: AnyRef, method: Method, args0: scala.Array[AnyRef]): AnyRef = {
+ val args = args0 match { case null => scala.Array.empty case x => x }
+ val clazz: Class[_] = classManifest[T].erasure
+
+ // look for static forwarder --- that means the method has a scala method body, so invoke it
+ def findAndInvokeForwarder(clazz: Class[_]): Option[Method] = try {
+ Some(
+ Class.
+ forName(clazz.getName+"$class").
+ getMethod(method.getName, clazz +: method.getParameterTypes: _*)
+ )
+ } catch {
+ case _: NoSuchMethodException | _: ClassNotFoundException =>
+ clazz.getInterfaces().map(findAndInvokeForwarder).find(_.isDefined) getOrElse (
+ clazz.getSuperclass match {
+ case null => None
+ case c => findAndInvokeForwarder(c)
+ }
+ )
+ }
+ //TODO hack
+
+ findAndInvokeForwarder(clazz).map(_.invoke(null, proxy +: args: _*)) getOrElse {
+ if (method.getName == "render" && method.getReturnType == classOf[String] && args.isEmpty)
+ ident
+ else if (method.getName == "ident" && method.getReturnType == classOf[Symbol] && args.isEmpty)
+ Symbol(ident)
+ else if (method.getName == "hashCode" && method.getReturnType == classOf[Int] && args.isEmpty)
+ System.identityHashCode(proxy): java.lang.Integer
+ else {
+ //It's a field if: (1) no args, and (2) either it's type is Assignable or it's a var
+ val (proxy, toReplace2) =
+ if (args.isEmpty && (
+ classOf[Assignable[_]].isAssignableFrom(method.getReturnType()) ||
+ clazz.getMethods.exists(_.getName == method.getName+"_$eq")
+ )) {
+ (new ProxyField(ident, method.getName), Nil)
+ } else {
+ val p = new ApplyProxyMethod(ident, method, args, toReplace)
+ (p, p :: Nil)
+ }
+
+ // Usually just return the proxy. But if it's a JsStub then the javascript is not fully built --- we need a new proxy.for the next step.
+ if (!(classOf[JsStub] isAssignableFrom method.getReturnType())) proxy
+ else java.lang.reflect.Proxy.newProxyInstance(
+ getClass.getClassLoader,
+ method.getReturnType().getInterfaces :+ method.getReturnType(),
+ new MethodInvocationHandler(proxy, toReplace2)(Manifest.classType(method.getReturnType()))
+ )
+ }
+ }
+ }
+}
+private[javascript] class MethodInvocationHandler[A <: JsStub: ClassManifest](val apm: JsExp[_], tr: List[JsStatement]) extends StubInvocationHandler(JsExp.render(apm), tr)
View
3  reactive-web/src/main/scala/reactive/web/javascript/JsStatement.scala
@@ -82,13 +82,14 @@ object JsStatement {
case _ => renderImpl(statement)
}
private def indentAndRender(statement: JsStatement) = indent.withValue(indent.value map (2+))(indentStr + render(statement))
+ private[javascript] def renderBlock(statements: List[JsStatement]): String = if (statements.isEmpty) "{}" else varsFirst(statements).map(indentAndRender).mkString("{"+nl, ";"+nl, nl + indentStr+"}")
private def renderImpl(statement: JsStatement): String = statement match {
case i: If.If => "if("+JsExp.render(i.cond)+") "+renderImpl(i.body)
case e: If.Elseable#Else => render(e.outer)+" else "+render(e.body)
case ei: If.Elseable#ElseIf => render(ei.outer)+" else if("+JsExp.render(ei.cond)+") "+render(ei.body)
case s: Apply1[_, _] => s.render
case s: ApplyProxyMethod[_] => s.render
- case b: Block => if (b.body.isEmpty) "{}" else varsFirst(b.body).map(indentAndRender).mkString("{"+nl, ";"+nl, nl + indentStr+"}")
+ case b: Block => renderBlock(b.body)
case w: While.While => "while("+JsExp.render(w.cond)+") "+render(w.body)
case dw: Do.DoWhile => "do "+render(dw.body)+" while("+JsExp.render(dw.cond)+")"
case s: Switch.Switch[_] => "switch("+JsExp.render(s.input)+") {"+nl + s.matches.map(renderMatch).mkString+"}"
View
51 reactive-web/src/main/scala/reactive/web/javascript/package.scala
@@ -1,8 +1,6 @@
package reactive
package web
-import java.lang.reflect.{ InvocationHandler, Proxy, Method }
-
package object javascript {
import JsTypes._
@@ -30,55 +28,6 @@ package object javascript {
*/
def $[T <: JsAny](name: Symbol) = JsIdent[T](name)
- private[javascript] class StubInvocationHandler[T <: JsStub: ClassManifest](val ident: String, val toReplace: List[JsStatement] = Nil) extends InvocationHandler {
- def invoke(proxy: AnyRef, method: Method, args0: scala.Array[AnyRef]): AnyRef = {
- val args = args0 match { case null => scala.Array.empty case x => x }
- val clazz: Class[_] = classManifest[T].erasure
-
- // look for static forwarder --- that means the method has a scala method body, so invoke it
- def findAndInvokeForwarder(clazz: Class[_]): Option[Method] = try {
- Some(
- Class.
- forName(clazz.getName+"$class").
- getMethod(method.getName, clazz +: method.getParameterTypes: _*)
- )
- } catch {
- case _: NoSuchMethodException | _: ClassNotFoundException =>
- clazz.getInterfaces().map(findAndInvokeForwarder).find(_.isDefined) getOrElse (
- clazz.getSuperclass match {
- case null => None
- case c => findAndInvokeForwarder(c)
- }
- )
- }
- //TODO hack
- if (method.getName == "render" && method.getReturnType == classOf[String] && args.isEmpty)
- ident
- else {
- findAndInvokeForwarder(clazz).map(_.invoke(null, proxy +: args: _*)) getOrElse {
- //It's a field if: (1) no args, and (2) either it's type is Assignable or it's a var
- val (proxy, toReplace2) =
- if (args.isEmpty && (
- classOf[Assignable[_]].isAssignableFrom(method.getReturnType()) ||
- clazz.getMethods.exists(_.getName == method.getName+"_$eq")
- )) {
- (new ProxyField(ident, method.getName), Nil)
- } else {
- val p = new ApplyProxyMethod(ident, method, args, toReplace)
- (p, p :: Nil)
- }
-
- // Usually just return the proxy. But if it's a JsStub then the javascript is not fully built --- we need a new proxy.for the next step.
- if (!(classOf[JsStub] isAssignableFrom method.getReturnType())) proxy
- else java.lang.reflect.Proxy.newProxyInstance(
- getClass.getClassLoader,
- method.getReturnType().getInterfaces :+ method.getReturnType(),
- new StubInvocationHandler(proxy.render, toReplace2)(Manifest.classType(method.getReturnType()))
- )
- }
- }
- }
- }
/**
* Returns a JsStub proxy for the specified
View
30 reactive-web/src/test/scala/reactive/web/javascript/JsTests.scala
@@ -29,7 +29,7 @@ class JsTests extends FunSuite with ShouldMatchers {
window alert "Small"
}
}.$.render should equal (
- "(function (arg0){if((arg0>10)) {window.alert(\"Greater\")} else {window.alert(\"Small\")}})"
+ "(function(arg){if((arg>10)) {window.alert(\"Greater\")} else {window.alert(\"Small\")};return })"
)
}
@@ -117,7 +117,7 @@ class JsTests extends FunSuite with ShouldMatchers {
"(function(arg){"+
"return window.jQuery(\".items\").jstree(\"create\",null,\"last\").bind("+
"\"rename.jstree\","+
- "(function (arg0){(function(arg){reactive.queueAjax(0)(arg);reactive.doAjax()})(10)})"+
+ "(function(arg){(function(arg){reactive.queueAjax(0)(arg);reactive.doAjax()})(10);return })"+
")"+"}),"+
"1000"+
")"+
@@ -127,6 +127,32 @@ class JsTests extends FunSuite with ShouldMatchers {
}
}
+ test("Function bodies do not get repeated") {
+ def isClean = window.get("isClean").asInstanceOf[Assignable[JsTypes.JsBoolean]]
+ val stmts = JsStatement.inScope{
+ // This does not create a statement
+ { e: $[JsTypes.JsObj] =>
+ Return()
+ }: JsExp[JsObj =|> JsAny]
+ // This does
+ window.onbeforeunload := { e: $[JsTypes.JsObj] =>
+ If(!isClean) {
+ object reply extends JsVar[JsTypes.JsString]
+ reply := "You have unsaved changes!"
+ object evt extends JsVar[JsTypes.JsObj]
+ evt := e
+ If (evt === null) { evt := window.event }
+ If (evt !== null) { evt.get("returnValue") := reply }
+ Return(reply)
+ }
+ }
+ }._2.map(JsStatement.render)
+ stmts foreach println
+ stmts.zipAll(List(
+ """window.onbeforeunload=(function(arg){if((!window["isClean"])) {var reply;var evt;reply="You have unsaved changes!";evt=arg;if((evt==null)) {evt=window.event};if((evt!=null)) {evt["returnValue"]=reply};return reply};return })"""
+ ), "", "") foreach { case (a, b) => a should equal (b) }
+ }
+
test("Statements") {
window.alert(window.encodeURIComponent("Message"))
JsStatement.render(JsStatement.pop) should equal ("window.alert(window.encodeURIComponent(\"Message\"))")
Please sign in to comment.
Something went wrong with that request. Please try again.