use hermes_ast::context::{Context, GCLock, NodeRc};
use hermes_ast::dump::dump_estree_json_with_sm;
use hermes_ast::dump::{ESTreeDumpMode, ESTreeRawProp, LocationDumpMode};
use hermes_ast::node::Node;
use hermes_support::diag::ResolvedDiagnostic;
use hermes_support::diag::{CollectingHandler, DiagKind, OutputOptions};
use hermes_support::manager::SourceErrorManager;
use hermes_support::render::render_diagnostic;
use crate::js::JSParserImpl;
use crate::lexer::{GrammarContext, JSLexer};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ParseFlags {
pub parse_flow: bool,
pub parse_flow_component_syntax: bool,
pub parse_flow_records: bool,
pub parse_flow_match: bool,
pub parse_ts: bool,
pub parse_jsx: bool,
pub strict_mode: bool,
}
impl ParseFlags {
fn apply(&self, ctx: &mut Context<'_>) {
let parse_flow = self.parse_flow
|| self.parse_flow_component_syntax
|| self.parse_flow_records
|| self.parse_flow_match;
ctx.set_parse_flow(parse_flow);
ctx.set_parse_flow_ambiguous(parse_flow);
ctx.set_parse_flow_component_syntax(self.parse_flow_component_syntax);
ctx.set_parse_flow_records(self.parse_flow_records);
ctx.set_parse_flow_match(self.parse_flow_match);
ctx.set_parse_ts(self.parse_ts);
ctx.set_parse_jsx(self.parse_jsx);
if self.strict_mode {
ctx.enable_strict_mode();
}
}
}
pub struct ParsedJS {
program: Option<NodeRc>,
ctx: Context<'static>,
sm: SourceErrorManager,
}
impl ParsedJS {
pub fn with_program<R, F>(&mut self, f: F) -> R
where
F: for<'gc> FnOnce(&'gc GCLock<'static, '_>, &'gc Node<'gc>) -> R,
{
let program = self.program.as_ref().expect("ParsedJS without program");
let gc = self.ctx.lock();
let node = program.node(&gc);
f(&gc, node)
}
pub fn transform_program<R, F>(&mut self, f: F) -> R
where
F: for<'gc> FnOnce(
&'gc GCLock<'static, '_>,
&'gc Node<'gc>,
&mut SourceErrorManager,
) -> (&'gc Node<'gc>, R),
{
let program = self.program.as_ref().expect("ParsedJS without program");
let gc = self.ctx.lock();
let node = program.node(&gc);
let (new_root, result) = f(&gc, node, &mut self.sm);
let new_program = NodeRc::from_node(&gc, new_root);
drop(gc);
self.program = Some(new_program);
result
}
pub fn to_estree_json(&mut self, pretty: bool) -> String {
self.to_estree_json_with(
pretty,
ESTreeDumpMode::HideEmpty,
LocationDumpMode::None,
ESTreeRawProp::Include,
)
}
pub fn to_estree_json_with(
&mut self,
pretty: bool,
mode: ESTreeDumpMode,
loc_mode: LocationDumpMode,
raw_prop: ESTreeRawProp,
) -> String {
let sm = &self.sm;
let program = self.program.as_ref().expect("ParsedJS without program");
let gc = self.ctx.lock();
let mut out = String::new();
dump_estree_json_with_sm(
&mut out,
program.node(&gc),
pretty,
mode,
sm,
loc_mode,
raw_prop,
&gc.ctx().atom_table,
);
out
}
pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
collected(&self.sm)
}
pub fn source_manager(&self) -> &SourceErrorManager {
&self.sm
}
}
impl std::fmt::Debug for ParsedJS {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParsedJS")
.field("nodes", &self.ctx.num_nodes())
.field("diagnostics", &self.diagnostics().len())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct ParseError {
diagnostics: Vec<ResolvedDiagnostic>,
error_count: u32,
}
impl ParseError {
pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
&self.diagnostics
}
pub fn error_count(&self) -> u32 {
self.error_count
}
pub fn messages(&self) -> Vec<String> {
let opts = OutputOptions {
show_colors: false,
..OutputOptions::default()
};
self.diagnostics
.iter()
.map(|d| render_diagnostic(d, &opts))
.collect()
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let plural = if self.error_count == 1 { "" } else { "s" };
match self.diagnostics.iter().find(|d| d.kind == DiagKind::Error) {
Some(d) => write!(
f,
"{} parse error{plural}; first at {}:{}:{}: {}",
self.error_count, d.file_name, d.line, d.col, d.message
),
None => write!(f, "{} parse error{plural}", self.error_count),
}
}
}
impl std::error::Error for ParseError {}
pub fn parse(source: &str, flags: ParseFlags) -> Result<ParsedJS, ParseError> {
parse_named(source, "input", flags)
}
pub fn parse_named(
source: &str,
file_name: &str,
flags: ParseFlags,
) -> Result<ParsedJS, ParseError> {
let mut sm = SourceErrorManager::new();
sm.set_handler(Box::new(CollectingHandler::new()));
let buf_id = sm.add_buffer(file_name, source);
let mut ctx = Context::new();
flags.apply(&mut ctx);
let mut parsed = ParsedJS {
program: None,
ctx,
sm,
};
{
let gc = parsed.ctx.lock();
let lexer = JSLexer::new(
buf_id,
&mut parsed.sm,
&gc.ctx().atom_table,
GrammarContext::AllowRegExp,
);
let mut parser = JSParserImpl::new(&gc, lexer);
if let Some(program) = parser.parse() {
parsed.program = Some(NodeRc::from_node(&gc, program));
}
}
let error_count = parsed.sm.error_count();
if parsed.program.is_some() && error_count == 0 {
Ok(parsed)
} else {
Err(ParseError {
diagnostics: collected(&parsed.sm).to_vec(),
error_count,
})
}
}
fn collected(sm: &SourceErrorManager) -> &[ResolvedDiagnostic] {
sm.handler_as::<CollectingHandler>()
.expect("collecting handler was replaced")
.messages()
}
#[cfg(test)]
mod tests {
use super::*;
use hermes_ast::node::NodeKind;
#[test]
fn parses_and_reports_program() {
let mut parsed = parse("1 + 2;", ParseFlags::default()).unwrap();
let (kind, len) = parsed.with_program(|_gc, program| match program {
Node::Program(p) => (program.kind(), p.body.iter().count()),
_ => panic!("root is not a Program"),
});
assert_eq!(kind, NodeKind::Program);
assert_eq!(len, 1);
assert!(parsed.diagnostics().is_empty());
}
#[test]
fn dumps_estree_json() {
let mut parsed = parse("0x10;", ParseFlags::default()).unwrap();
let json = parsed.to_estree_json(false);
assert!(json.starts_with(r#"{"type":"Program""#), "{json}");
assert!(json.contains(r#""raw":"0x10""#), "{json}");
assert!(!json.contains("\"loc\""), "{json}");
let with_loc = parsed.to_estree_json_with(
false,
ESTreeDumpMode::HideEmpty,
LocationDumpMode::LocAndRange,
ESTreeRawProp::Exclude,
);
assert!(with_loc.contains("\"loc\""), "{with_loc}");
assert!(!with_loc.contains("\"raw\""), "{with_loc}");
}
#[test]
fn transform_program_adopts_the_returned_root() {
let mut parsed = parse("1 + 2;", ParseFlags::default()).unwrap();
let old = parsed.transform_program(|_gc, program, sm| {
assert_eq!(sm.error_count(), 0);
let stmt = match program {
Node::Program(p) => p.body.iter().next().unwrap(),
_ => panic!("root is not a Program"),
};
let expr = match stmt {
Node::ExpressionStatement(e) => e.expression,
_ => panic!("not an ExpressionStatement"),
};
(expr, program.kind())
});
assert_eq!(old, NodeKind::Program);
let kind = parsed.with_program(|_gc, root| root.kind());
assert_eq!(kind, NodeKind::BinaryExpression);
assert!(parsed.ctx.num_nodes() > 0);
}
#[test]
fn transform_program_keeps_the_root_when_unchanged() {
let mut parsed = parse("var x;", ParseFlags::default()).unwrap();
parsed.transform_program(|_gc, program, _sm| (program, ()));
assert_eq!(
parsed.with_program(|_gc, root| root.kind()),
NodeKind::Program
);
}
#[test]
fn reports_errors_without_printing() {
let err = parse("1 +", ParseFlags::default()).unwrap_err();
assert_eq!(err.error_count(), 1);
assert_eq!(err.diagnostics().len() as u32, err.error_count());
assert_eq!(err.diagnostics()[0].kind, DiagKind::Error);
let _: &[crate::ResolvedDiagnostic] = err.diagnostics();
let shown = err.to_string();
assert!(!shown.contains('\n'), "{shown}");
let want = "1 parse error; first at input:1:";
assert!(shown.starts_with(want), "{shown}");
assert_eq!(err.messages().len(), 1);
assert!(err.messages()[0].contains('\n'), "{:?}", err.messages()[0]);
}
#[test]
fn flow_needs_its_flag() {
let src = "type T = number;";
assert!(parse(src, ParseFlags::default()).is_err());
let flags = ParseFlags {
parse_flow: true,
..Default::default()
};
assert!(parse(src, flags).is_ok());
}
#[test]
fn typescript_and_jsx() {
let ts = ParseFlags {
parse_ts: true,
..Default::default()
};
assert!(parse("let x: number = 1;", ts).is_ok());
let jsx = ParseFlags {
parse_jsx: true,
..Default::default()
};
assert!(parse("<a b={c} />;", jsx).is_ok());
}
#[test]
fn strict_mode_flag_is_honored() {
let src = "01;";
assert!(parse(src, ParseFlags::default()).is_ok());
let strict = ParseFlags {
strict_mode: true,
..Default::default()
};
assert!(parse(src, strict).is_err());
}
}