hermes_parser/facade.rs
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! The convenience front door: [`parse`] a string, get a [`ParsedJS`].
9//!
10//! This module adds no parsing behavior. It is a thin assembly of the pieces
11//! the `ast-dump` bin wires up by hand — an [`hermes_ast::context::Context`] (the AST
12//! arena), a [`SourceErrorManager`] (source buffers + diagnostics), a
13//! [`JSLexer`], and a [`JSParserImpl`] — into one call, so that a consumer who
14//! only wants "source in, AST out" does not have to know the assembly order.
15//!
16//! Everything it uses stays public: for lazy parsing, a custom
17//! [`hermes_support::diag::DiagHandler`], a shared `Context` across several files, or
18//! any other control the façade does not expose, drive
19//! [`crate::js::JSParserImpl`] directly the way
20//! `crates/tools/src/bin/ast_dump.rs` does.
21//!
22//! # Lifetime model
23//!
24//! AST nodes live in the `Context` arena and are only reachable while a
25//! [`GCLock`] is held, so [`ParsedJS`] owns the `Context` and keeps the
26//! `Program` node pinned with an [`hermes_ast::context::NodeRc`]. Reading the AST
27//! therefore goes through [`ParsedJS::with_program`], which takes the lock for
28//! the duration of a closure. Only one `GCLock` may exist per thread at a
29//! time, so `with_program` calls must not be nested.
30
31use hermes_ast::context::{Context, GCLock, NodeRc};
32use hermes_ast::dump::dump_estree_json_with_sm;
33use hermes_ast::dump::{ESTreeDumpMode, ESTreeRawProp, LocationDumpMode};
34use hermes_ast::node::Node;
35use hermes_support::diag::ResolvedDiagnostic;
36use hermes_support::diag::{CollectingHandler, DiagKind, OutputOptions};
37use hermes_support::manager::SourceErrorManager;
38use hermes_support::render::render_diagnostic;
39
40use crate::js::JSParserImpl;
41use crate::lexer::{GrammarContext, JSLexer};
42
43/// Which dialect(s) the parser accepts, plus forced strict mode.
44///
45/// `Default` is plain ECMAScript, non-strict — every flag `false`. Each field
46/// maps to the identically-named [`hermes_ast::context::Context`] flag, which is
47/// where the parser reads it from; the C++ `Context` getters cited in that
48/// module are the authoritative semantics. Two fields set more than their own
49/// flag, as documented below.
50///
51/// ```
52/// use hermes_parser::ParseFlags;
53/// let flags = ParseFlags { parse_flow: true, ..Default::default() };
54/// ```
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct ParseFlags {
57 /// Parse the Flow type grammar (`hermesc -parse-flow`).
58 ///
59 /// This also enables the Flow *ambiguous-expression* grammar (typed
60 /// arrows, `as`, type-args on call/new, type casts), because `hermesc`'s
61 /// `-parse-flow` means `ParseFlowSetting::ALL`. Flow and TypeScript are
62 /// mutually exclusive dialects: do not set this together with
63 /// [`Self::parse_ts`].
64 pub parse_flow: bool,
65
66 /// Parse Flow `component`/`hook` declarations
67 /// (`hermesc -Xparse-component-syntax`). Implies [`Self::parse_flow`].
68 pub parse_flow_component_syntax: bool,
69
70 /// Parse Flow `record` declarations and expressions
71 /// (`hermesc -Xparse-flow-records`). Implies [`Self::parse_flow`].
72 pub parse_flow_records: bool,
73
74 /// Parse Flow `match` expressions and statements
75 /// (`hermesc -Xparse-flow-match`). Implies [`Self::parse_flow`].
76 pub parse_flow_match: bool,
77
78 /// Parse the TypeScript type grammar (`hermesc -parse-ts`). Mutually
79 /// exclusive with [`Self::parse_flow`] and its extensions.
80 pub parse_ts: bool,
81
82 /// Parse JSX (`hermesc -parse-jsx`). Independent of the type dialect:
83 /// combines with Flow, with TypeScript, or with neither. Note that
84 /// enabling JSX disables the TypeScript `<Type>expr` assertion grammar,
85 /// exactly as in the C++ parser.
86 pub parse_jsx: bool,
87
88 /// Force strict mode for the whole source, as if it began with a
89 /// `"use strict"` directive. Sets `Context::enable_strict_mode`.
90 pub strict_mode: bool,
91}
92
93impl ParseFlags {
94 /// Apply these flags to a fresh `Context`.
95 ///
96 /// Mirrors the flag wiring in `crates/tools/src/bin/ast_dump.rs`,
97 /// including the two implications documented on the fields: the three
98 /// `parse_flow_*` extensions turn on `parse_flow`, and `parse_flow` turns
99 /// on the ambiguous-expression grammar.
100 fn apply(&self, ctx: &mut Context<'_>) {
101 let parse_flow = self.parse_flow
102 || self.parse_flow_component_syntax
103 || self.parse_flow_records
104 || self.parse_flow_match;
105 ctx.set_parse_flow(parse_flow);
106 ctx.set_parse_flow_ambiguous(parse_flow);
107 ctx.set_parse_flow_component_syntax(self.parse_flow_component_syntax);
108 ctx.set_parse_flow_records(self.parse_flow_records);
109 ctx.set_parse_flow_match(self.parse_flow_match);
110 ctx.set_parse_ts(self.parse_ts);
111 ctx.set_parse_jsx(self.parse_jsx);
112 if self.strict_mode {
113 ctx.enable_strict_mode();
114 }
115 }
116}
117
118/// A successful parse: the AST arena, the source manager, and the `Program`
119/// node, owned together.
120///
121/// The AST is only valid while its arena is alive, so this value owns the
122/// arena; dropping it frees the AST. Read the tree with
123/// [`with_program`](Self::with_program), or dump it with
124/// [`to_estree_json`](Self::to_estree_json).
125///
126/// **Not `Send`.** The arena uses `Cell`/`UnsafeCell` and the `GCLock` that
127/// guards it is thread-local by design, so a `ParsedJS` cannot be moved to
128/// another thread — parse on the thread that will read the AST. (The name
129/// keeps the crate's `JSParserImpl`/`JSLexer` casing rather than Rust's
130/// `ParsedJs`; that is deliberate, for consistency inside the port.)
131pub struct ParsedJS {
132 /// The `Program` node, pinned so it survives outside a `GCLock`.
133 ///
134 /// Always `Some` once [`parse_named`] has returned `Ok`; the `Option`
135 /// exists only so the value can be built before the parse runs (the
136 /// `NodeRc` cannot be created until a `GCLock` over `ctx` exists, and
137 /// `ctx` must not move afterwards).
138 ///
139 /// **Must be declared before `ctx`**: fields drop in declaration order and
140 /// `Context::drop` panics if a `NodeRc` into it is still alive.
141 program: Option<NodeRc>,
142
143 /// The arena owning every node of the AST.
144 ctx: Context<'static>,
145
146 /// Owns the source buffer and recorded the diagnostics. Needed by the
147 /// ESTree dumper for `loc`/`range`/`raw`.
148 sm: SourceErrorManager,
149}
150
151impl ParsedJS {
152 /// Run `f` with the arena locked and the `Program` node in hand.
153 ///
154 /// This is the read path for the AST: walk it with an
155 /// [`hermes_ast::visitor::Visitor`], match on [`Node`] arms, or read
156 /// [`Node::kind`]. References into the arena cannot escape the closure —
157 /// their lifetime ends with the lock — so return owned data instead. The
158 /// one thing that *can* escape is an [`hermes_ast::context::NodeRc`], which is
159 /// refcounted rather than borrowed; dropping this `ParsedJS` while such a
160 /// handle is still alive panics inside `Context::drop`.
161 ///
162 /// The bound is higher-ranked because [`Node`] is *invariant* in its
163 /// lifetime: a walker (`hermes_ast::visitor::Visitor<'gc>`) needs the node
164 /// reference and the node's own lifetime to be the same `'gc`, which only
165 /// a `for<'gc>` closure can promise.
166 ///
167 /// # Panics
168 ///
169 /// Panics if another [`GCLock`] is active on this thread — in particular
170 /// if `with_program` is called from inside another `with_program`.
171 pub fn with_program<R, F>(&mut self, f: F) -> R
172 where
173 F: for<'gc> FnOnce(&'gc GCLock<'static, '_>, &'gc Node<'gc>) -> R,
174 {
175 // Disjoint field borrows: `program` immutably, `ctx` mutably.
176 let program = self.program.as_ref().expect("ParsedJS without program");
177 let gc = self.ctx.lock();
178 let node = program.node(&gc);
179 f(&gc, node)
180 }
181
182 /// Run `f` with the arena locked, the `Program` node, and the source
183 /// manager, then adopt the node `f` returns as this `ParsedJS`'s program.
184 ///
185 /// This is [`with_program`](Self::with_program) for a pass that *rewrites*
186 /// the tree. A transforming visitor cannot mutate a node in place — the
187 /// arena hands out shared references — so it rebuilds the ancestors of
188 /// whatever it rewrote and returns a new root; that root is the one
189 /// carrying the pass's results, and keeping the old one would silently
190 /// read a stale tree. Returning it from the closure is how the new root
191 /// gets re-pinned here, in the one place that can do it without dropping
192 /// the arena or leaving the pin dangling: the old pin is released only
193 /// after the new one exists, and the arena is never touched in between.
194 /// Returning the node `f` was given is fine and means "unchanged".
195 ///
196 /// The `&mut SourceErrorManager` is the pass's diagnostic sink, and it is
197 /// the same one the parse used: a pass reports through it, and the
198 /// messages join the parse's in [`diagnostics`](Self::diagnostics).
199 ///
200 /// This exists for `hermes-sema`'s `resolve` façade, which is a
201 /// transforming visitor over exactly this shape; nothing about it is
202 /// specific to that crate.
203 ///
204 /// The escape rules of [`with_program`](Self::with_program) apply
205 /// unchanged: references into the arena cannot leave the closure (the
206 /// bound is higher-ranked for the same reason), while an
207 /// [`hermes_ast::context::NodeRc`] can — and dropping this `ParsedJS`
208 /// while one is alive panics inside `Context::drop`.
209 ///
210 /// # Panics
211 ///
212 /// Panics if another [`GCLock`] is active on this thread — in particular
213 /// if this is called from inside [`with_program`](Self::with_program) or
214 /// from inside another `transform_program`.
215 pub fn transform_program<R, F>(&mut self, f: F) -> R
216 where
217 F: for<'gc> FnOnce(
218 &'gc GCLock<'static, '_>,
219 &'gc Node<'gc>,
220 &mut SourceErrorManager,
221 ) -> (&'gc Node<'gc>, R),
222 {
223 // Disjoint field borrows: `program` immutably, `ctx` mutably (through
224 // the lock), `sm` mutably.
225 let program = self.program.as_ref().expect("ParsedJS without program");
226 let gc = self.ctx.lock();
227 let node = program.node(&gc);
228 let (new_root, result) = f(&gc, node, &mut self.sm);
229 // Pin the new root *before* releasing the old pin, so the arena is
230 // never observed without one.
231 let new_program = NodeRc::from_node(&gc, new_root);
232 // Release the lock before the store: the old `NodeRc` is dropped by
233 // the assignment, and a `NodeRc` drop only decrements refcounts — it
234 // neither needs nor may take a lock.
235 drop(gc);
236 self.program = Some(new_program);
237 result
238 }
239
240 /// Dump the AST as ESTree JSON: empty fields hidden, no `loc`/`range`,
241 /// and `"raw"` source text on numeric literals (the only node the dumper
242 /// emits it for). That is `hermesc -dump-ast` plus
243 /// `-include-raw-ast-prop`.
244 ///
245 /// `pretty` selects indented output. For other dumper settings use
246 /// [`to_estree_json_with`](Self::to_estree_json_with).
247 ///
248 /// # Panics
249 ///
250 /// Takes the arena lock, so it panics if another [`GCLock`] is live on
251 /// this thread — in particular when called from inside
252 /// [`with_program`](Self::with_program).
253 pub fn to_estree_json(&mut self, pretty: bool) -> String {
254 self.to_estree_json_with(
255 pretty,
256 ESTreeDumpMode::HideEmpty,
257 LocationDumpMode::None,
258 ESTreeRawProp::Include,
259 )
260 }
261
262 /// Dump the AST as ESTree JSON with full control over the dumper, which
263 /// is [`hermes_ast::dump::dump_estree_json_with_sm`] — see it for what each
264 /// argument does.
265 ///
266 /// # Panics
267 ///
268 /// Takes the arena lock, so it panics if another [`GCLock`] is live on
269 /// this thread — in particular when called from inside
270 /// [`with_program`](Self::with_program).
271 pub fn to_estree_json_with(
272 &mut self,
273 pretty: bool,
274 mode: ESTreeDumpMode,
275 loc_mode: LocationDumpMode,
276 raw_prop: ESTreeRawProp,
277 ) -> String {
278 let sm = &self.sm;
279 let program = self.program.as_ref().expect("ParsedJS without program");
280 let gc = self.ctx.lock();
281 let mut out = String::new();
282 dump_estree_json_with_sm(
283 &mut out,
284 program.node(&gc),
285 pretty,
286 mode,
287 sm,
288 loc_mode,
289 raw_prop,
290 &gc.ctx().atom_table,
291 );
292 out
293 }
294
295 /// The diagnostics recorded while parsing.
296 ///
297 /// An `Ok` parse reported no errors, so these are warnings and notes.
298 /// Render one with [`hermes_support::render::render_diagnostic`].
299 pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
300 collected(&self.sm)
301 }
302
303 /// The source manager that owns the parsed buffer, for coordinate lookups
304 /// (`find_coords`) and for driving the `ast` dumper by hand.
305 pub fn source_manager(&self) -> &SourceErrorManager {
306 &self.sm
307 }
308}
309
310impl std::fmt::Debug for ParsedJS {
311 /// Summarizes the arena rather than printing the AST, which can be huge.
312 /// (Hand-written because `SourceErrorManager` is not `Debug`.)
313 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 f.debug_struct("ParsedJS")
315 .field("nodes", &self.ctx.num_nodes())
316 .field("diagnostics", &self.diagnostics().len())
317 .finish_non_exhaustive()
318 }
319}
320
321/// A parse that reported at least one error.
322///
323/// There is no AST to carry: the parser returns no tree once it has reported
324/// an error, so only the diagnostics survive.
325#[derive(Debug, Clone)]
326pub struct ParseError {
327 /// Every diagnostic recorded, in emission order: errors, warnings, notes.
328 diagnostics: Vec<ResolvedDiagnostic>,
329 /// How many of them were errors.
330 error_count: u32,
331}
332
333impl ParseError {
334 /// Every diagnostic recorded during the parse, in emission order.
335 pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
336 &self.diagnostics
337 }
338
339 /// How many of the diagnostics are errors. Greater than zero for every
340 /// `ParseError` the parser produces — it fails only after reporting.
341 pub fn error_count(&self) -> u32 {
342 self.error_count
343 }
344
345 /// The diagnostics rendered one string each, LLVM-style (location line,
346 /// message, source line, caret), without ANSI colors.
347 pub fn messages(&self) -> Vec<String> {
348 let opts = OutputOptions {
349 show_colors: false,
350 ..OutputOptions::default()
351 };
352 self.diagnostics
353 .iter()
354 .map(|d| render_diagnostic(d, &opts))
355 .collect()
356 }
357}
358
359impl std::fmt::Display for ParseError {
360 /// A single line — count plus the first error's location and text — as
361 /// error types are expected to produce. The full LLVM-style rendering
362 /// (source line and caret) is [`messages`](Self::messages); the
363 /// structured form is [`diagnostics`](Self::diagnostics).
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 let plural = if self.error_count == 1 { "" } else { "s" };
366 match self.diagnostics.iter().find(|d| d.kind == DiagKind::Error) {
367 Some(d) => write!(
368 f,
369 "{} parse error{plural}; first at {}:{}:{}: {}",
370 self.error_count, d.file_name, d.line, d.col, d.message
371 ),
372 None => write!(f, "{} parse error{plural}", self.error_count),
373 }
374 }
375}
376
377impl std::error::Error for ParseError {}
378
379/// Parse `source` as a script named `"input"` in diagnostics.
380///
381/// See [`parse_named`], which this calls, for the details.
382///
383/// ```
384/// use hermes_parser::{parse, ParseFlags};
385///
386/// let parsed = parse("let x = 1;", ParseFlags::default()).unwrap();
387/// # let _ = parsed;
388/// ```
389pub fn parse(source: &str, flags: ParseFlags) -> Result<ParsedJS, ParseError> {
390 parse_named(source, "input", flags)
391}
392
393/// Parse `source`, calling it `file_name` in diagnostics.
394///
395/// The whole source is parsed eagerly (`ParserPass::FullParse`) as a Program,
396/// in the dialect `flags` selects. Returns [`ParseError`] if the parser
397/// reported any error — that is the same success condition the `ast-dump` bin
398/// applies: a `Program` was produced *and* the error count is zero.
399///
400/// Diagnostics are recorded in memory rather than printed; nothing is written
401/// to stderr.
402///
403/// ```
404/// use hermes_parser::{parse_named, ParseFlags};
405///
406/// let err = parse_named("1 +", "bad.js", ParseFlags::default())
407/// .expect_err("should not parse");
408/// assert_eq!(err.error_count(), 1);
409/// assert!(err.to_string().contains("bad.js"), "{err}");
410/// ```
411pub fn parse_named(
412 source: &str,
413 file_name: &str,
414 flags: ParseFlags,
415) -> Result<ParsedJS, ParseError> {
416 let mut sm = SourceErrorManager::new();
417 // Record diagnostics instead of printing them: a library must not write to
418 // stderr behind its caller's back.
419 sm.set_handler(Box::new(CollectingHandler::new()));
420 let buf_id = sm.add_buffer(file_name, source);
421
422 let mut ctx = Context::new();
423 flags.apply(&mut ctx);
424
425 let mut parsed = ParsedJS {
426 program: None,
427 ctx,
428 sm,
429 };
430
431 {
432 // The `GCLock` borrows `parsed.ctx`; the lexer borrows `parsed.sm`.
433 // Disjoint fields, so both borrows coexist.
434 let gc = parsed.ctx.lock();
435 let lexer = JSLexer::new(
436 buf_id,
437 &mut parsed.sm,
438 &gc.ctx().atom_table,
439 GrammarContext::AllowRegExp,
440 );
441 let mut parser = JSParserImpl::new(&gc, lexer);
442 if let Some(program) = parser.parse() {
443 parsed.program = Some(NodeRc::from_node(&gc, program));
444 }
445 }
446
447 // `JSParserImpl::parse` already returns `None` whenever an error was
448 // reported; the error-count check mirrors what every in-tree driver does.
449 let error_count = parsed.sm.error_count();
450 if parsed.program.is_some() && error_count == 0 {
451 Ok(parsed)
452 } else {
453 Err(ParseError {
454 diagnostics: collected(&parsed.sm).to_vec(),
455 error_count,
456 })
457 }
458}
459
460/// The diagnostics the [`CollectingHandler`] installed by [`parse_named`] has
461/// accumulated in `sm`.
462fn collected(sm: &SourceErrorManager) -> &[ResolvedDiagnostic] {
463 sm.handler_as::<CollectingHandler>()
464 .expect("collecting handler was replaced")
465 .messages()
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use hermes_ast::node::NodeKind;
472
473 #[test]
474 fn parses_and_reports_program() {
475 let mut parsed = parse("1 + 2;", ParseFlags::default()).unwrap();
476 let (kind, len) = parsed.with_program(|_gc, program| match program {
477 Node::Program(p) => (program.kind(), p.body.iter().count()),
478 _ => panic!("root is not a Program"),
479 });
480 assert_eq!(kind, NodeKind::Program);
481 assert_eq!(len, 1);
482 assert!(parsed.diagnostics().is_empty());
483 }
484
485 #[test]
486 fn dumps_estree_json() {
487 let mut parsed = parse("0x10;", ParseFlags::default()).unwrap();
488 let json = parsed.to_estree_json(false);
489 assert!(json.starts_with(r#"{"type":"Program""#), "{json}");
490 // The source manager reached the dumper, so `raw` resolved.
491 assert!(json.contains(r#""raw":"0x10""#), "{json}");
492 // Locations are off by default.
493 assert!(!json.contains("\"loc\""), "{json}");
494 let with_loc = parsed.to_estree_json_with(
495 false,
496 ESTreeDumpMode::HideEmpty,
497 LocationDumpMode::LocAndRange,
498 ESTreeRawProp::Exclude,
499 );
500 assert!(with_loc.contains("\"loc\""), "{with_loc}");
501 assert!(!with_loc.contains("\"raw\""), "{with_loc}");
502 }
503
504 #[test]
505 fn transform_program_adopts_the_returned_root() {
506 let mut parsed = parse("1 + 2;", ParseFlags::default()).unwrap();
507 // Stand in for a rewriting pass: hand back a *different* node of the
508 // same arena as the new root.
509 let old = parsed.transform_program(|_gc, program, sm| {
510 assert_eq!(sm.error_count(), 0);
511 let stmt = match program {
512 Node::Program(p) => p.body.iter().next().unwrap(),
513 _ => panic!("root is not a Program"),
514 };
515 let expr = match stmt {
516 Node::ExpressionStatement(e) => e.expression,
517 _ => panic!("not an ExpressionStatement"),
518 };
519 (expr, program.kind())
520 });
521 assert_eq!(old, NodeKind::Program);
522 // The new root is what every later read sees.
523 let kind = parsed.with_program(|_gc, root| root.kind());
524 assert_eq!(kind, NodeKind::BinaryExpression);
525 // The old root is unpinned but still allocated, and the arena is
526 // intact: dropping `parsed` must not panic (checked at scope exit).
527 assert!(parsed.ctx.num_nodes() > 0);
528 }
529
530 #[test]
531 fn transform_program_keeps_the_root_when_unchanged() {
532 let mut parsed = parse("var x;", ParseFlags::default()).unwrap();
533 parsed.transform_program(|_gc, program, _sm| (program, ()));
534 assert_eq!(
535 parsed.with_program(|_gc, root| root.kind()),
536 NodeKind::Program
537 );
538 }
539
540 #[test]
541 fn reports_errors_without_printing() {
542 let err = parse("1 +", ParseFlags::default()).unwrap_err();
543 assert_eq!(err.error_count(), 1);
544 assert_eq!(err.diagnostics().len() as u32, err.error_count());
545 assert_eq!(err.diagnostics()[0].kind, DiagKind::Error);
546 // The re-export at the crate root names the same type.
547 let _: &[crate::ResolvedDiagnostic] = err.diagnostics();
548 // `Display` is a one-line summary; the full rendering is `messages`.
549 let shown = err.to_string();
550 assert!(!shown.contains('\n'), "{shown}");
551 let want = "1 parse error; first at input:1:";
552 assert!(shown.starts_with(want), "{shown}");
553 assert_eq!(err.messages().len(), 1);
554 assert!(err.messages()[0].contains('\n'), "{:?}", err.messages()[0]);
555 }
556
557 #[test]
558 fn flow_needs_its_flag() {
559 let src = "type T = number;";
560 assert!(parse(src, ParseFlags::default()).is_err());
561 let flags = ParseFlags {
562 parse_flow: true,
563 ..Default::default()
564 };
565 assert!(parse(src, flags).is_ok());
566 }
567
568 #[test]
569 fn typescript_and_jsx() {
570 let ts = ParseFlags {
571 parse_ts: true,
572 ..Default::default()
573 };
574 assert!(parse("let x: number = 1;", ts).is_ok());
575 let jsx = ParseFlags {
576 parse_jsx: true,
577 ..Default::default()
578 };
579 assert!(parse("<a b={c} />;", jsx).is_ok());
580 }
581
582 #[test]
583 fn strict_mode_flag_is_honored() {
584 // A legacy octal literal: legal in sloppy mode, an error in strict.
585 let src = "01;";
586 assert!(parse(src, ParseFlags::default()).is_ok());
587 let strict = ParseFlags {
588 strict_mode: true,
589 ..Default::default()
590 };
591 assert!(parse(src, strict).is_err());
592 }
593}