diff --git a/assert-gooder.lua b/assert-gooder.lua index a7b7bfc..e91bba9 100644 --- a/assert-gooder.lua +++ b/assert-gooder.lua @@ -39,6 +39,41 @@ local function get_value_token (token) assert(false) end +local function get_variable (var_name, info) + -- + assert(type(var_name) == 'string') + assert(type(info) == 'table') + + -- Local + if info.locals[var_name] then + local var_info = info.locals[var_name] + return var_info[1], var_info[2] and ('argument #'..var_info[3]) or 'local', info.name or var_info[2] and '' + end + + -- Up-value + local index = 0 + repeat + index = index + 1 + local name, val = debug.getupvalue(info.func, index) + if name == var_name then return val, 'upvalue' end + until not name + + -- Global + return getfenv(info.func)[var_name], 'global' +end + +local function get_value_from_lvalue (lvalue, info) + assert(type(lvalue) == 'table') + assert(type(info) == 'table') + + -- Base value + local value, var_scope, in_func = get_variable(lvalue[1], info) + -- Sub value + for i = 2, #lvalue do value = value[lvalue[i].value] end + -- + return value, var_scope, in_func +end + -------------------------------------------------------------------------------- -- Parsing @@ -51,15 +86,15 @@ local function parse (tokens) return { exp = 'COMPARE', binop = 'EQ', - left = { exp = 'CALL', callee = get_value_token(tokens[1]), get_value_token(tokens[3]) }, - right = get_value_token(tokens[6]), + [1] = { exp = 'CALL', get_value_token(tokens[1]), get_value_token(tokens[3]) }, + [2] = get_value_token(tokens[6]), } elseif #tokens == 3 and VALUE_TOKEN[tokens[1].token] and COMPARE_BINOP[tokens[2].token] and VALUE_TOKEN[tokens[3].token] then return { exp = 'COMPARE', binop = tokens[2].token, - left = get_value_token(tokens[1]), - right = get_value_token(tokens[3]) + [1] = get_value_token(tokens[1]), + [2] = get_value_token(tokens[3]) } elseif #tokens == 3 and tokens[1].token == 'IDENTIFIER' and tokens[2].token == 'DOT' and tokens[3].token == 'IDENTIFIER' then return { exp = 'LVALUE', tokens[1].text, { exp = 'STRING', value = tokens[3].text } } @@ -72,8 +107,13 @@ local function parse (tokens) end end -local function populate_ast_with_semantics (node, env) - +local function populate_ast_with_semantics (node, info) + if type(node) ~= 'table' then return end + for i = 1, #node do populate_ast_with_semantics(node[i], info) end + -- + if node.exp == 'LVALUE' then + node.value = get_value_from_lvalue(node, info) + end end -------------------------------------------------------------------------------- @@ -112,29 +152,6 @@ local function get_assert_body (call_info) return lexer:lex(text), text end -local function get_variable (var_name, info) - -- - assert(type(var_name) == 'string') - assert(type(info) == 'table') - - -- Local - if info.locals[var_name] then - local var_info = info.locals[var_name] - return var_info[1], var_info[2] and ('argument #'..var_info[3]) or 'local', info.name or var_info[2] and '' - end - - -- Up-value - local index = 0 - repeat - index = index + 1 - local name, val = debug.getupvalue(info.func, index) - if name == var_name then return val, 'upvalue' end - until not name - - -- Global - return getfenv(info.func)[var_name], 'global' -end - local function get_function_name (call_info) -- if call_info.name then return string.format('\'%s\'', call_info.name) end @@ -170,10 +187,7 @@ end local function get_variable_and_prefix (lvalue, call_info) assert(type(lvalue) == 'table' and lvalue.exp == 'LVALUE') -- - local base_value, var_scope, in_func = get_variable(lvalue[1], call_info) - -- Determine value of variable - local value = base_value - for i = 2, #lvalue do value = value[lvalue[i].value] end + local value, var_scope, in_func = get_value_from_lvalue(lvalue, call_info) -- local func_name = in_func and (' to '..get_function_name(call_info)) or '' return value, ('bad %s%s'):format(fmt_lvalue(lvalue, var_scope), func_name) @@ -189,9 +203,10 @@ local COMPLEX_TYPES = { ['table'] = true, ['userdata'] = true, ['cdata'] = true, + ['function'] = true, } -local function fmt_with_type (val) +local function fmt_val_with_type (val) -- Primitive values ARE their type, and don't need the annotation. if PRIMITIVE_VALUES[type(val)] then return tostring(val) end -- Complex types are already formatted with some type information. @@ -205,25 +220,26 @@ end local function determine_error_message (call_info, msg, condition) local tokens, body_text = get_assert_body(call_info) local ast = parse(tokens) + populate_ast_with_semantics(ast, call_info) msg[1] = ('expression `%s` evaluated to %s'):format(body_text, condition) local var_prefix = function(token) return get_variable_and_prefix(token, call_info) end if not ast then return nil - elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' and ast.left.exp == 'CALL' and ast.left.callee.exp == 'LVALUE' and ast.left.callee[1] == 'type' then - local gotten_val, prefix = var_prefix(ast.left[1]) - msg[1] = ('%s (%s expected, but got %s: %s)'):format(prefix, ast.right.value, type(gotten_val), fmt_val(gotten_val)) + elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' and ast[1].exp == 'CALL' and ast[1][1].exp == 'LVALUE' and ast[1][1][1] == 'type' then + local gotten_val, prefix = var_prefix(ast[1][2]) + msg[1] = ('%s (%s expected, but got %s)'):format(prefix, ast[2].value, fmt_val_with_type(gotten_val)) elseif ast.exp == 'COMPARE' and ast.binop == 'EQ' then - local gotten_value, prefix = var_prefix(ast.left) - local expected_value = ast.right.value - local type_annotation = (type(expected_value) == type(gotten_value)) and '' or (' '..type(gotten_value)) - msg[1] = ('%s (%s expected, but got%s: %s)'):format(prefix, fmt_with_type(expected_value), type_annotation, fmt_val(gotten_value)) + local gotten_value, prefix = var_prefix(ast[1]) + local expected_value = ast[2].value + local fmt_gotten = (type(expected_value) == type(gotten_value)) and fmt_val or fmt_val_with_type + msg[1] = ('%s (%s expected, but got %s)'):format(prefix, fmt_val_with_type(expected_value), fmt_gotten(gotten_value)) elseif ast.exp == 'COMPARE' and ast.binop == 'NEQ' then - local gotten_val, prefix = var_prefix(ast.left) - local expected_value = ast.right.value - msg[1] = ('%s (expected anything other than %s, but got %s)'):format(prefix, fmt_with_type(expected_value), fmt_val(gotten_val)) + local gotten_val, prefix = var_prefix(ast[1]) + local expected_value = ast[2].value + msg[1] = ('%s (expected anything other than %s, but got %s)'):format(prefix, fmt_val_with_type(expected_value), fmt_val(gotten_val)) elseif ast.exp == 'LVALUE' then local gotten_val, prefix = var_prefix(ast) diff --git a/test/test_assert-gooder.lua b/test/test_assert-gooder.lua index 8790d9a..f349868 100644 --- a/test/test_assert-gooder.lua +++ b/test/test_assert-gooder.lua @@ -11,7 +11,7 @@ SUITE:addTest('local variable', function () local a = 2 assert(type(a) == 'string') end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (string expected, but got number 2)', msg) end) SUITE:addTest('upvalue variable', function () @@ -19,7 +19,7 @@ SUITE:addTest('upvalue variable', function () local _, msg = pcall(function () assert(type(a) == 'string') end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad upvalue \'a\' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad upvalue \'a\' (string expected, but got number 2)', msg) end) SUITE:addTest('global variable', function () @@ -27,7 +27,7 @@ SUITE:addTest('global variable', function () local _, msg = pcall(function () assert(type(a) == 'string') end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad global \'a\' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad global \'a\' (string expected, but got number 2)', msg) end) SUITE:addTest('argument to anon function', function () @@ -36,7 +36,7 @@ SUITE:addTest('argument to anon function', function () assert(type(a) == 'string') end)(2) end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-3)..': '..'assertion failed! bad argument #1 \'a\' to the anonymous function at ./test/test_assert-gooder.lua:'..curline(-4)..' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-3)..': '..'assertion failed! bad argument #1 \'a\' to the anonymous function at ./test/test_assert-gooder.lua:'..curline(-4)..' (string expected, but got number 2)', msg) end) SUITE:addTest('argument to named function', function () @@ -44,7 +44,7 @@ SUITE:addTest('argument to named function', function () assert(type(a) == 'string') end local _, msg = pcall(function () f(2) end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-3)..': '..'assertion failed! bad argument #1 \'a\' to \'f\' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-3)..': '..'assertion failed! bad argument #1 \'a\' to \'f\' (string expected, but got number 2)', msg) end) @@ -53,7 +53,7 @@ SUITE:addTest('indexing', function () local a = { b = 39 } assert(type(a.b) == 'string') end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad key "b" in local \'a\' (string expected, but got number: 39)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad key "b" in local \'a\' (string expected, but got number 39)', msg) end) SUITE:addTest('subscript constant', function () @@ -61,7 +61,7 @@ SUITE:addTest('subscript constant', function () local a = { 4, 2, 3, 6 } assert(type(a.b) == 'string') end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad key 2 in local \'a\' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad key 2 in local \'a\' (string expected, but got number 2)', msg) end) SUITE:addTest('subscript variable', function () @@ -69,7 +69,7 @@ SUITE:addTest('subscript variable', function () local a, i = { 4, 2, 3, 6 }, 2 assert(type(a.b) == 'string') end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad key 2 in local \'a\' (string expected, but got number: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad key 2 in local \'a\' (string expected, but got number 2)', msg) end) -------------------------------------------------------------------------------- @@ -77,7 +77,7 @@ end) SUITE:addTest('can improve asserts in loaded strings too', function () local func = loadstring "return function(a) assert(type(a) == 'string') end" () local _, msg = pcall(setfenv(func, getfenv()), 42) - assert_equal('[string "return function(a) assert(type(a) == \'string\'..."]:1: '..'assertion failed! bad argument #1 \'a\' to the anonymous function from loaded string (string expected, but got number: 42)', msg) + assert_equal('[string "return function(a) assert(type(a) == \'string\'..."]:1: '..'assertion failed! bad argument #1 \'a\' to the anonymous function from loaded string (string expected, but got number 42)', msg) end) SUITE:addTest('really complicated expression', function () @@ -96,7 +96,7 @@ SUITE:addTest('compare values', function () local a = 2 assert(a == 4) end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (number 4 expected, but got: 2)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (number 4 expected, but got 2)', msg) end) SUITE:addTest('compare values across types', function () @@ -104,7 +104,7 @@ SUITE:addTest('compare values across types', function () local a = "hi" assert(a == 4) end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (number 4 expected, but got string: "hi")', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (number 4 expected, but got string "hi")', msg) end) SUITE:addTest('is nil', function () @@ -112,7 +112,7 @@ SUITE:addTest('is nil', function () local a = "hi" assert(a == nil) end) - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (nil expected, but got string: "hi")', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (nil expected, but got string "hi")', msg) end) SUITE:addTest('truthy', function () @@ -175,12 +175,11 @@ SUITE:addTest('constant nil', function () end) SUITE:addTest('function as type argument', function () + local f = function() end local _, msg = pcall(function () - local f = function() end assert(type(f) == 'string') end) - -- TODO: How do we test this? - assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'f\' (string expected, but got function)', msg) + assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad upvalue \'f\' (string expected, but got '..tostring(f)..')', msg) end) --------------------------------------------------------------------------------