Skip to content
Merged
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
21 changes: 21 additions & 0 deletions spec/jsonata-suite/baseline.lua
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ return {
["comparison-operators/deep-equals/7"] = true,
["comparison-operators/deep-equals/8"] = true,
["comparison-operators/deep-equals/9"] = true,
["conditionals/case000"] = true,
["conditionals/case001"] = true,
["conditionals/case002"] = true,
["conditionals/case003"] = true,
["conditionals/case004"] = true,
["conditionals/case005"] = true,
["conditionals/case006"] = true,
["conditionals/case007"] = true,
Expand Down Expand Up @@ -435,6 +439,10 @@ return {
["function-join/case011"] = true,
["function-keys/case000"] = true,
["function-keys/case001"] = true,
["function-keys/case002"] = true,
["function-keys/case004"] = true,
["function-keys/case005"] = true,
["function-keys/case006"] = true,
["function-length/case000"] = true,
["function-length/case001"] = true,
["function-length/case002"] = true,
Expand All @@ -454,6 +462,7 @@ return {
["function-lookup/case000"] = true,
["function-lookup/case001"] = true,
["function-lookup/case002"] = true,
["function-lookup/case003"] = true,
["function-lowercase/case000"] = true,
["function-lowercase/case001"] = true,
["function-max/case000"] = true,
Expand Down Expand Up @@ -645,8 +654,10 @@ return {
["function-split/case016"] = true,
["function-split/case017"] = true,
["function-split/case018"] = true,
["function-spread/case000"] = true,
["function-spread/case001"] = true,
["function-spread/case002"] = true,
["function-spread/case003"] = true,
["function-sqrt/case000"] = true,
["function-sqrt/case001"] = true,
["function-sqrt/case002"] = true,
Expand Down Expand Up @@ -830,6 +841,7 @@ return {
["missing-paths/case001"] = true,
["missing-paths/case002"] = true,
["missing-paths/case003"] = true,
["missing-paths/case004"] = true,
["missing-paths/case005"] = true,
["multiple-array-selectors/case000"] = true,
["multiple-array-selectors/case001"] = true,
Expand All @@ -853,7 +865,10 @@ return {
["numeric-operators/case009"] = true,
["numeric-operators/case010"] = true,
["numeric-operators/case011"] = true,
["numeric-operators/case012"] = true,
["numeric-operators/case013"] = true,
["numeric-operators/case015"] = true,
["numeric-operators/case016"] = true,
["numeric-operators/case017"] = true,
["numeric-operators/case018"] = true,
["object-constructor/case000"] = true,
Expand All @@ -868,12 +883,15 @@ return {
["object-constructor/case011"] = true,
["object-constructor/case012"] = true,
["object-constructor/case013"] = true,
["object-constructor/case014"] = true,
["object-constructor/case015"] = true,
["object-constructor/case016"] = true,
["object-constructor/case017"] = true,
["object-constructor/case018"] = true,
["object-constructor/case021"] = true,
["object-constructor/case022"] = true,
["object-constructor/case023"] = true,
["object-constructor/case024"] = true,
["object-constructor/case025"] = true,
["object-constructor/case026"] = true,
["parent-operator/errors/0"] = true,
Expand Down Expand Up @@ -929,6 +947,7 @@ return {
["partial-application/case002"] = true,
["partial-application/case003"] = true,
["partial-application/case004"] = true,
["performance/case000"] = true,
["predicates/case000"] = true,
["predicates/case001"] = true,
["predicates/case002"] = true,
Expand Down Expand Up @@ -976,6 +995,7 @@ return {
["simple-array-selectors/case011"] = true,
["simple-array-selectors/case012"] = true,
["simple-array-selectors/case013"] = true,
["simple-array-selectors/case014"] = true,
["simple-array-selectors/case015"] = true,
["simple-array-selectors/case016"] = true,
["simple-array-selectors/case017"] = true,
Expand Down Expand Up @@ -1151,6 +1171,7 @@ return {
["wildcards/case004"] = true,
["wildcards/case005"] = true,
["wildcards/case006"] = true,
["wildcards/case008"] = true,
["wildcards/case009"] = true,
["wildcards/case010/0"] = true,
["wildcards/case010/1"] = true,
Expand Down
116 changes: 116 additions & 0 deletions spec/m6a_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
local jsonata = require("jsonata")
local function run(src, input)
return jsonata.compile(src):evaluate(input)
end

describe("M6a C-1: $$ root context", function()
it("$$ resolves to the root input", function()
assert.are.same({ a = 1 }, run("$$", { a = 1 }))
end)
it("$$ reaches root from a nested path step", function()
assert.are.same({ a = { b = 2 } }, run("a.$$", { a = { b = 2 } }))
end)
it("$$.field indexes the root", function()
assert.are.equal(1, run("$$.a", { a = 1 }))
end)
it("$ (current context) still works", function()
assert.are.equal(5, run("a.$", { a = 5 }))
assert.are.same({ x = 1 }, run("$", { x = 1 }))
end)
end)

describe("M6a B-3: $keys/$spread empty & scalar", function()
it("$keys of an empty result is undefined, not []", function()
assert.is_nil(run("$keys(5)"))
assert.is_nil(run("$keys({})"))
end)
it("$keys still returns the keys of an object", function()
assert.are.same({ "a", "b" }, run('$keys({"a":1, "b":2})'))
end)
it("$spread echoes a scalar argument", function()
assert.are.equal(5, run("$spread(5)"))
assert.is_true(run("$spread(true)"))
end)
it("$spread still spreads an object", function()
assert.are.same({ { a = 1 } }, run('$spread({"a":1})'))
end)
end)

describe("M6a C-4a: undefined-operand arithmetic", function()
local function code(src, input)
local ok, err = pcall(run, src, input)
assert.is_false(ok)
return err.code
end
it("arithmetic with an undefined operand is undefined", function()
assert.is_nil(run("5 + nope", {}))
assert.is_nil(run("nope - 1", {}))
assert.is_nil(run("nope * 2", {}))
assert.is_nil(run("10 / nope", {}))
assert.is_nil(run("-nope", {}))
end)
it("normal arithmetic still works", function()
assert.are.equal(7, run("5 + 2"))
assert.are.equal(-3, run("-(1 + 2)"))
end)
it("a genuine non-number operand still raises a type error", function()
assert.are.equal("T2001", code("'x' + 5"))
assert.are.equal("T2002", code("5 + 'x'"))
end)
it("matches jsonata operand ordering (type error before undefined short-circuit)", function()
assert.are.equal("T2002", code("nope + 'x'", {})) -- undefined LHS, defined non-number RHS -> T2002
assert.are.equal("T2001", code("'x' + nope", {})) -- defined non-number LHS -> T2001
assert.is_nil(run("nope + 5", {})) -- undefined LHS, number RHS -> undefined
assert.is_nil(run("5 + nope", {})) -- number LHS, undefined RHS -> undefined
assert.are.equal("T2001", code("false + nope", {})) -- defined non-number LHS (case018) -> T2001
end)
end)

describe("M6a C-3: object-literal key rules", function()
local function code(src, input)
local ok, err = pcall(run, src, input)
assert.is_false(ok)
return err.code
end
it("an undefined key skips the whole pair", function()
assert.are.same({}, run("{nope: 1}", {}))
end)
it("an undefined value omits the pair", function()
assert.are.same({}, run('{"a": nope}', {}))
end)
it("a non-string key raises T1003", function()
assert.are.equal("T1003", code('{1: "x"}'))
end)
it("a duplicate key raises D1009", function()
assert.are.equal("D1009", code('{"a":1, "a":2}'))
end)
it("a normal object literal still builds correctly", function()
assert.are.equal(2, run('{"a":1, "b":2}.b', {}))
end)
end)

describe("M6a adversarial fixes", function()
local function code(src, input)
local ok, err = pcall(run, src, input)
assert.is_false(ok)
return err.code
end

it("$spread of an empty object/array is undefined", function()
assert.is_nil(run("$spread({})"))
assert.is_nil(run("$spread([])"))
end)

it("a duplicate object key with an undefined value still raises D1009", function()
assert.are.equal("D1009", code('{"a":1, "a":nope}', {}))
assert.are.equal("D1009", code('{"a":nope, "a":2}', {}))
assert.are.equal("D1009", code('{"a":nope, "a":nope}', {}))
end)

it("unary minus on a non-number raises D1002", function()
assert.are.equal("D1002", code("-'x'"))
assert.are.equal("D1002", code("-true"))
assert.is_nil(run("-nope", {})) -- undefined still propagates
assert.are.equal(-5, run("-5")) -- normal still works
end)
end)
1 change: 1 addition & 0 deletions src/jsonata/errors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ local MESSAGES = {
T2012 = "The delete clause of the transform expression must evaluate to an array of strings",
-- Dynamic / runtime errors
D1001 = "Number out of range to be formatted",
D1002 = "Cannot negate a non-numeric value: {{value}}",
D1009 = "Multiple key definitions evaluate to same key: {{value}}",
D2014 = "The size of the sequence allocated by the range operator (..) must not exceed 1e7. Attempted to allocate {{value}}.",
D3001 = "Unsupported in M1",
Expand Down
39 changes: 33 additions & 6 deletions src/jsonata/evaluator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,17 @@ local function eval_binary(node, input, env)
end

if op == "+" or op == "-" or op == "*" or op == "/" or op == "%" then
local a = as_number(lhs, "T2001")
local b = as_number(rhs, "T2002")
if not V.is_nothing(lhs) and V.typeof(lhs) ~= "number" then
errors.raise("T2001", { value = lhs })
end
if not V.is_nothing(rhs) and V.typeof(rhs) ~= "number" then
errors.raise("T2002", { value = rhs })
end
if V.is_nothing(lhs) or V.is_nothing(rhs) then
return V.NOTHING
end
local a = lhs
local b = rhs
if op == "+" then
return a + b
elseif op == "-" then
Expand Down Expand Up @@ -696,7 +705,14 @@ local function _evaluate(node, input, env)
return V.NULL
elseif t == "unary" then
if node.value == "-" then
return -as_number(evaluate(node.expression, input, env), "T2001")
local v = evaluate(node.expression, input, env)
if V.is_nothing(v) then
return V.NOTHING
end
if V.typeof(v) ~= "number" then
errors.raise("D1002", { value = v })
end
return -v
end
errors.raise("S0211", { token = node.value })
elseif t == "binary" then
Expand Down Expand Up @@ -765,11 +781,22 @@ local function _evaluate(node, input, env)
return arr
elseif t == "object" then
local obj = V.object()
local seen = {}
for _, pair in ipairs(node.pairs) do
local k = evaluate(pair[1], input, env)
local val = evaluate(pair[2], input, env)
local kstr = V.is_nothing(k) and "" or functions.string.impl(k)
V.obj_set(obj, kstr, val)
if not V.is_nothing(k) then -- undefined key: skip the whole pair
if V.typeof(k) ~= "string" then
errors.raise("T1003", { value = k })
end
if seen[k] then -- duplicate key (regardless of value)
errors.raise("D1009", { value = k })
end
seen[k] = true
local val = evaluate(pair[2], input, env)
if not V.is_nothing(val) then -- undefined value: omit the pair
V.obj_set(obj, k, val)
end
end
end
return obj
elseif t == "function" then
Expand Down
8 changes: 8 additions & 0 deletions src/jsonata/functions/object.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ R.keys = H.def(function(x)
end
end
end
if #out == 0 then
return V.NOTHING
end
return out
end, 1, 1, "<x-:a<s>>")

Expand Down Expand Up @@ -75,6 +78,11 @@ R.spread = H.def(function(x)
spread_obj(x[i])
end
end
else
return x -- scalar: jsonata functionSpread echoes the argument unchanged
end
if #out == 0 then
return V.NOTHING
end
return out
end, 1, 1, "<x-:a<o>>")
Expand Down
1 change: 1 addition & 0 deletions src/jsonata/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function Expression:evaluate(input, bindings)
env:bind("__explain_hook", self._explain_hook)
end
local internal_input = adapter.from_lua(input)
env:bind("$", internal_input)
local result = Evaluator.evaluate(self.ast, internal_input, env)
return adapter.to_lua(result)
end
Expand Down
Loading