diff --git a/src/ast_utils.js b/src/ast_utils.js index 918c503..7b20b40 100644 --- a/src/ast_utils.js +++ b/src/ast_utils.js @@ -145,20 +145,30 @@ export const find_node = (node, pred) => { ) } +// TODO refactor, have explicit information if node is error origin, without +// guessing. See also color.js +// TODO check if return result is null and throw early export const find_error_origin_node = node => find_node( + // TODO do not go inside function_expr node, n => n.result != null && !n.result.ok && ( n.result.error != null || - // In case if throw null or throw undefined - n.type == 'throw' - || - // await can also throw null - n.type == 'unary' && n.operator == 'await' - // or function call throwing null or undefined - || - n.type == 'function_call' + // node has no error, but its children also have no error, so this node + // is error origin + n.children.find(c => find_error_origin_node(c) != null) == null + && + ( + // In case if throw null or throw undefined + n.type == 'throw' + || + // await can also throw null + n.type == 'unary' && n.operator == 'await' + // or function call throwing null or undefined + || + n.type == 'function_call' + ) ) ) diff --git a/src/color.js b/src/color.js index aa00200..ec8e78c 100644 --- a/src/color.js +++ b/src/color.js @@ -13,6 +13,8 @@ const node_to_color = node => ({ ? null : node.result.ok ? {ok: true} + // node.result.error may be null, for example if throw null + // See find_error_origin_node : node.result.error == null ? {ok: false, error_origin: false} : {ok: false, error_origin: true} diff --git a/src/eval.js b/src/eval.js index 4721c4d..76ab8c8 100644 --- a/src/eval.js +++ b/src/eval.js @@ -171,7 +171,7 @@ const codegen = (node, node_cxt, parent) => { } else if(node.type == 'object_literal'){ const elements = node.elements.map(el => { - if(el.type == 'spread'){ + if(el.type == 'object_spread'){ return do_codegen(el) } else if(el.type == 'identifier') { return el.value @@ -241,7 +241,7 @@ const codegen = (node, node_cxt, parent) => { + node.operator + ' ' + do_codegen(node.args[1]) - } else if(node.type == 'spread'){ + } else if(node.type == 'array_spread' || node.type == 'object_spread'){ return '...(' + do_codegen(node.expr) + ')' } else if(node.type == 'new') { const args = `[${node.args.children.map(do_codegen).join(',')}]` @@ -510,8 +510,24 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { // TODO exprs inside backtick string // Pass scope for backtick string return {...eval_codestring(node.value, scope), calls: callsleft} + } else if(node.type == 'array_spread') { + const result = eval_children(node, scope, callsleft, context) + if(!result.ok) { + return result + } + const child = result.children[0] + if((typeof(child.result.value?.[Symbol.iterator])) == 'function') { + return result + } else { + return { + ok: false, + children: result.children, + calls: result.calls, + error: new TypeError(child.string + ' is not iterable'), + } + } } else if([ - 'spread', + 'object_spread', 'key_value_pair', 'computed_property' ].includes(node.type)) { @@ -523,8 +539,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { } const value = children.reduce( (arr, el) => { - if(el.type == 'spread') { - // TODO check if iterable and throw error + if(el.type == 'array_spread') { return [...arr, ...el.children[0].result.value] } else { return [...arr, el.result.value] @@ -540,7 +555,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => { } const value = children.reduce( (value, el) => { - if(el.type == 'spread'){ + if(el.type == 'object_spread'){ return {...value, ...el.children[0].result.value} } else if(el.type == 'identifier') { // TODO check that it works diff --git a/src/parse_js.js b/src/parse_js.js index 1559d0b..426e389 100644 --- a/src/parse_js.js +++ b/src/parse_js.js @@ -828,7 +828,12 @@ const array_element = either( literal('...'), cxt => expr(cxt), ]), - ({value, ...node}) => ({...node, type: 'spread', not_evaluatable: true, children: [value]}) + ({value, ...node}) => ({ + ...node, + type: 'array_spread', + not_evaluatable: true, + children: [value] + }) ), cxt => expr(cxt), ) @@ -857,7 +862,12 @@ const object_literal = literal('...'), cxt => expr(cxt), ]), - ({value, ...node}) => ({...node, type: 'spread', children: [value], not_evaluatable: true}) + ({value, ...node}) => ({ + ...node, + type: 'object_spread', + children: [value], + not_evaluatable: true + }) ), // Or key-value pair @@ -1456,7 +1466,7 @@ const update_children_not_rec = (node, children = node.children) => { } } else if(node.type == 'call_args') { return node - } else if(node.type == 'spread') { + } else if(node.type == 'array_spread' || node.type == 'object_spread') { return {...node, expr: children[0], } diff --git a/test/test.js b/test/test.js index 81505d8..3521cf1 100644 --- a/test/test.js +++ b/test/test.js @@ -837,6 +837,23 @@ export const tests = [ ) }), + test('array spread not iterable', () => { + assert_code_error( + `[...null]`, + new Error('null is not iterable'), + ) + }), + + test('args spread not iterable', () => { + assert_code_error( + ` + function x() {} + x(...null) + `, + new Error('null is not iterable'), + ) + }), + test('module not found', () => { const parsed = parse_modules( 'a', @@ -2336,6 +2353,20 @@ const y = x()` assert_equal(s2.value_explorer.result.error.message, 'boom') }), + test('move_cursor error in fn args bug', () => { + const code = ` + function x() {} + x(null.foo) + ` + const i = test_initial_state(code) + + const m = COMMANDS.move_cursor(i, code.indexOf('x(null')) + assert_equal( + m.value_explorer.result.error, + new Error("Cannot read properties of null (reading 'foo')") + ) + }), + test('frame follows cursor toplevel', () => { const code = ` const x = () => { diff --git a/test/utils.js b/test/utils.js index 5881d94..61da4b5 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,3 +1,4 @@ +import {find_error_origin_node} from '../src/ast_utils.js' import {parse, print_debug_node, load_modules} from '../src/parse_js.js' import {eval_modules} from '../src/eval.js' import {active_frame, pp_calltree} from '../src/calltree.js' @@ -81,9 +82,8 @@ export const assert_code_evals_to = (codestring, expected) => { export const assert_code_error = (codestring, error) => { const state = test_initial_state(codestring) const frame = active_frame(state) - const result = frame.children.at(-1).result - assert_equal(result.ok, false) - assert_equal(result.error, error) + assert_equal(frame.result.ok, false) + assert_equal(find_error_origin_node(frame).result.error, error) } export const assert_code_evals_to_async = async (codestring, expected) => { @@ -168,9 +168,10 @@ export const test_deferred_calls_state = code => { export const stringify = val => JSON.stringify(val, (key, value) => { - // TODO do not use instanceof because currently not implemented in parser - if(value?.constructor == Set){ + if(value instanceof Set){ return [...value] + } else if(value instanceof Error) { + return {message: value.message} } else { return value }