use hermes_ast::context::{Context, GCLock};
use hermes_ast::node::Node;
use hermes_parser::js::JSParserImpl;
use hermes_parser::lexer::{GrammarContext, JSLexer};
use hermes_sema::ids::FunctionInfoId;
use hermes_sema::keywords::Keywords;
use hermes_sema::resolve::{resolve_ast, resolve_ast_for_parser};
use hermes_sema::sem_context::SemContext;
use hermes_support::manager::SourceErrorManager;
fn parse<'gc>(
gc: &'gc GCLock,
sm: &mut SourceErrorManager,
src: &str,
) -> &'gc Node<'gc> {
let buf_id = sm.add_buffer_bytes("input", src.as_bytes());
let result: Option<&Node> = {
let atoms = &gc.ctx().atom_table;
let lexer =
JSLexer::new(buf_id, sm, atoms, GrammarContext::AllowRegExp);
let mut parser = JSParserImpl::new(gc, lexer);
parser.parse()
};
assert_eq!(sm.error_count(), 0, "unexpected parse errors in: {src}");
result.expect("parser returned no Program")
}
fn flags_and_errors(src: &str) -> (Vec<bool>, u32) {
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
let _resolved = resolve_ast(&gc, &mut sem_ctx, &mut sm, root, &[]);
let flags = (0..sem_ctx.functions_len())
.map(|i| {
let id = FunctionInfoId::from_sema_id(hermes_ast::SemaId(i as u32));
sem_ctx.function(id).may_reach_implicit_return
})
.collect();
(flags, sm.error_count())
}
fn flags(src: &str) -> Vec<bool> {
let (flags, errors) = flags_and_errors(src);
assert_eq!(errors, 0, "unexpected resolution errors in: {src}");
flags
}
fn flag(src: &str) -> bool {
let flags = flags(src);
assert!(flags.len() >= 2, "no function in: {src}");
flags[1]
}
fn body_flag(body: &str) -> bool {
flag(&format!("var x, y, o;\nfunction f() {{\n{body}\n}}\n"))
}
fn body_flag_parser_entry(body: &str) -> bool {
let src = format!("var x, y, o;\nfunction f() {{\n{body}\n}}\n");
let mut ctx = Context::new();
let gc = ctx.lock();
let mut sm = SourceErrorManager::new();
let root = parse(&gc, &mut sm, &src);
let mut sem_ctx = SemContext::new(Keywords::new(&gc));
resolve_ast_for_parser(&gc, &mut sem_ctx, &mut sm, root);
assert_eq!(sm.error_count(), 0, "unexpected resolution errors in: {src}");
assert!(sem_ctx.functions_len() >= 2, "no function in: {src}");
let id = FunctionInfoId::from_sema_id(hermes_ast::SemaId(1));
sem_ctx.function(id).may_reach_implicit_return
}
#[track_caller]
fn check_bodies(rows: &[(&str, bool)]) {
for (body, expected) in rows {
assert_eq!(
body_flag(body),
*expected,
"mayReachImplicitReturn of `function f() {{ {body} }}`"
);
}
}
#[test]
fn statement_lists_fall_off_the_end() {
check_bodies(&[
("", true),
("x;", true),
(";", true),
("debugger;", true),
("{ x; }", true),
("{ }", true),
]);
}
#[test]
fn return_and_throw_terminate_the_list() {
check_bodies(&[
("return;", false),
("return 1;", false),
("throw x;", false),
("return 1; x;", false),
("throw x; x;", false),
("{ return 1; }", false),
("x; { x; throw x; }", false),
]);
}
#[test]
fn non_statement_children_of_a_block_just_continue() {
check_bodies(&[
("var v = 1;", true),
("let v = 1;", true),
("function g() { return 1; }", true),
("class C {}", true),
("var v = 1; return 1;", false),
]);
}
#[test]
fn if_statement_unions_its_branches() {
check_bodies(&[
("if (x) return 1; else return 2;", false),
("if (x) { return 1; } else { throw x; }", false),
(
"if (x) { return 1; } else { if (y) return 2; else return 3; }",
false,
),
("if (x) return 1;", true),
("if (x) return 1; else x;", true),
("if (x) x; else return 1;", true),
("if (x) return 1; else return 2; x;", false),
("if (x) return 1; return 2;", false),
]);
}
#[test]
fn precondition_loops_always_continue() {
check_bodies(&[
("while (x) return 1;", true),
("while (true) { }", true),
("while (true) { return 1; }", true),
("while (true) { throw x; }", true),
("for (;;) return 1;", true),
("for (var k = 0; k < 10; ++k) return 1;", true),
("for (var k in o) return 1;", true),
("for (var k of o) return 1;", true),
]);
}
#[test]
fn do_while_must_run_its_body() {
check_bodies(&[
("do return 1; while (x);", false),
("do { throw x; } while (x);", false),
("do { } while (x);", true),
("do { if (x) return 1; } while (x);", true),
("do { break; } while (x);", true),
("do { continue; } while (x);", true),
("do return 1; while (x); x;", false),
]);
}
#[test]
fn breaks_targeting_an_outer_statement_are_not_erased_by_the_inner_one() {
check_bodies(&[
("outer: do { do { break outer; } while (y); } while (x);", true),
("outer: do { do { return 1; } while (y); } while (x);", false),
(
"outer: do { do { continue outer; } while (y); } while (x);",
true,
),
("outer: do { break outer; } while (x); return 1;", false),
(
"outer: do { do { continue outer; } while (y); } while (x); \
return 1;",
false,
),
]);
}
#[test]
fn labeled_statement_body_must_execute() {
check_bodies(&[
("L: { return 1; }", false),
("L: { throw x; }", false),
("L: { }", true),
("L: { break L; }", true),
("L: { if (x) break L; return 1; }", true),
("L: { break L; } return 1;", false),
("L: L2: { break L; } return 1;", false),
]);
}
#[test]
fn switch_without_default_may_be_skipped() {
check_bodies(&[
("switch (x) { }", true),
("switch (x) { case 1: return 1; }", true),
("switch (x) { case 1: return 1; case 2: return 2; }", true),
]);
}
#[test]
fn switch_with_exhaustive_default_terminates() {
check_bodies(&[
("switch (x) { default: return 1; }", false),
("switch (x) { case 1: return 1; default: return 2; }", false),
("switch (x) { default: return 1; case 1: return 2; }", false),
("switch (x) { default: throw x; }", false),
("switch (x) { default: }", true),
("switch (x) { case 1: default: return 1; }", false),
("switch (x) { default: case 1: }", true),
]);
}
#[test]
fn switch_break_makes_the_switch_completable() {
check_bodies(&[
("switch (x) { default: break; }", true),
("switch (x) { case 1: break; default: return 1; }", true),
("switch (x) { default: if (x) break; return 1; }", true),
("switch (x) { default: break; } return 1;", false),
(
"switch (x) { default: switch (y) { default: break; } return 1; }",
false,
),
]);
}
#[test]
fn try_catch_unions_both_bodies() {
check_bodies(&[
("try { return 1; } catch (e) { return 2; }", false),
("try { throw x; } catch (e) { throw x; }", false),
("try { return 1; } catch (e) { }", true),
("try { } catch (e) { return 1; }", true),
("try { } catch (e) { }", true),
("try { return 1; } catch { return 2; }", false),
]);
}
#[test]
fn try_finally_is_decided_by_the_finalizer_first() {
check_bodies(&[
("try { } finally { return 1; }", false),
("try { x; } finally { throw x; }", false),
("try { return 1; } finally { }", false),
("try { return 1; } finally { x; }", false),
("try { } finally { }", true),
("try { if (x) return 1; } finally { }", true),
]);
}
#[test]
fn a_finalizer_that_breaks_out_defeats_the_terminating_try() {
check_bodies(&[
("L: try { return 1; } finally { break L; }", true),
("L: try { return 1; } finally { }", false),
("L: try { return 1; } finally { break L; } return 2;", false),
]);
}
const TRY_CATCH_FINALLY_SHAPES: &[(&str, bool)] = &[
(
"try { return 1; } catch (e) { return 2; } finally { return 3; }",
false,
),
("try { return 1; } catch (e) { return 2; } finally { }", false),
("try { return 1; } catch (e) { } finally { }", true),
("try { } catch (e) { } finally { }", true),
("try { x; } catch (e) { x; } finally { return 1; }", false),
(
"L: { try { return 1; } catch (e) { return 2; } finally { break L; } }",
true,
),
(
"L: { try { return 1; } catch (e) { return 2; } \
finally { if (x) break L; } }",
true,
),
];
#[test]
fn try_catch_finally_is_analyzed_after_the_resolver_rewrite() {
check_bodies(TRY_CATCH_FINALLY_SHAPES);
}
#[test]
fn try_catch_finally_gives_the_same_answers_unsplit() {
for (body, expected) in TRY_CATCH_FINALLY_SHAPES {
assert_eq!(
body_flag_parser_entry(body),
*expected,
"parser-entry mayReachImplicitReturn of \
`function f() {{ {body} }}`"
);
}
}
#[test]
fn the_flag_is_per_function() {
let f = flags(
"function a() { return 1; }\n\
function b() { }\n\
function c() { function d() { } return 1; }\n",
);
assert_eq!(f, vec![true, false, true, false, true]);
}
#[test]
fn the_program_node_keeps_the_default() {
assert!(flags("x;")[0]);
assert!(flags("var x;\nfunction f() { return 1; }\n")[0]);
}
#[test]
fn arrow_functions_use_the_rewritten_block_body() {
assert!(!flag("var f = x => x;"));
assert!(!flag("var f = () => { return 1; };"));
assert!(flag("var f = () => { };"));
assert!(flag("var f = () => { if (0) return 1; };"));
}
#[test]
fn class_methods_and_accessors_get_the_flag() {
assert!(!flag("class C { m() { return 1; } }"));
assert!(flag("class C { m() { } }"));
assert!(!flag("var o2 = { get p() { return 1; } };"));
}
#[test]
fn generators_and_async_functions_are_not_special_cased() {
assert!(flag("function* g() { yield 1; }"));
assert!(!flag("function* g() { yield 1; return 2; }"));
assert!(flag("async function g() { }"));
}
#[test]
fn the_flag_is_not_computed_when_resolution_failed() {
let (f, errors) = flags_and_errors("function f() { break; return 1; }");
assert_eq!(errors, 1, "expected the 'break' to be rejected");
assert!(f[1], "the flag must keep SemContext.h:354's default");
}
#[test]
fn with_statements_never_reach_the_analysis() {
let (f, errors) = flags_and_errors("function f() { with (x) return 1; }");
assert!(errors > 0, "expected `with` to be rejected");
assert!(f[1], "the flag must keep SemContext.h:354's default");
}