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    /// ## Why `&mut self` for a read
168    ///
169    /// Reading the AST takes the arena lock, and
170    /// [`Context::lock`](hermes_ast::context::Context::lock) takes
171    /// `&mut self`: the `GCLock` holds a `&mut Context`, which is what stops
172    /// [`Context::gc`](hermes_ast::context::Context::gc) — the mark-and-sweep
173    /// that would invalidate every outstanding `&Node` — from running while
174    /// the tree is being read. So `&mut` here means "exclusive view of the
175    /// arena", not "the AST is modified"; nothing in this method writes to the
176    /// tree. Consequently a `ParsedJS` cannot be read through a shared
177    /// reference: to share a parse, collect what you need inside the closure
178    /// and hand out the owned result.
179    ///
180    /// # Panics
181    ///
182    /// Panics if another [`GCLock`] is active on this thread — in particular
183    /// if `with_program` is called from inside another `with_program`.
184    pub fn with_program<R, F>(&mut self, f: F) -> R
185    where
186        F: for<'gc> FnOnce(&'gc GCLock<'static, '_>, &'gc Node<'gc>) -> R,
187    {
188        // Disjoint field borrows: `program` immutably, `ctx` mutably.
189        let program = self.program.as_ref().expect("ParsedJS without program");
190        let gc = self.ctx.lock();
191        let node = program.node(&gc);
192        f(&gc, node)
193    }
194
195    /// Run `f` with the arena locked, the `Program` node, and the source
196    /// manager, then adopt the node `f` returns as this `ParsedJS`'s program.
197    ///
198    /// This is [`with_program`](Self::with_program) for a pass that *rewrites*
199    /// the tree. A transforming visitor cannot mutate a node in place — the
200    /// arena hands out shared references — so it rebuilds the ancestors of
201    /// whatever it rewrote and returns a new root; that root is the one
202    /// carrying the pass's results, and keeping the old one would silently
203    /// read a stale tree. Returning it from the closure is how the new root
204    /// gets re-pinned here, in the one place that can do it without dropping
205    /// the arena or leaving the pin dangling: the old pin is released only
206    /// after the new one exists, and the arena is never touched in between.
207    /// Returning the node `f` was given is fine and means "unchanged".
208    ///
209    /// The `&mut SourceErrorManager` is the pass's diagnostic sink, and it is
210    /// the same one the parse used: a pass reports through it, and the
211    /// messages join the parse's in [`diagnostics`](Self::diagnostics).
212    ///
213    /// This exists for `hermes-sema`'s `resolve` façade, which is a
214    /// transforming visitor over exactly this shape; nothing about it is
215    /// specific to that crate.
216    ///
217    /// The escape rules of [`with_program`](Self::with_program) apply
218    /// unchanged: references into the arena cannot leave the closure (the
219    /// bound is higher-ranked for the same reason), while an
220    /// [`hermes_ast::context::NodeRc`] can — and dropping this `ParsedJS`
221    /// while one is alive panics inside `Context::drop`.
222    ///
223    /// # Panics
224    ///
225    /// Panics if another [`GCLock`] is active on this thread — in particular
226    /// if this is called from inside [`with_program`](Self::with_program) or
227    /// from inside another `transform_program`.
228    pub fn transform_program<R, F>(&mut self, f: F) -> R
229    where
230        F: for<'gc> FnOnce(
231            &'gc GCLock<'static, '_>,
232            &'gc Node<'gc>,
233            &mut SourceErrorManager,
234        ) -> (&'gc Node<'gc>, R),
235    {
236        // Disjoint field borrows: `program` immutably, `ctx` mutably (through
237        // the lock), `sm` mutably.
238        let program = self.program.as_ref().expect("ParsedJS without program");
239        let gc = self.ctx.lock();
240        let node = program.node(&gc);
241        let (new_root, result) = f(&gc, node, &mut self.sm);
242        // Pin the new root *before* releasing the old pin, so the arena is
243        // never observed without one.
244        let new_program = NodeRc::from_node(&gc, new_root);
245        // Release the lock before the store: the old `NodeRc` is dropped by
246        // the assignment, and a `NodeRc` drop only decrements refcounts — it
247        // neither needs nor may take a lock.
248        drop(gc);
249        self.program = Some(new_program);
250        result
251    }
252
253    /// Dump the AST as ESTree JSON: empty fields hidden, no `loc`/`range`,
254    /// and `"raw"` source text on numeric literals (the only node the dumper
255    /// emits it for). That is `hermesc -dump-ast` plus
256    /// `-include-raw-ast-prop`.
257    ///
258    /// `pretty` selects indented output. For other dumper settings use
259    /// [`to_estree_json_with`](Self::to_estree_json_with).
260    ///
261    /// Takes `&mut self` although it only reads the tree, for the reason
262    /// [`with_program`](Self::with_program) documents: dumping locks the
263    /// arena, and taking the lock needs exclusive access to the `Context`.
264    ///
265    /// # Panics
266    ///
267    /// Takes the arena lock, so it panics if another [`GCLock`] is live on
268    /// this thread — in particular when called from inside
269    /// [`with_program`](Self::with_program).
270    pub fn to_estree_json(&mut self, pretty: bool) -> String {
271        self.to_estree_json_with(
272            pretty,
273            ESTreeDumpMode::HideEmpty,
274            LocationDumpMode::None,
275            ESTreeRawProp::Include,
276        )
277    }
278
279    /// Dump the AST as ESTree JSON with full control over the dumper, which
280    /// is [`hermes_ast::dump::dump_estree_json_with_sm`] — see it for what each
281    /// argument does.
282    ///
283    /// `&mut self` for the same reason as
284    /// [`to_estree_json`](Self::to_estree_json): it takes the arena lock.
285    ///
286    /// # Panics
287    ///
288    /// Takes the arena lock, so it panics if another [`GCLock`] is live on
289    /// this thread — in particular when called from inside
290    /// [`with_program`](Self::with_program).
291    pub fn to_estree_json_with(
292        &mut self,
293        pretty: bool,
294        mode: ESTreeDumpMode,
295        loc_mode: LocationDumpMode,
296        raw_prop: ESTreeRawProp,
297    ) -> String {
298        let sm = &self.sm;
299        let program = self.program.as_ref().expect("ParsedJS without program");
300        let gc = self.ctx.lock();
301        let mut out = String::new();
302        dump_estree_json_with_sm(
303            &mut out,
304            program.node(&gc),
305            pretty,
306            mode,
307            sm,
308            loc_mode,
309            raw_prop,
310            &gc.ctx().atom_table,
311        );
312        out
313    }
314
315    /// The diagnostics recorded while parsing.
316    ///
317    /// An `Ok` parse reported no errors, so these are warnings and notes.
318    /// Render one with [`hermes_support::render::render_diagnostic`].
319    pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
320        collected(&self.sm)
321    }
322
323    /// The source manager that owns the parsed buffer, for coordinate lookups
324    /// (`find_coords`) and for driving the `ast` dumper by hand.
325    pub fn source_manager(&self) -> &SourceErrorManager {
326        &self.sm
327    }
328}
329
330impl std::fmt::Debug for ParsedJS {
331    /// Summarizes the arena rather than printing the AST, which can be huge.
332    /// (Hand-written because `SourceErrorManager` is not `Debug`.)
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        f.debug_struct("ParsedJS")
335            .field("nodes", &self.ctx.num_nodes())
336            .field("diagnostics", &self.diagnostics().len())
337            .finish_non_exhaustive()
338    }
339}
340
341/// A parse that reported at least one error.
342///
343/// There is no AST to carry: the parser returns no tree once it has reported
344/// an error, so only the diagnostics survive.
345#[derive(Debug, Clone)]
346pub struct ParseError {
347    /// Every diagnostic recorded, in emission order: errors, warnings, notes.
348    diagnostics: Vec<ResolvedDiagnostic>,
349    /// How many of them were errors.
350    error_count: u32,
351}
352
353impl ParseError {
354    /// Every diagnostic recorded during the parse, in emission order.
355    pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
356        &self.diagnostics
357    }
358
359    /// How many of the diagnostics are errors. Greater than zero for every
360    /// `ParseError` the parser produces — it fails only after reporting.
361    pub fn error_count(&self) -> u32 {
362        self.error_count
363    }
364
365    /// The diagnostics rendered one string each, LLVM-style (location line,
366    /// message, source line, caret), without ANSI colors.
367    ///
368    /// **Each string already ends with a newline** — it is multi-line, and
369    /// [`hermes_support::render::render_diagnostic`] terminates every line it
370    /// writes. Print them with `print!`/`eprint!`; `println!` adds a blank
371    /// line between diagnostics.
372    ///
373    /// ```
374    /// use hermes_parser::{parse, ParseFlags};
375    ///
376    /// let err = parse("1 +", ParseFlags::default()).expect_err("bad parse");
377    /// for m in err.messages() {
378    ///     assert!(m.ends_with('\n'), "{m:?}");
379    ///     eprint!("{m}");
380    /// }
381    /// ```
382    pub fn messages(&self) -> Vec<String> {
383        let opts = OutputOptions {
384            show_colors: false,
385            ..OutputOptions::default()
386        };
387        self.diagnostics
388            .iter()
389            .map(|d| render_diagnostic(d, &opts))
390            .collect()
391    }
392}
393
394impl std::fmt::Display for ParseError {
395    /// A single line — count plus the first error's location and text — as
396    /// error types are expected to produce. The full LLVM-style rendering
397    /// (source line and caret) is [`messages`](Self::messages); the
398    /// structured form is [`diagnostics`](Self::diagnostics).
399    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400        let plural = if self.error_count == 1 { "" } else { "s" };
401        match self.diagnostics.iter().find(|d| d.kind == DiagKind::Error) {
402            Some(d) => write!(
403                f,
404                "{} parse error{plural}; first at {}:{}:{}: {}",
405                self.error_count, d.file_name, d.line, d.col, d.message
406            ),
407            None => write!(f, "{} parse error{plural}", self.error_count),
408        }
409    }
410}
411
412impl std::error::Error for ParseError {}
413
414/// Parse `source` as a script named `"input"` in diagnostics.
415///
416/// See [`parse_named`], which this calls, for the details.
417///
418/// ```
419/// use hermes_parser::{parse, ParseFlags};
420///
421/// let parsed = parse("let x = 1;", ParseFlags::default()).unwrap();
422/// # let _ = parsed;
423/// ```
424pub fn parse(source: &str, flags: ParseFlags) -> Result<ParsedJS, ParseError> {
425    parse_named(source, "input", flags)
426}
427
428/// Parse `source`, calling it `file_name` in diagnostics.
429///
430/// The whole source is parsed eagerly (`ParserPass::FullParse`) as a Program,
431/// in the dialect `flags` selects. Returns [`ParseError`] if the parser
432/// reported any error — that is the same success condition the `ast-dump` bin
433/// applies: a `Program` was produced *and* the error count is zero.
434///
435/// Diagnostics are recorded in memory rather than printed; nothing is written
436/// to stderr.
437///
438/// ```
439/// use hermes_parser::{parse_named, ParseFlags};
440///
441/// let err = parse_named("1 +", "bad.js", ParseFlags::default())
442///     .expect_err("should not parse");
443/// assert_eq!(err.error_count(), 1);
444/// assert!(err.to_string().contains("bad.js"), "{err}");
445/// ```
446pub fn parse_named(
447    source: &str,
448    file_name: &str,
449    flags: ParseFlags,
450) -> Result<ParsedJS, ParseError> {
451    let mut sm = SourceErrorManager::new();
452    // Record diagnostics instead of printing them: a library must not write to
453    // stderr behind its caller's back.
454    sm.set_handler(Box::new(CollectingHandler::new()));
455    let buf_id = sm.add_buffer(file_name, source);
456
457    let mut ctx = Context::new();
458    flags.apply(&mut ctx);
459
460    let mut parsed = ParsedJS {
461        program: None,
462        ctx,
463        sm,
464    };
465
466    {
467        // The `GCLock` borrows `parsed.ctx`; the lexer borrows `parsed.sm`.
468        // Disjoint fields, so both borrows coexist.
469        let gc = parsed.ctx.lock();
470        let lexer = JSLexer::new(
471            buf_id,
472            &mut parsed.sm,
473            &gc.ctx().atom_table,
474            GrammarContext::AllowRegExp,
475        );
476        let mut parser = JSParserImpl::new(&gc, lexer);
477        if let Some(program) = parser.parse() {
478            parsed.program = Some(NodeRc::from_node(&gc, program));
479        }
480    }
481
482    // `JSParserImpl::parse` already returns `None` whenever an error was
483    // reported; the error-count check mirrors what every in-tree driver does.
484    let error_count = parsed.sm.error_count();
485    if parsed.program.is_some() && error_count == 0 {
486        Ok(parsed)
487    } else {
488        Err(ParseError {
489            diagnostics: collected(&parsed.sm).to_vec(),
490            error_count,
491        })
492    }
493}
494
495/// The diagnostics the [`CollectingHandler`] installed by [`parse_named`] has
496/// accumulated in `sm`.
497fn collected(sm: &SourceErrorManager) -> &[ResolvedDiagnostic] {
498    sm.handler_as::<CollectingHandler>()
499        .expect("collecting handler was replaced")
500        .messages()
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use hermes_ast::node::NodeKind;
507
508    #[test]
509    fn parses_and_reports_program() {
510        let mut parsed = parse("1 + 2;", ParseFlags::default()).unwrap();
511        let (kind, len) = parsed.with_program(|_gc, program| match program {
512            Node::Program(p) => (program.kind(), p.body.iter().count()),
513            _ => panic!("root is not a Program"),
514        });
515        assert_eq!(kind, NodeKind::Program);
516        assert_eq!(len, 1);
517        assert!(parsed.diagnostics().is_empty());
518    }
519
520    #[test]
521    fn dumps_estree_json() {
522        let mut parsed = parse("0x10;", ParseFlags::default()).unwrap();
523        let json = parsed.to_estree_json(false);
524        assert!(json.starts_with(r#"{"type":"Program""#), "{json}");
525        // The source manager reached the dumper, so `raw` resolved.
526        assert!(json.contains(r#""raw":"0x10""#), "{json}");
527        // Locations are off by default.
528        assert!(!json.contains("\"loc\""), "{json}");
529        let with_loc = parsed.to_estree_json_with(
530            false,
531            ESTreeDumpMode::HideEmpty,
532            LocationDumpMode::LocAndRange,
533            ESTreeRawProp::Exclude,
534        );
535        assert!(with_loc.contains("\"loc\""), "{with_loc}");
536        assert!(!with_loc.contains("\"raw\""), "{with_loc}");
537    }
538
539    #[test]
540    fn transform_program_adopts_the_returned_root() {
541        let mut parsed = parse("1 + 2;", ParseFlags::default()).unwrap();
542        // Stand in for a rewriting pass: hand back a *different* node of the
543        // same arena as the new root.
544        let old = parsed.transform_program(|_gc, program, sm| {
545            assert_eq!(sm.error_count(), 0);
546            let stmt = match program {
547                Node::Program(p) => p.body.iter().next().unwrap(),
548                _ => panic!("root is not a Program"),
549            };
550            let expr = match stmt {
551                Node::ExpressionStatement(e) => e.expression,
552                _ => panic!("not an ExpressionStatement"),
553            };
554            (expr, program.kind())
555        });
556        assert_eq!(old, NodeKind::Program);
557        // The new root is what every later read sees.
558        let kind = parsed.with_program(|_gc, root| root.kind());
559        assert_eq!(kind, NodeKind::BinaryExpression);
560        // The old root is unpinned but still allocated, and the arena is
561        // intact: dropping `parsed` must not panic (checked at scope exit).
562        assert!(parsed.ctx.num_nodes() > 0);
563    }
564
565    #[test]
566    fn transform_program_keeps_the_root_when_unchanged() {
567        let mut parsed = parse("var x;", ParseFlags::default()).unwrap();
568        parsed.transform_program(|_gc, program, _sm| (program, ()));
569        assert_eq!(
570            parsed.with_program(|_gc, root| root.kind()),
571            NodeKind::Program
572        );
573    }
574
575    #[test]
576    fn reports_errors_without_printing() {
577        let err = parse("1 +", ParseFlags::default()).unwrap_err();
578        assert_eq!(err.error_count(), 1);
579        assert_eq!(err.diagnostics().len() as u32, err.error_count());
580        assert_eq!(err.diagnostics()[0].kind, DiagKind::Error);
581        // The re-export at the crate root names the same type.
582        let _: &[crate::ResolvedDiagnostic] = err.diagnostics();
583        // `Display` is a one-line summary; the full rendering is `messages`.
584        let shown = err.to_string();
585        assert!(!shown.contains('\n'), "{shown}");
586        let want = "1 parse error; first at input:1:";
587        assert!(shown.starts_with(want), "{shown}");
588        assert_eq!(err.messages().len(), 1);
589        assert!(err.messages()[0].contains('\n'), "{:?}", err.messages()[0]);
590    }
591
592    #[test]
593    fn flow_needs_its_flag() {
594        let src = "type T = number;";
595        assert!(parse(src, ParseFlags::default()).is_err());
596        let flags = ParseFlags {
597            parse_flow: true,
598            ..Default::default()
599        };
600        assert!(parse(src, flags).is_ok());
601    }
602
603    #[test]
604    fn typescript_and_jsx() {
605        let ts = ParseFlags {
606            parse_ts: true,
607            ..Default::default()
608        };
609        assert!(parse("let x: number = 1;", ts).is_ok());
610        let jsx = ParseFlags {
611            parse_jsx: true,
612            ..Default::default()
613        };
614        assert!(parse("<a b={c} />;", jsx).is_ok());
615    }
616
617    #[test]
618    fn strict_mode_flag_is_honored() {
619        // A legacy octal literal: legal in sloppy mode, an error in strict.
620        let src = "01;";
621        assert!(parse(src, ParseFlags::default()).is_ok());
622        let strict = ParseFlags {
623            strict_mode: true,
624            ..Default::default()
625        };
626        assert!(parse(src, strict).is_err());
627    }
628}