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}