Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions sjsonnet/src/sjsonnet/Importer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,14 @@ class CachedResolver(
val parsed: Either[Error, (Expr, FileScope)] = content.preParsedAst match {
case Some(pre) => Right(pre)
case None =>
CachedResolver.parseJsonImport(
val parsedJson = CachedResolver.parseJsonImport(
path,
content,
internedStrings,
settings
) match {
case Some(parsedJson) => Right(parsedJson)
case None => parseJsonnet(path, content)
}
)
if (parsedJson.isDefined) Right(parsedJson.get)
else parseJsonnet(path, content)
}
parsed.flatMap { case (e, fs) => process(e, fs) }
}
Expand Down Expand Up @@ -369,17 +368,17 @@ object CachedResolver {
path: Path,
content: ResolvedFile,
internedStrings: mutable.HashMap[String, String],
settings: Settings): Option[(Expr, FileScope)] = {
if (!path.last.endsWith(".json")) return None
settings: Settings): OptionVal[(Expr, FileScope)] = {
if (!path.last.endsWith(".json")) return OptionVal.None
val fileScope = new FileScope(path)
try {
val visitor =
new JsonImportVisitor(fileScope, internedStrings, settings)
Some((ujson.ByteArrayParser.transform(content.readRawBytes(), visitor), fileScope))
OptionVal.some((ujson.ByteArrayParser.transform(content.readRawBytes(), visitor), fileScope))
} catch {
case _: ujson.ParsingFailedException | _: DuplicateJsonKey | _: InvalidJsonNumber |
_: JsonParseDepthExceeded | _: NumberFormatException =>
None
OptionVal.None
}
}

Expand Down
30 changes: 30 additions & 0 deletions sjsonnet/src/sjsonnet/OptionVal.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package sjsonnet

/**
* Optional reference value similar to `Option`, represented as a value class over a nullable
* reference. Use this only in allocation-sensitive internal paths where the payload type is a
* non-null reference value.
*/
private[sjsonnet] object OptionVal {
@inline def some[A <: AnyRef](x: A): OptionVal[A] = {
if (x eq null) throw new NullPointerException("OptionVal.some(null)")
new OptionVal(x)
}

/**
* Boxed singleton sentinel; returning a freshly constructed null value class can erase to null.
*/
val None: OptionVal[Null] = new OptionVal[Null](null)
}

private[sjsonnet] final class OptionVal[+A <: AnyRef](val x: A) extends AnyVal {
@inline def isEmpty: Boolean = x eq null
@inline def isDefined: Boolean = !isEmpty

@inline def get: A =
if (isEmpty) throw new NoSuchElementException("OptionVal.None.get")
else x

@inline def getOrElse[B >: A](default: => B): B =
if (isEmpty) default else x
}
59 changes: 31 additions & 28 deletions sjsonnet/src/sjsonnet/Preloader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,40 +130,43 @@ class Preloader(parentImporter: Importer, settings: Settings = Settings.default)
}

private def discover(path: Path, content: ResolvedFile): Either[Error, Unit] = {
CachedResolver.parseJsonImport(
val parsedJson = CachedResolver.parseJsonImport(
path,
content,
internedStrings,
settings
) match {
case Some((expr, fs)) =>
cache.put((path, false), PreParsedResolvedFile(content, expr, fs))
Right(())
case None =>
val parser = new Parser(path, internedStrings, internedFieldSets, settings)
try {
fastparse.parse(content.getParserInput(), parser.document(_)) match {
case f: Parsed.Failure =>
val traced = f.trace()
Left(new ParseError(s"$path: ${traced.msg}", offset = traced.index))
case Parsed.Success((expr, fs), _) =>
// Stash the parsed AST on the cache entry so the Interpreter doesn't re-run fastparse.
// The optimizer still runs once at evaluation time on cache hit.
cache.put((path, false), PreParsedResolvedFile(content, expr, fs))
// Match the synchronous evaluator's docBase: resolve relative to the importing file's
// parent directory, not the file path itself. See Importer.resolveAndReadOrFail, which
// calls resolve(pos.fileScope.currentFile.parent(), ...).
val docBase = path.parent()
ImportFinder.collect(expr).foreach { found =>
parentImporter.resolve(docBase, found.value).foreach { resolved =>
record(resolved, found.kind)
}
)
if (parsedJson.isDefined) {
val parsed = parsedJson.get
val expr = parsed._1
val fs = parsed._2
cache.put((path, false), PreParsedResolvedFile(content, expr, fs))
Right(())
} else {
val parser = new Parser(path, internedStrings, internedFieldSets, settings)
try {
fastparse.parse(content.getParserInput(), parser.document(_)) match {
case f: Parsed.Failure =>
val traced = f.trace()
Left(new ParseError(s"$path: ${traced.msg}", offset = traced.index))
case Parsed.Success((expr, fs), _) =>
// Stash the parsed AST on the cache entry so the Interpreter doesn't re-run fastparse.
// The optimizer still runs once at evaluation time on cache hit.
cache.put((path, false), PreParsedResolvedFile(content, expr, fs))
// Match the synchronous evaluator's docBase: resolve relative to the importing file's
// parent directory, not the file path itself. See Importer.resolveAndReadOrFail, which
// calls resolve(pos.fileScope.currentFile.parent(), ...).
val docBase = path.parent()
ImportFinder.collect(expr).foreach { found =>
parentImporter.resolve(docBase, found.value).foreach { resolved =>
record(resolved, found.kind)
}
Right(())
}
} catch {
case e: ParseError => Left(e)
}
Right(())
}
} catch {
case e: ParseError => Left(e)
}
}
}

Expand Down
28 changes: 28 additions & 0 deletions sjsonnet/test/src/sjsonnet/OptionValTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package sjsonnet

import utest._

object OptionValTests extends TestSuite {
def tests: Tests = Tests {
test("some stores non-null references") {
val value = new String("value")
val option = OptionVal.some(value)
assert(option.isDefined)
assert(!option.isEmpty)
assert(option.get eq value)
assert(option.getOrElse("fallback") eq value)
}

test("none widens to any reference payload type") {
val option: OptionVal[String] = OptionVal.None
assert(option.isEmpty)
assert(!option.isDefined)
assert(option.getOrElse("fallback") == "fallback")
assertThrows[NoSuchElementException](option.get)
}

test("some rejects null payloads") {
assertThrows[NullPointerException](OptionVal.some(null))
}
}
}
Loading