Skip to main content

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}