use bumpalo::Bump;
use hermes_ast::context::Context;
use hermes_atom_table::AtomTable;
use hermes_parser::js::JSParserImpl;
use hermes_parser::json::{JSONFactory, JSONParser};
use hermes_parser::lexer::{GrammarContext, JSLexer};
use hermes_support::diag::CollectingHandler;
use hermes_support::manager::SourceErrorManager;
use hermes_support::render::render_diagnostic;
enum Dialect {
Jsx,
FlowMatch,
}
fn render_parse_errors(dialect: Dialect, name: &str, src: &str) -> String {
let mut sm = SourceErrorManager::new();
let opts = sm.output_options();
sm.set_handler(Box::new(CollectingHandler::new()));
let buf_id = sm.add_buffer(name, src);
let mut ctx = Context::new();
match dialect {
Dialect::Jsx => ctx.set_parse_jsx(true),
Dialect::FlowMatch => {
ctx.set_parse_flow(true);
ctx.set_parse_flow_ambiguous(true);
ctx.set_parse_flow_match(true);
}
}
let gc = ctx.lock();
{
let atoms = &gc.ctx().atom_table;
let lexer =
JSLexer::new(buf_id, &mut sm, atoms, GrammarContext::AllowRegExp);
let mut parser = JSParserImpl::new(&gc, lexer);
assert!(parser.parse().is_none(), "`{src}` must fail to parse");
}
sm.handler_as::<CollectingHandler>()
.expect("CollectingHandler installed above")
.messages()
.iter()
.map(|d| render_diagnostic(d, &opts))
.collect()
}
#[test]
fn jsx_member_expression_attribute_name_is_rejected() {
assert_eq!(
render_parse_errors(
Dialect::Jsx,
"jsx-error-attr-member.js",
"<foo a.b=\"1\"></foo>\n"
),
"jsx-error-attr-member.js:1:6: error: unexpected member expression\n\
<foo a.b=\"1\"></foo>\n \
^~~\n"
);
}
#[test]
fn match_binding_pattern_without_identifier_recovers_cleanly() {
assert_eq!(
render_parse_errors(
Dialect::FlowMatch,
"pattern-binding-error.js",
"const e = match (x) { const [y]: 2 };\n"
),
"pattern-binding-error.js:1:29: error: 'identifier' expected in \
match binding pattern\n\
const e = match (x) { const [y]: 2 };\n \
~~~~~~^\n"
);
}
#[test]
fn match_object_property_with_unparseable_value_recovers_cleanly() {
assert_eq!(
render_parse_errors(
Dialect::FlowMatch,
"pattern-object-value-error.js",
"const e = match (x) { {a: *}: 2 };\n"
),
"pattern-object-value-error.js:1:27: error: invalid match pattern\n\
const e = match (x) { {a: *}: 2 };\n \
^\n"
);
assert_eq!(
render_parse_errors(
Dialect::FlowMatch,
"pattern-object-binding-error.js",
"const e = match (x) { {a: const [y]}: 2 };\n"
),
"pattern-object-binding-error.js:1:33: error: 'identifier' expected \
in match binding pattern\n\
const e = match (x) { {a: const [y]}: 2 };\n \
~~~~~~^\n"
);
}
fn parse_json(src: &str) -> (bool, Vec<String>) {
let arena = Bump::new();
let atoms = AtomTable::new();
let mut sm = SourceErrorManager::new();
sm.set_handler(Box::new(CollectingHandler::new()));
let buf_id = sm.add_buffer("deep.json", src);
let parsed = {
let f = arena.alloc(JSONFactory::new(&arena, &atoms));
let mut p = JSONParser::new(f, buf_id, &mut sm, &atoms, false);
p.parse().is_some()
};
let messages = sm
.handler_as::<CollectingHandler>()
.expect("CollectingHandler installed above")
.messages()
.iter()
.map(|d| d.message.clone())
.collect();
(parsed, messages)
}
const JSON_MAX_RECURSION_DEPTH: usize = if cfg!(debug_assertions) { 512 } else { 4096 };
fn balanced_arrays(n: usize) -> String {
format!("{}{}", "[".repeat(n), "]".repeat(n))
}
#[test]
fn deeply_nested_json_is_rejected() {
let (parsed, messages) = parse_json(&"[".repeat(100000));
assert!(!parsed, "deeply nested JSON must not parse");
assert_eq!(messages, vec!["Too many nested JSON values".to_string()]);
}
#[test]
fn json_nesting_boundary_is_max_recursion_depth() {
let max = JSON_MAX_RECURSION_DEPTH;
let (parsed, messages) = parse_json(&balanced_arrays(max));
assert!(parsed, "{max} levels must parse: {messages:?}");
assert!(messages.is_empty(), "{messages:?}");
let (parsed, messages) = parse_json(&balanced_arrays(max + 1));
assert!(!parsed, "{} levels must not parse", max + 1);
assert_eq!(messages, vec!["Too many nested JSON values".to_string()]);
}