Skip to content

Commit c767eb1

Browse files
authored
chore: Attach function name in error message with best effects (#618)
refs: #416 Best effects only
1 parent ebcda2b commit c767eb1

10 files changed

Lines changed: 117 additions & 17 deletions

sjsonnet/src/sjsonnet/Evaluator.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class Evaluator(
116116
newScope.bindings(base + i) = b.args match {
117117
case null => visitAsLazy(b.rhs)(newScope)
118118
case argSpec =>
119-
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
119+
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos, b.name)(newScope))
120120
}
121121
i += 1
122122
}
@@ -634,9 +634,10 @@ class Evaluator(
634634
Error.fail(s"Field name must be string or null, not ${fieldName.prettyName}", pos)
635635
}
636636

637-
def visitMethod(rhs: Expr, params: Params, outerPos: Position)(implicit
637+
def visitMethod(rhs: Expr, params: Params, outerPos: Position, name: String = null)(implicit
638638
scope: ValScope): Val.Func =
639639
new Val.Func(outerPos, scope, params) {
640+
override def functionName: String = name
640641
def evalRhs(vs: ValScope, es: EvalScope, fs: FileScope, pos: Position): Val =
641642
visitExpr(rhs)(vs)
642643
override def evalDefault(expr: Expr, vs: ValScope, es: EvalScope): Val = visitExpr(expr)(vs)
@@ -651,7 +652,7 @@ class Evaluator(
651652
case null =>
652653
new Lazy(() => visitExpr(b.rhs)(scope))
653654
case argSpec =>
654-
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(scope))
655+
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos, b.name)(scope))
655656
}
656657
i += 1
657658
}

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,19 @@ object Val {
666666

667667
def evalDefault(expr: Expr, vs: ValScope, es: EvalScope): Val = null
668668

669+
/** Override to provide a function name for error messages. Only called on error paths. */
670+
def functionName: String = null
671+
672+
private def functionNamePrefix: String = {
673+
val name = functionName
674+
if (name != null) s" $name" else ""
675+
}
676+
677+
private def functionNameSuffix: String = {
678+
val name = functionName
679+
if (name != null) s" in function $name" else ""
680+
}
681+
669682
def prettyName = "function"
670683

671684
override def exprErrorString: String = "Function"
@@ -701,18 +714,24 @@ object Val {
701714
while (i < namedNames.length) {
702715
val idx = params.paramMap.getOrElse(
703716
namedNames(i),
704-
Error.fail(s"Function has no parameter ${namedNames(i)}", outerPos)
717+
Error.fail(
718+
s"Function$functionNamePrefix has no parameter ${namedNames(i)}",
719+
outerPos
720+
)
705721
)
706722
if (argVals(base + idx) != null)
707-
Error.fail(s"binding parameter a second time: ${namedNames(i)}", outerPos)
723+
Error.fail(
724+
s"binding parameter a second time: ${namedNames(i)}$functionNameSuffix",
725+
outerPos
726+
)
708727
argVals(base + idx) = argsL(j)
709728
i += 1
710729
j += 1
711730
}
712731
}
713732
if (argsL.length > params.names.length)
714733
Error.fail(
715-
"Too many args, function has " + params.names.length + " parameter(s)",
734+
"Too many args, function" + functionNamePrefix + " has " + params.names.length + " parameter(s)",
716735
outerPos
717736
)
718737
if (params.names.length != argsL.length) { // Args missing -> add defaults
@@ -735,7 +754,7 @@ object Val {
735754
if (missing != null) {
736755
val plural = if (missing.size > 1) "s" else ""
737756
Error.fail(
738-
s"Function parameter$plural ${missing.mkString(", ")} not bound in call",
757+
s"Function$functionNamePrefix parameter$plural ${missing.mkString(", ")} not bound in call",
739758
outerPos
740759
)
741760
}
@@ -815,7 +834,7 @@ object Val {
815834

816835
/** Superclass for standard library functions */
817836
abstract class Builtin(
818-
val functionName: String,
837+
override val functionName: String,
819838
paramNames: Array[String],
820839
defaults: Array[Expr] = null)
821840
extends Func(
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
sjsonnet.Error: Function has no parameter y
1+
sjsonnet.Error: Function jsonToString has no parameter y
22
at [Apply].(native7.jsonnet:1:27)
33

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
sjsonnet.Error: Function has no parameter y
1+
sjsonnet.Error: Function foo has no parameter y
22
at [Apply].(optional_args8.jsonnet:2:4)
33

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
sjsonnet.Error: Function has no parameter blahblah
1+
sjsonnet.Error: Function makeArray has no parameter blahblah
22
at [Apply].(std.makeArrayNamed3.jsonnet:1:14)
33

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
sjsonnet.Error: Too many args, function has 2 parameter(s)
1+
sjsonnet.Error: Too many args, function foo has 2 parameter(s)
22
at [Apply3].(error.function_too_many_args.jsonnet:19:4)
33

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
sjsonnet.Error: Function parameter rest not bound in call
1+
sjsonnet.Error: Function trace parameter rest not bound in call
22
at [Apply1].(error.trace_one_param.jsonnet:17:20)
33
at [ValidId v].(error.trace_one_param.jsonnet:19:6)
44

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
sjsonnet.Error: Too many args, function has 2 parameter(s)
1+
sjsonnet.Error: Too many args, function trace has 2 parameter(s)
22
at [Apply3].(error.trace_three_param.jsonnet:17:20)
33
at [ValidId v].(error.trace_three_param.jsonnet:19:6)
44

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
sjsonnet.Error: Function parameters str, rest not bound in call
1+
sjsonnet.Error: Function trace parameters str, rest not bound in call
22
at [Apply0].(error.trace_zero_param.jsonnet:17:20)
33
at [ValidId v].(error.trace_zero_param.jsonnet:19:6)
44

sjsonnet/test/src/sjsonnet/EvaluatorTests.scala

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ object EvaluatorTests extends TestSuite {
457457
)
458458
}
459459

460-
assert(ex.getMessage.contains("Function parameter y not bound in call"))
460+
assert(ex.getMessage.contains("Function newParams parameter y not bound in call"))
461461
}
462462

463463
test("invalidParam") {
@@ -475,7 +475,7 @@ object EvaluatorTests extends TestSuite {
475475
)
476476
}
477477

478-
assert(ex.getMessage.contains("Function has no parameter hello"))
478+
assert(ex.getMessage.contains("Function Person has no parameter hello"))
479479
}
480480

481481
test("unknownVariable") {
@@ -866,6 +866,86 @@ object EvaluatorTests extends TestSuite {
866866
}
867867
}
868868

869+
test("builtinErrorMessageIncludesFunctionName") {
870+
// Issue #416: error messages for builtin functions should include the function name.
871+
872+
// Too many args
873+
test("tooManyArgs") {
874+
val ex = assertThrows[Exception] {
875+
eval("std.length([1], [2])", useNewEvaluator = useNewEvaluator)
876+
}
877+
assert(ex.getMessage.contains("Too many args, function length has"))
878+
}
879+
880+
// Parameter not bound in call (missing required arg)
881+
test("missingRequiredArg") {
882+
val ex = assertThrows[Exception] {
883+
eval("std.substr('hello')", useNewEvaluator = useNewEvaluator)
884+
}
885+
assert(ex.getMessage.contains("Function substr parameter"))
886+
assert(ex.getMessage.contains("not bound in call"))
887+
}
888+
889+
// Function has no parameter (invalid named arg)
890+
test("invalidNamedArg") {
891+
val ex = assertThrows[Exception] {
892+
eval("std.length(x=[1], noSuchParam=2)", useNewEvaluator = useNewEvaluator)
893+
}
894+
assert(ex.getMessage.contains("Function length has no parameter noSuchParam"))
895+
}
896+
897+
// Binding parameter a second time
898+
test("duplicateArg") {
899+
val ex = assertThrows[Exception] {
900+
eval("std.pow(2, x=3)", useNewEvaluator = useNewEvaluator)
901+
}
902+
assert(ex.getMessage.contains("binding parameter a second time: x in function pow"))
903+
}
904+
905+
// User-defined function should include function name from local binding
906+
test("userDefinedFunctionIncludesFunctionName") {
907+
val ex = assertThrows[Exception] {
908+
eval(
909+
"""local myFunc(x, y) = x + y;
910+
|myFunc("a")
911+
""".stripMargin,
912+
useNewEvaluator = useNewEvaluator
913+
)
914+
}
915+
assert(ex.getMessage.contains("Function myFunc parameter y not bound in call"))
916+
}
917+
918+
// User-defined function: too many args should include function name
919+
test("userDefinedTooManyArgs") {
920+
val ex = assertThrows[Exception] {
921+
eval(
922+
"""local add(x, y) = x + y;
923+
|add(1, 2, 3)
924+
""".stripMargin,
925+
useNewEvaluator = useNewEvaluator
926+
)
927+
}
928+
assert(ex.getMessage.contains("Too many args, function add has 2 parameter(s)"))
929+
}
930+
931+
// Anonymous function should NOT include function name in error message
932+
test("anonymousFunctionNoFunctionName") {
933+
val ex = assertThrows[Exception] {
934+
eval("(function(x, y) x + y)('a')", useNewEvaluator = useNewEvaluator)
935+
}
936+
// Should be exactly "Function parameter..." without a function name inserted
937+
assert(ex.getMessage.contains("Function parameter y not bound in call"))
938+
}
939+
940+
// Anonymous function: too many args should NOT include function name
941+
test("anonymousFunctionTooManyArgs") {
942+
val ex = assertThrows[Exception] {
943+
eval("(function(x) x)(1, 2)", useNewEvaluator = useNewEvaluator)
944+
}
945+
assert(ex.getMessage.contains("Too many args, function has 1 parameter(s)"))
946+
}
947+
}
948+
869949
}
870950
def tests: Tests = allTests(false).prefix("Evaluator") ++ allTests(true).prefix("NewEvaluator")
871951
}

0 commit comments

Comments
 (0)