-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support sbt 1.4 virtual files when displaying error source #10648
Support sbt 1.4 virtual files when displaying error source #10648
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just some minor comments.
However what about requiring sbt 1.4+ in the next release of play so that we could remove the reflection stuff?
case vf => { // sbt 1.4+ virtual file, see #10486 | ||
vf.getClass.getSimpleName match { | ||
case "BasicVirtualFileRef" | "MappedVirtualFile" => { | ||
val names = vf.getClass.getMethod("names").invoke(vf).asInstanceOf[Array[String]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be worth to store the result of getClass.getMethod("names")
somewhere, so we don't need to pay this penalty for every file on every compilation?
Alternatively, would be worth to implement this with structural typing? I know the reflection is still there, but...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
..., so we don't need to pay this penalty for every file on every compilation?
This code isn't called for every file on every compilation, but just for one file (the source file that caused the exception) and only when a runtime error occurs and only in dev mode (when the play error page pops up in your browser).
So I don't think there is the need for any caching, basically there is no penalty. It would just complicate the code, plus I think the cache would be gone immediately anyway because after an exception in dev mode the server restarts anyway...
Alternatively, would be worth to implement this with structural typing? I know the reflection is still there, but...
You mean by instead of checking the class names, by observing if a certain methods with certain parameters and with a certain return type exists and if everything matches, then call it? I think my solution is good enough, I guess (or hope) sbt won't change the class and methods names on every minor release, so I think it's good enough for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICT, this method sourceMap
is called on play.sbt.run.PlayReload#compile
which happens for each compilation... Then eventually the resulting Map from this, will be used on play.runsupport.Reloader#findSource
...
However looking again at the stack trace from #10486, you are right and the function inside the flatMap is only called on findSource
. I think this is just luck because analysis.relations.classes.reverseMap
is lazy (you can see in the exception that it is a MappedValues
) .
That said forget my suggestion, because I think this might need proper profiling to see if it is a problem in a big project or not...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Probably it's luck, I didn't really check when this method is called where, I just debugged and it was always only called when runtime exceptions occured.
Anyway, if the reflection solution will ever be a problem performance wise, we can soon just require sbt 1.4 for an upcoming release and replace the reflection code with the correct implementation. So don't think this is worth anymore effort right now.
Paths.get(names.drop(1).head, names.drop(2): _*) | ||
} else { | ||
// It's an absolute path, sbt uses them e.g. for subprojects located outside of the base project | ||
val id = vf.getClass.getMethod("id").invoke(vf).asInstanceOf[String] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same answer as above...
val prefix = "file://" + | ||
(if (!id.startsWith("/")) { | ||
"/" // In Windows the sbt virtual file id does not start with a slash, but absolute paths in Java URIs need that | ||
} else "") | ||
// The URI will be like file:///home/user/project/SomeClass.scala (Linux/Mac) or file:///C:/Users/user/project/SomeClass.scala (Windows) | ||
Paths.get(URI.create(s"$prefix$id")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to find something to avoid this logic in https://github.com/sbt/io/blob/develop/io/src/main/scala/sbt/io/IO.scala but no luck.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Not my decision, that's up the the Lightbend team which sbt version will be required for the next Play 2.9 release (whenever that happens). I started to work on this fix on the 2.8.x branch, where reflection is needed because people out here right now use sbt 1.2.x or 1.3.x, so if we want to fix it for Play 2.7.x/2.8.x we have to use reflection anyway. In the end I just cherry-picked my patches from the 2.8.x branch to the master branch. |
Actually, this pull request is about runtime errors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's looking good.
Thanks @mkurz for all the research work. I can imagine that it's not easy to assemble all that info.
I left a few suggestions, no blockers.
For the Scala extractors I mention, I would like to try it out and eventually send a commit.
I'm approving it otherwise already.
val prefix = "file://" + | ||
(if (!id.startsWith("/")) { | ||
"/" // In Windows the sbt virtual file id does not start with a slash, but absolute paths in Java URIs need that | ||
} else "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpicking...
I find it hard to read when we do if/else (or any logic or computation) embedded in strings or other places.
I think this could benefit of a new val
We can also revert the logic to avoid !
.
// In Windows the sbt virtual file id does not start with a slash, but absolute paths in Java URIs need that
val extraSlash = if (id.startsWith("/")) "" else "/"
val prefix = "file://" + extraSlash
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied you suggestion.
case Some(file) => | ||
file match { | ||
case file: File => // sbt < 1.4 | ||
Map(name -> Source(file, MaybeGeneratedSource.unapply(file).flatMap(_.source))) | ||
case vf => { // sbt 1.4+ virtual file, see #10486 | ||
vf.getClass.getSimpleName match { | ||
case "BasicVirtualFileRef" | "MappedVirtualFile" => { | ||
val names = vf.getClass.getMethod("names").invoke(vf).asInstanceOf[Array[String]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nexting pattern matching can be avoided with Scala custom extractors. I usually prefer to avoid them, but in this case I will make the code more clear.
It could look like:
files.headOption match {
case None => Map.empty[String, Source]
case SomeFile(file) =>
case SomeVirtualFile(file) =>
}
btw, why we need .asInstanceOf[Option[Any]]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, why we need
.asInstanceOf[Option[Any]]
?
This was needed (before you added the extractors). I was wondering as well, however if it was removed following exception would be thrown (I just tried again):
[info] java.lang.ClassCastException: class sbt.internal.inc.MappedVirtualFile cannot be cast to class java.io.File
[info] at play.sbt.run.PlayReload$.$anonfun$sourceMap$1(PlayReload.scala:76)
[info] at scala.collection.TraversableLike.$anonfun$flatMap$1(TraversableLike.scala:292)
[info] at scala.collection.immutable.HashMap$HashMap1.foreach(HashMap.scala:394)
[info] at scala.collection.immutable.HashMap$HashTrieMap.foreach(HashMap.scala:721)
[info] at scala.collection.TraversableLike.flatMap(TraversableLike.scala:292)
[info] at scala.collection.TraversableLike.flatMap$(TraversableLike.scala:289)
[info] at scala.collection.AbstractTraversable.flatMap(Traversable.scala:108)
[info] at play.sbt.run.PlayReload$.sourceMap(PlayReload.scala:71)
[info] at play.sbt.run.PlayReload$.$anonfun$compile$2(PlayReload.scala:66)
[info] at scala.util.Either$RightProjection.map(Either.scala:713)
Line 76 was:
case Some(file) =>
I guess the bytecode Scala emitted somehow was casting the file
to a File
already in this line (because for the compiler files.headOption
is Option[File]
) even tough actually its a MappedVirtualFile
.
Anyway, with the extractors you added it is also working without that asInstanceOf
casting.
object JFile { | ||
class FileOption(val anyOpt: Option[Any]) extends AnyVal { | ||
def isEmpty: Boolean = anyOpt.exists(_.isInstanceOf[java.io.File]) | ||
def get: java.io.File = anyOpt.get.asInstanceOf[java.io.File] | ||
} | ||
def unapply(any: Option[Any]): FileOption = new FileOption(any) | ||
} | ||
|
||
object VirtualFile { | ||
def unapply(value: Some[Any]): Option[Any] = | ||
value.filter { vf => | ||
val name = value.getClass.getSimpleName | ||
(name == "BasicVirtualFileRef" || name == "MappedVirtualFile" ) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mkurz, I took the liberty to push on this PR and add these two extractors.
Let me know if you find that it improves the pattern matching bellow. If you don't think so, I can revert.
I'm just trying this out to avoid too much pattern matching nesting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed your code and it's working now.
@mkurz, sorry for the bad formatting. I didn't think about it. |
Fixes #10486
Fixes #10497
Backport for 2.8.x: #10649
Backport for 2.7.x: #10650
sbt 1.4 introduced a virtual file system it is using internally. So instead of Java
File
objects, Play now receivesBasicVirtualFileRef
andMappedVirtualFile
objects, which both are subclasses of theVirtualFileRef
interface.Fortunately, that
VirtualFileRef
interface has methods that, with some reflection kung fu, we can use to figure out the correct file system path which we want to display on a Play error page if an exception occured:id()
andnames()
.Such virtual file objects wrap a path in two possible ways:
${BASE}
(like${BASE}/app/controllers/HomeController.scala
/home/user/my_sub_project/src/main/scala/Foo.scala
) That is the case for example with sub-projects that are not contained within the base project.I also added scripted tests to make sure future changes or sbt upgrades that break things will be noticed asap.
BTW: I also went that annoying extra kilometer and did test this patch and the scripted tests on Microsoft® Windows 10 to make sure that file system path stuff also works with these obscure backslash and c: style of doing things as well.
Making-of:
This pull request is inspired by the reflection porn done in lombok to make it support sbt's virtual file system.
Originally I submitted a patch (projectlombok/lombok#2643) which was refused (because it was just a workaround) and instead the maintainer did his thing and came up with a reflection heavy commit that actually worked and did fix that bug. However soon I ran into another problem because files outside of a root project did not work yet, so I came up with an additional fix.
Oh and yes, along the way I discovered another change of behaviour ins sbt: sbt/sbt#6275 But that is not that relevant for this pull request here.
So yeah, as you can see, sbt 1.4 already caused me some headache...