Skip to main content

hermes_sema/
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: [`resolve`] a [`ParsedJS`], get a
9//! [`ResolvedJS`].
10//!
11//! This module adds no analysis. It is a thin assembly of the pieces
12//! `tools`' `sema-dump` bin wires up by hand — a [`SemContext`] seeded with
13//! the [`Keywords`] table, the [`SourceErrorManager`] the parse already used,
14//! and one of the two [`crate::resolve`] entry points — into one call, so
15//! that a consumer who only wants "source in, resolved AST out" does not have
16//! to know the assembly order. It is the semantic counterpart of
17//! `hermes_parser`'s `parse` façade, and it starts where that one leaves off:
18//! its input is that façade's [`ParsedJS`].
19//!
20//! Everything it uses stays public: for a shared `SemContext` across several
21//! files, a hand-built arena, or any other control the façade does not
22//! expose, call [`crate::resolve::resolve_ast`] /
23//! [`crate::resolve::resolve_ast_for_parser`] directly the way
24//! `crates/tools/src/bin/sema_dump.rs` does.
25//!
26//! # Why `resolve` consumes the `ParsedJS`
27//!
28//! The resolver is a *transforming* visitor (see [`crate::resolver`]'s module
29//! doc). Rewriting a node rebuilds its ancestors, so the root that comes out
30//! of resolution is a different node than the one that went in, and it is the
31//! one carrying the results — reading the old root would silently read a
32//! stale tree. Taking the `ParsedJS` by value and handing back a `ResolvedJS`
33//! makes that unmissable: after `resolve`, the pre-resolution root is simply
34//! not reachable any more.
35//!
36//! # Lifetime model
37//!
38//! Inherited from `ParsedJS`, which [`ResolvedJS`] owns: AST nodes live in
39//! the `Context` arena and are only reachable while a [`GCLock`] is held, so
40//! reading the tree goes through [`ResolvedJS::with_program`], which takes the
41//! lock for the duration of a closure. Only one `GCLock` may exist per thread
42//! at a time, so those calls must not be nested.
43
44use hermes_ast::context::{GCLock, NodeRc};
45use hermes_ast::node::Node;
46use hermes_parser::js::JSParserImpl;
47use hermes_parser::lexer::{GrammarContext, JSLexer};
48use hermes_parser::ParsedJS;
49use hermes_support::diag::{DiagKind, OutputOptions, ResolvedDiagnostic};
50use hermes_support::manager::SourceErrorManager;
51use hermes_support::render::render_diagnostic;
52
53use crate::dump::sem_dump;
54use crate::keywords::Keywords;
55use crate::libhermes::LIBHERMES;
56use crate::resolve::{resolve_ast, resolve_ast_for_parser};
57use crate::sem_context::SemContext;
58
59/// A successful resolution: the arena, the resolved AST, and the
60/// [`SemContext`] holding the results, owned together.
61///
62/// Produced by [`resolve`], [`resolve_for_parser`] or [`resolve_for_compile`],
63/// each of which consumes the [`ParsedJS`] it resolves. Read the tree *with*
64/// its semantic information through [`with_program`](Self::with_program), or
65/// dump both with [`to_sema_dump`](Self::to_sema_dump).
66///
67/// **Not `Send`**, for the same reason `ParsedJS` is not: the arena uses
68/// `Cell`/`UnsafeCell` and the `GCLock` guarding it is thread-local by design.
69/// (The name keeps the port's `ParsedJS` casing rather than Rust's
70/// `ResolvedJs`; that is deliberate, for consistency inside the port.)
71pub struct ResolvedJS {
72    /// The resolution results: every `Decl`, `LexicalScope` and
73    /// `FunctionInfo`, plus the side tables keyed by AST node.
74    ///
75    /// **Must be declared before `parsed`**: fields drop in declaration order,
76    /// a `SemContext` holds [`NodeRc`]s into the arena (binding identifiers
77    /// and `$SHBuiltin` declarations), and `Context::drop` panics if a
78    /// `NodeRc` into it is still alive.
79    sem_ctx: SemContext,
80
81    /// The arena, the source manager, and the pinned root — which
82    /// `ParsedJS::transform_program` has re-pinned to the *resolved* root.
83    parsed: ParsedJS,
84}
85
86impl ResolvedJS {
87    /// Run `f` with the arena locked, the resolved root node, and the
88    /// [`SemContext`] in hand.
89    ///
90    /// This is the read path. The `SemContext` comes along because that is
91    /// the point of resolution: given an [`hermes_ast::node::Identifier`] in
92    /// the tree, [`SemContext::get_expression_decl`] /
93    /// [`SemContext::get_declaration_decl`] give the [`crate::ids::DeclId`] it
94    /// binds to, and [`SemContext::decl`] gives that declaration.
95    ///
96    /// References into the arena cannot escape the closure — their lifetime
97    /// ends with the lock — so return owned data instead. The one thing that
98    /// *can* escape is a [`NodeRc`], which is refcounted rather than borrowed;
99    /// dropping this `ResolvedJS` while such a handle is still alive panics
100    /// inside `Context::drop`.
101    ///
102    /// The bound is higher-ranked because [`Node`] is *invariant* in its
103    /// lifetime: a walker ([`hermes_ast::visitor::Visitor`]) needs the node
104    /// reference and the node's own lifetime to be the same `'gc`, which only
105    /// a `for<'gc>` closure can promise. A visitor that keeps the `&GCLock` in
106    /// a field must give the lock its own lifetime parameters rather than
107    /// reusing `'gc` — `GCLock<'ast, 'ctx>` is invariant in `'ast`, so the two
108    /// cannot be equated; `crates/sema/examples/print_bindings.rs` shows the
109    /// pattern and the error it avoids.
110    ///
111    /// ## Why `&mut self` for a read
112    ///
113    /// This locks the arena, and
114    /// [`Context::lock`](hermes_ast::context::Context::lock) takes
115    /// `&mut self`: the `GCLock` holds a `&mut Context`, which is what stops
116    /// [`Context::gc`](hermes_ast::context::Context::gc) — the mark-and-sweep
117    /// that would invalidate every outstanding `&Node` — from running while
118    /// the tree is being read. `&mut` therefore means "exclusive view of the
119    /// arena", not "the AST or the `SemContext` is modified"; the `SemContext`
120    /// is in fact handed to `f` as `&`. To share results, collect owned data
121    /// inside the closure. ([`sem_context`](Self::sem_context) needs no lock
122    /// and does take `&self`.)
123    ///
124    /// # Panics
125    ///
126    /// Panics if another [`GCLock`] is active on this thread — in particular
127    /// if `with_program` is called from inside another `with_program`.
128    pub fn with_program<R, F>(&mut self, f: F) -> R
129    where
130        F: for<'gc> FnOnce(
131            &'gc GCLock<'static, '_>,
132            &'gc Node<'gc>,
133            &SemContext,
134        ) -> R,
135    {
136        // Disjoint field borrows: `sem_ctx` immutably, `parsed` mutably.
137        let sem_ctx = &self.sem_ctx;
138        self.parsed.with_program(|gc, root| f(gc, root, sem_ctx))
139    }
140
141    /// The resolution results, for the queries that do not need the AST —
142    /// walking the scope tree, or reading a `FunctionInfo` reached from a
143    /// [`crate::ids::FunctionInfoId`] obtained inside
144    /// [`with_program`](Self::with_program).
145    pub fn sem_context(&self) -> &SemContext {
146        &self.sem_ctx
147    }
148
149    /// Dump the `SemContext` and the annotated AST as text, through
150    /// [`crate::dump::sem_dump`] — the `-dump-sema` format, byte-for-byte.
151    /// (After [`resolve_for_compile`] this is exactly what
152    /// `hermesc -dump-sema` prints for the same input; that differential is
153    /// this crate's correctness gate. After the parser path it is the same
154    /// format over the differently-resolved tree, which is what the C++
155    /// `sema-parser-dump` tool prints.)
156    ///
157    /// Bytes rather than a `String` because an identifier in the source may
158    /// be an unpaired surrogate, which the dumper writes out as WTF-8 — not
159    /// valid UTF-8. For ordinary sources `String::from_utf8` succeeds.
160    ///
161    /// Takes `&mut self` although it only reads, for the reason
162    /// [`with_program`](Self::with_program) documents: dumping locks the
163    /// arena, and taking the lock needs exclusive access to the `Context`.
164    ///
165    /// # Panics
166    ///
167    /// Takes the arena lock, so it panics if another [`GCLock`] is live on
168    /// this thread — in particular when called from inside
169    /// [`with_program`](Self::with_program).
170    pub fn to_sema_dump(&mut self) -> Vec<u8> {
171        let mut out: Vec<u8> = Vec::new();
172        self.with_program(|gc, root, sem_ctx| {
173            sem_dump(&mut out, gc, sem_ctx, root);
174        });
175        out
176    }
177
178    /// The diagnostics recorded so far, in emission order: the parse's (which
179    /// were warnings and notes, since the parse succeeded) followed by
180    /// resolution's.
181    ///
182    /// For a `ResolvedJS` that came out of [`resolve`] or
183    /// [`resolve_for_compile`] these are again warnings and notes only;
184    /// [`resolve_for_parser`] can return one carrying errors — see its doc.
185    /// Render one with [`hermes_support::render::render_diagnostic`].
186    pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
187        self.parsed.diagnostics()
188    }
189
190    /// How many of the recorded diagnostics are errors.
191    ///
192    /// Zero for a `ResolvedJS` from [`resolve`] or [`resolve_for_compile`].
193    /// This is the check the C++ `resolveASTForParser` callers make instead
194    /// of looking at a return value, so it is what
195    /// [`resolve_for_parser`]'s result must be tested with.
196    pub fn error_count(&self) -> u32 {
197        self.parsed.source_manager().error_count()
198    }
199
200    /// The source manager owning the parsed buffer (and the `libhermes`
201    /// buffer, if [`CompileOptions::std_globals`] was on), for coordinate
202    /// lookups and for driving the AST dumper by hand.
203    pub fn source_manager(&self) -> &SourceErrorManager {
204        self.parsed.source_manager()
205    }
206
207    /// Give back the [`ParsedJS`], now holding the *resolved* AST, and drop
208    /// the `SemContext`.
209    ///
210    /// This is how the rest of the parser façade's surface — ESTree JSON
211    /// dumping in particular, which is what a resolve-then-serialize consumer
212    /// like `hermes-parser-wasm` does — stays reachable after resolution
213    /// without this type mirroring it method for method. The AST keeps every
214    /// rewrite the resolver made; only the `Decl`/scope tables go away.
215    pub fn into_parsed(self) -> ParsedJS {
216        let ResolvedJS { sem_ctx, parsed } = self;
217        // Explicit, and load-bearing: the `SemContext` holds `NodeRc`s into
218        // the arena `parsed` owns, and `Context::drop` panics if one outlives
219        // it. Destructuring drops nothing on its own, so without this the
220        // order would be the caller's to get wrong.
221        drop(sem_ctx);
222        parsed
223    }
224}
225
226impl std::fmt::Debug for ResolvedJS {
227    /// Summarizes rather than printing the AST or the tables, which can be
228    /// huge. (Hand-written because neither `ParsedJS` nor `SemContext`
229    /// derives `Debug`.)
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        f.debug_struct("ResolvedJS")
232            .field("functions", &self.sem_ctx.functions_len())
233            .field("diagnostics", &self.diagnostics().len())
234            .field("errors", &self.error_count())
235            .finish_non_exhaustive()
236    }
237}
238
239/// A resolution that reported at least one error.
240///
241/// There is no AST to carry, matching `hermes_parser::ParseError`: on the
242/// compile path there genuinely is none (the resolver returns nothing once it
243/// has failed), and on the parser path the tree is dropped rather than
244/// returned — use [`resolve_for_parser`] if you need the partially resolved
245/// tree that reported these errors.
246#[derive(Debug, Clone)]
247pub struct ResolveError {
248    /// Every diagnostic recorded, in emission order: errors, warnings, notes.
249    diagnostics: Vec<ResolvedDiagnostic>,
250    /// How many of them were errors.
251    error_count: u32,
252}
253
254impl ResolveError {
255    /// Every diagnostic recorded during parsing and resolution, in emission
256    /// order.
257    pub fn diagnostics(&self) -> &[ResolvedDiagnostic] {
258        &self.diagnostics
259    }
260
261    /// How many of the diagnostics are errors. Greater than zero for every
262    /// `ResolveError` the façade produces.
263    pub fn error_count(&self) -> u32 {
264        self.error_count
265    }
266
267    /// The diagnostics rendered one string each, LLVM-style (location line,
268    /// message, source line, caret), without ANSI colors.
269    ///
270    /// **Each string already ends with a newline**, as
271    /// [`hermes_parser::ParseError::messages`] does — print them with
272    /// `print!`/`eprint!`, since `println!` adds a blank line between
273    /// diagnostics.
274    pub fn messages(&self) -> Vec<String> {
275        let opts = OutputOptions {
276            show_colors: false,
277            ..OutputOptions::default()
278        };
279        self.diagnostics
280            .iter()
281            .map(|d| render_diagnostic(d, &opts))
282            .collect()
283    }
284
285    /// Collect what the source manager recorded. The manager is the one the
286    /// parse installed a collecting handler on, so this is every message from
287    /// both phases.
288    fn from_resolved(resolved: &ResolvedJS) -> ResolveError {
289        ResolveError {
290            diagnostics: resolved.diagnostics().to_vec(),
291            error_count: resolved.error_count(),
292        }
293    }
294}
295
296impl std::fmt::Display for ResolveError {
297    /// A single line — count plus the first error's location and text — as
298    /// error types are expected to produce. The full LLVM-style rendering
299    /// (source line and caret) is [`messages`](Self::messages); the
300    /// structured form is [`diagnostics`](Self::diagnostics).
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        let plural = if self.error_count == 1 { "" } else { "s" };
303        match self.diagnostics.iter().find(|d| d.kind == DiagKind::Error) {
304            Some(d) => write!(
305                f,
306                "{} semantic error{plural}; first at {}:{}:{}: {}",
307                self.error_count, d.file_name, d.line, d.col, d.message
308            ),
309            None => write!(f, "{} semantic error{plural}", self.error_count),
310        }
311    }
312}
313
314impl std::error::Error for ResolveError {}
315
316/// One file's worth of ambient global declarations, as source text.
317///
318/// The compile path's `ambientDecls` are parsed files whose top-level
319/// declarations are injected into the global scope — that is how a host tells
320/// the compiler which globals its runtime provides. hermesc's
321/// `-include-globals` takes them as file paths; this takes the text, because
322/// the façade owns the parsing.
323#[derive(Debug, Clone, Default, PartialEq, Eq)]
324pub struct GlobalDefinitions {
325    /// The name to use for this buffer in diagnostics.
326    pub file_name: String,
327    /// The declarations, as JavaScript source.
328    pub source: String,
329}
330
331/// What [`resolve_for_compile`] injects into the global scope before it
332/// resolves.
333///
334/// `Default` is hermesc's default: the standard globals on, nothing else.
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct CompileOptions {
337    /// Declare the JavaScript standard library's globals (`Object`, `Math`,
338    /// `print`, …) — hermesc's `-fstd-globals`, on by default there too.
339    ///
340    /// With this off, every reference to a standard global resolves to an
341    /// implicitly created `UndeclaredGlobalProperty` instead of to an ambient
342    /// declaration; nothing fails, but the `SemContext` differs. The
343    /// declarations are [`crate::libhermes::LIBHERMES`], compiled into this
344    /// crate.
345    pub std_globals: bool,
346
347    /// Additional ambient declaration files, parsed in this order after the
348    /// standard globals — hermesc's `-include-globals`.
349    ///
350    /// A parse error in one of these is reported like any other and makes
351    /// [`resolve_for_compile`] fail.
352    pub global_definitions: Vec<GlobalDefinitions>,
353}
354
355impl Default for CompileOptions {
356    fn default() -> Self {
357        CompileOptions {
358            std_globals: true,
359            global_definitions: Vec::new(),
360        }
361    }
362}
363
364/// Resolve a parsed program, failing if resolution reported any error.
365///
366/// This is [`resolve_for_parser`] — the `compile = false` entry point, so no
367/// compile-only validation and no AST rewrites — plus the error check its C++
368/// callers make by hand. It is the one to reach for when the goal is "names
369/// resolved, or tell me what is wrong": a linter, an editor service, a
370/// transform that needs binding information.
371///
372/// Use [`resolve_for_compile`] instead when the result feeds a compiler back
373/// end, and [`resolve_for_parser`] when the partially resolved tree is wanted
374/// even though resolution reported errors.
375///
376/// # Panics
377///
378/// Takes the arena lock, so it panics if a [`GCLock`] is already live on this
379/// thread.
380///
381/// ```
382/// use hermes_parser::{parse, ParseFlags};
383///
384/// let parsed = parse("let x = 1; x;", ParseFlags::default()).unwrap();
385/// let resolved = hermes_sema::resolve(parsed).unwrap();
386/// assert_eq!(resolved.error_count(), 0);
387///
388/// // `continue` outside a loop is a semantic error, not a syntax error.
389/// let parsed = parse("continue;", ParseFlags::default()).unwrap();
390/// let err = hermes_sema::resolve(parsed).expect_err("should not resolve");
391/// assert_eq!(err.error_count(), 1);
392/// assert!(err.to_string().contains("'continue' not"), "{err}");
393/// ```
394pub fn resolve(parsed: ParsedJS) -> Result<ResolvedJS, ResolveError> {
395    let resolved = resolve_for_parser(parsed);
396    if resolved.error_count() == 0 {
397        Ok(resolved)
398    } else {
399        Err(ResolveError::from_resolved(&resolved))
400    }
401}
402
403/// Resolve a parsed program the way the C++ `resolveASTForParser`
404/// (`SemResolve.cpp:299-310`) does, and hand back the result **whether or not
405/// resolution reported errors**.
406///
407/// That is the entry point a parser-only consumer wants, and this is its
408/// exact contract, ported: `compile = false`, so it will not error on
409/// constructs that parse but cannot be compiled, will not perform
410/// compile-specific validation, and will not transform the AST (no constant
411/// folding, no arrow-body or `try` rewriting, no `$SHBuiltin` collapsing); it
412/// takes no ambient declarations; and it *always* produces a tree. The C++
413/// caller (`hermes-parser-wasm.cpp:104`) ignores the `bool` return value and
414/// serializes whatever root it gets, checking its diagnostic handler
415/// separately — [`ResolvedJS::error_count`] is that check here.
416///
417/// [`resolve`] is this plus the check, for callers who want the usual
418/// `Result`.
419///
420/// # Panics
421///
422/// Takes the arena lock, so it panics if a [`GCLock`] is already live on this
423/// thread.
424///
425/// ```
426/// use hermes_parser::{parse, ParseFlags};
427///
428/// // Resolution fails, but the tree — and what could be resolved of it —
429/// // still comes back.
430/// let parsed = parse("continue; var x;", ParseFlags::default()).unwrap();
431/// let mut resolved = hermes_sema::resolve_for_parser(parsed);
432/// assert_eq!(resolved.error_count(), 1);
433/// assert!(!resolved.to_sema_dump().is_empty());
434/// ```
435pub fn resolve_for_parser(parsed: ParsedJS) -> ResolvedJS {
436    let mut parsed = parsed;
437    let sem_ctx = parsed.transform_program(|gc, root, sm| {
438        let mut sem_ctx = SemContext::new(Keywords::new(gc));
439        let resolved = resolve_ast_for_parser(gc, &mut sem_ctx, sm, root);
440        (resolved, sem_ctx)
441    });
442    ResolvedJS { sem_ctx, parsed }
443}
444
445/// Resolve a parsed program the way the C++ `resolveAST`
446/// (`SemResolve.cpp:163-195`) does: the **compile** path.
447///
448/// Differences from [`resolve_for_parser`], which are exactly the C++ entry
449/// points' differences (`compile = true`):
450///
451/// * It rejects what the compiler cannot handle, and runs the
452///   compile-specific validation.
453/// * It **transforms the AST**: constant folding of `+`/`-` chains and unary
454///   operators, an expression-bodied arrow rewritten to a block with a
455///   `return`, `try`/`catch`/`finally` split into nested `try`s,
456///   `$SHBuiltin.x` collapsed to an `SHBuiltin` node, an anonymous
457///   `export default function` turned into a function expression, and
458///   block-scoped function promotion.
459/// * It takes ambient declarations — see [`CompileOptions`].
460/// * It can fail: the C++ returns `false` and this returns `Err`, in which
461///   case there is no tree at all (unlike the parser path, which always has
462///   one).
463///
464/// # Panics
465///
466/// Takes the arena lock, so it panics if a [`GCLock`] is already live on this
467/// thread. Also panics if [`CompileOptions::std_globals`] is set and the
468/// compiled-in `libhermes` declarations fail to parse, which would be a bug
469/// in this crate rather than in the caller's input.
470///
471/// ```
472/// use hermes_parser::{parse, ParseFlags};
473/// use hermes_sema::{resolve_for_compile, CompileOptions};
474///
475/// let parsed = parse("Math.max(1, 2);", ParseFlags::default()).unwrap();
476/// let mut resolved =
477///     resolve_for_compile(parsed, &CompileOptions::default()).unwrap();
478/// // `Math` came from the standard globals, so it is an ambient declaration
479/// // rather than an implicitly created one.
480/// let dump = String::from_utf8(resolved.to_sema_dump()).unwrap();
481/// assert!(dump.contains("'Math' UndeclaredGlobalProperty"), "{dump}");
482/// ```
483pub fn resolve_for_compile(
484    parsed: ParsedJS,
485    options: &CompileOptions,
486) -> Result<ResolvedJS, ResolveError> {
487    let mut parsed = parsed;
488    let sem_ctx = parsed.transform_program(|gc, root, sm| {
489        // The ambient files are parsed into the same arena and the same
490        // source manager as the input, exactly like `loadGlobalDefinition`
491        // (CompilerDriver.cpp:773-785); their `Program` nodes are what
492        // `resolveAST` takes as `ambientDecls`. (The C++ driver parses them
493        // *before* the input file because that is when it reads its command
494        // line; the resolver visits them in list order either way, so the
495        // resolution results do not depend on it.)
496        let mut ambient_decls: Vec<NodeRc> = Vec::new();
497        let mut ambient_ok = true;
498        if options.std_globals {
499            match parse_ambient(gc, sm, "<libhermes>", LIBHERMES) {
500                // `libhermes` is a compiled-in constant, so a failure here is
501                // a bug in this crate rather than in the caller's input.
502                None => panic!("libhermes must parse: it is a constant"),
503                Some(program) => ambient_decls.push(program),
504            }
505        }
506        for gd in &options.global_definitions {
507            match parse_ambient(gc, sm, &gd.file_name, &gd.source) {
508                None => {
509                    // The parser reported the error into `sm`; stop before
510                    // resolving against a half-loaded global scope, as the
511                    // driver does (`LoadGlobalsFailed`).
512                    ambient_ok = false;
513                    break;
514                }
515                Some(program) => ambient_decls.push(program),
516            }
517        }
518
519        if !ambient_ok {
520            return (root, None);
521        }
522        let mut sem_ctx = SemContext::new(Keywords::new(gc));
523        match resolve_ast(gc, &mut sem_ctx, sm, root, &ambient_decls) {
524            Some(resolved) => (resolved, Some(sem_ctx)),
525            // Resolution failed: keep the pre-resolution root (the arena must
526            // keep exactly one pinned root either way) and report below. The
527            // `SemContext` is dropped here, releasing its `NodeRc`s while the
528            // arena is still alive, which is what `Context::drop` requires.
529            None => (root, None),
530        }
531    });
532
533    match sem_ctx {
534        Some(sem_ctx) => Ok(ResolvedJS { sem_ctx, parsed }),
535        None => {
536            // No `ResolvedJS` to build the error from; read the same two
537            // things off the `ParsedJS` directly, then drop it.
538            Err(ResolveError {
539                diagnostics: parsed.diagnostics().to_vec(),
540                error_count: parsed.source_manager().error_count(),
541            })
542        }
543    }
544}
545
546/// Parse one ambient-declaration file into `gc`'s arena, returning its
547/// `Program` pinned, or `None` if the parse reported an error.
548///
549/// Mirrors `loadGlobalDefinition` (CompilerDriver.cpp:773-785): its own
550/// buffer in the shared source manager, the same parser as the input, and the
551/// resulting `Program` becomes one entry of the `DeclarationFileListTy`.
552fn parse_ambient<'gc>(
553    gc: &'gc GCLock<'static, '_>,
554    sm: &mut SourceErrorManager,
555    file_name: &str,
556    source: &str,
557) -> Option<NodeRc> {
558    let buf_id = sm.add_buffer(file_name, source);
559    // Scoped so the parser — and its `&mut sm` borrow — is gone before the
560    // caller uses `sm` again. The returned node lives in the arena, so it
561    // outlives the parser.
562    let program: Option<&'gc Node<'gc>> = {
563        let lexer = JSLexer::new(
564            buf_id,
565            sm,
566            &gc.ctx().atom_table,
567            GrammarContext::AllowRegExp,
568        );
569        JSParserImpl::new(gc, lexer).parse()
570    };
571    program.map(|p| NodeRc::from_node(gc, p))
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use crate::ids::DeclId;
578    use crate::sem_context::DeclKind;
579    use hermes_ast::node::NodeKind;
580    use hermes_parser::{parse, ParseFlags};
581
582    /// Resolve `source` on the parser path and return the dump as text.
583    fn dump(source: &str) -> String {
584        let parsed = parse(source, ParseFlags::default()).unwrap();
585        let mut resolved = resolve(parsed).unwrap();
586        String::from_utf8(resolved.to_sema_dump()).unwrap()
587    }
588
589    /// The `Decl` the last statement's identifier expression binds to.
590    fn last_expression_decl(resolved: &mut ResolvedJS) -> Option<DeclId> {
591        resolved.with_program(|_gc, program, sem| {
592            let body = match program {
593                Node::Program(p) => p.body,
594                _ => panic!("root is not a Program"),
595            };
596            let stmt = body.iter().last().expect("empty program");
597            let expr = match stmt {
598                Node::ExpressionStatement(e) => e.expression,
599                _ => panic!("last statement is not an ExpressionStatement"),
600            };
601            match expr {
602                Node::Identifier(id) => sem.get_expression_decl(id),
603                _ => panic!("not an identifier expression"),
604            }
605        })
606    }
607
608    #[test]
609    fn resolves_a_reference_to_its_declaration() {
610        let parsed = parse("var x = 1; x;", ParseFlags::default()).unwrap();
611        let mut resolved = resolve(parsed).unwrap();
612        let decl = last_expression_decl(&mut resolved).expect("unresolved");
613        // `var` at the top level of a script is a property of the global
614        // object, not a plain `Var`.
615        assert_eq!(
616            resolved.sem_context().decl(decl).kind,
617            DeclKind::GlobalProperty
618        );
619        assert!(resolved.diagnostics().is_empty());
620    }
621
622    #[test]
623    fn lexical_declarations_are_scoped() {
624        let src = "let x = 1; { let x = 2; } x;";
625        let parsed = parse(src, ParseFlags::default()).unwrap();
626        let mut resolved = resolve(parsed).unwrap();
627        let decl = last_expression_decl(&mut resolved).expect("unresolved");
628        assert_eq!(resolved.sem_context().decl(decl).kind, DeclKind::Let);
629        // The inner `let x` is a second, distinct declaration.
630        let dump = String::from_utf8(resolved.to_sema_dump()).unwrap();
631        assert_eq!(dump.matches("'x' Let").count(), 2, "{dump}");
632    }
633
634    #[test]
635    fn semantic_errors_are_reported_without_printing() {
636        let parsed = parse("continue;", ParseFlags::default()).unwrap();
637        let err = resolve(parsed).unwrap_err();
638        assert_eq!(err.error_count(), 1);
639        assert_eq!(err.diagnostics().len() as u32, err.error_count());
640        assert_eq!(err.diagnostics()[0].kind, DiagKind::Error);
641        // The re-export at the crate root names the same type.
642        let _: &[crate::ResolvedDiagnostic] = err.diagnostics();
643        // `Display` is a one-line summary; the full rendering is `messages`.
644        let shown = err.to_string();
645        assert!(!shown.contains('\n'), "{shown}");
646        assert!(
647            shown.starts_with("1 semantic error; first at input:1:"),
648            "{shown}"
649        );
650        assert_eq!(err.messages().len(), 1);
651        assert!(err.messages()[0].contains('\n'), "{:?}", err.messages()[0]);
652    }
653
654    #[test]
655    fn parser_path_keeps_the_tree_on_error() {
656        let parsed = parse("continue; var x;", ParseFlags::default()).unwrap();
657        let mut resolved = resolve_for_parser(parsed);
658        assert_eq!(resolved.error_count(), 1);
659        // Still a whole program, and `var x` still resolved.
660        assert_eq!(
661            resolved.with_program(|_gc, root, _sem| root.kind()),
662            NodeKind::Program
663        );
664        let dump = String::from_utf8(resolved.to_sema_dump()).unwrap();
665        assert!(dump.contains("'x' GlobalProperty"), "{dump}");
666    }
667
668    #[test]
669    fn compile_path_transforms_the_ast_and_the_parser_path_does_not() {
670        // Constant folding is a `compile = true` rewrite.
671        let parsed = parse("var y = 1 + 2;", ParseFlags::default()).unwrap();
672        let mut compiled = resolve_for_compile(
673            parsed,
674            &CompileOptions {
675                std_globals: false,
676                ..Default::default()
677            },
678        )
679        .unwrap();
680        let folded = String::from_utf8(compiled.to_sema_dump()).unwrap();
681        assert!(!folded.contains("BinaryExpression"), "{folded}");
682
683        let unfolded = dump("var y = 1 + 2;");
684        assert!(unfolded.contains("BinaryExpression"), "{unfolded}");
685    }
686
687    #[test]
688    fn compile_path_fails_where_the_parser_path_does_not() {
689        // `with` is legal sloppy-mode JavaScript that the compiler refuses;
690        // the error is `compile`-gated (SemanticResolver.cpp), so the parser
691        // path accepts it and only marks the scope unresolvable.
692        let src = "with (o) { x; }";
693        let parsed = parse(src, ParseFlags::default()).unwrap();
694        assert_eq!(resolve_for_parser(parsed).error_count(), 0);
695
696        let parsed = parse(src, ParseFlags::default()).unwrap();
697        let err = resolve_for_compile(parsed, &CompileOptions::default())
698            .expect_err("compile path must reject it");
699        assert!(err.error_count() > 0);
700    }
701
702    #[test]
703    fn std_globals_are_ambient_declarations() {
704        let parsed = parse("print(1);", ParseFlags::default()).unwrap();
705        let mut with = resolve_for_compile(parsed, &CompileOptions::default())
706            .unwrap()
707            .to_sema_dump();
708        let parsed = parse("print(1);", ParseFlags::default()).unwrap();
709        let without = resolve_for_compile(
710            parsed,
711            &CompileOptions {
712                std_globals: false,
713                ..Default::default()
714            },
715        )
716        .unwrap()
717        .to_sema_dump();
718        // Both resolve `print`; only the first has the other ~60 ambient
719        // globals declared alongside it.
720        assert!(with.len() > without.len());
721        with.truncate(0);
722        assert!(String::from_utf8(without)
723            .unwrap()
724            .contains("'print' UndeclaredGlobalProperty"));
725    }
726
727    #[test]
728    fn user_global_definitions_are_declared() {
729        let parsed = parse("myGlobal;", ParseFlags::default()).unwrap();
730        let opts = CompileOptions {
731            std_globals: false,
732            global_definitions: vec![GlobalDefinitions {
733                file_name: "<host>".to_string(),
734                source: "var myGlobal;".to_string(),
735            }],
736        };
737        let mut resolved = resolve_for_compile(parsed, &opts).unwrap();
738        let decl = last_expression_decl(&mut resolved).expect("unresolved");
739        // An ambient `var` becomes an ambient global property, which is what
740        // distinguishes it from the implicitly created kind.
741        assert_eq!(
742            resolved.sem_context().decl(decl).kind,
743            DeclKind::UndeclaredGlobalProperty
744        );
745    }
746
747    #[test]
748    fn a_broken_global_definition_file_is_an_error() {
749        let parsed = parse("x;", ParseFlags::default()).unwrap();
750        let opts = CompileOptions {
751            std_globals: false,
752            global_definitions: vec![GlobalDefinitions {
753                file_name: "<host>".to_string(),
754                source: "var 1x;".to_string(),
755            }],
756        };
757        let err = resolve_for_compile(parsed, &opts).unwrap_err();
758        assert!(err.error_count() > 0);
759        assert!(err.to_string().contains("<host>"), "{err}");
760    }
761
762    #[test]
763    fn into_parsed_gives_back_the_resolved_tree() {
764        let parsed = parse("var y = 1 + 2;", ParseFlags::default()).unwrap();
765        let resolved = resolve_for_compile(
766            parsed,
767            &CompileOptions {
768                std_globals: false,
769                ..Default::default()
770            },
771        )
772        .unwrap();
773        // The constant folding survives; the ESTree dumper is reachable.
774        let json = resolved.into_parsed().to_estree_json(false);
775        assert!(json.contains(r#""value":3"#), "{json}");
776    }
777
778    #[test]
779    fn debug_summarizes() {
780        let parsed = parse("function f() {}", ParseFlags::default()).unwrap();
781        let resolved = resolve(parsed).unwrap();
782        let shown = format!("{resolved:?}");
783        assert!(shown.starts_with("ResolvedJS { functions: 2"), "{shown}");
784    }
785
786    #[test]
787    fn dump_is_the_hermesc_dump_sema_text() {
788        let text = dump("var x;");
789        assert!(text.starts_with("SemContext\n"), "{text}");
790        assert!(text.ends_with('\n'), "{text}");
791    }
792}