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}