Skip to main content

hermes_ast/
dump.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//! Port of `lib/AST/ESTreeJSONDumper.cpp`. Emits an AST as ESTree JSON — the
9//! byte-for-byte differential-oracle surface (the gate lands at Parser time).
10//! The per-kind field walk + the `"type"` name live in the generated
11//! `node.rs` (`Node::dump_children` / `Node::node_type_str`); this module is the
12//! driver: modes, locations, the `raw` prop, value emission, and the public
13//! entry points.
14
15use std::collections::HashSet;
16
17use hermes_atom_table::{AtomBytes, AtomTable, INVALID_ATOM_BYTES};
18use hermes_support::json_emitter::JSONEmitter;
19use hermes_support::location::SMRange;
20use hermes_support::manager::SourceErrorManager;
21
22use crate::node::{Node, NodeKind};
23use crate::node_child::{NodeLabel, NodeList};
24
25/// Which fields to dump. Mirrors `ESTreeDumpMode`.
26#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27pub enum ESTreeDumpMode {
28    /// Hide every empty field.
29    Compact,
30    /// Hide empty fields that are in the `ESTREE_IGNORE_IF_EMPTY` set.
31    HideEmpty,
32    /// Force-dump all fields.
33    DumpAll,
34}
35
36/// Which location info to dump. Mirrors `LocationDumpMode`.
37#[derive(Clone, Copy, PartialEq, Eq, Debug)]
38pub enum LocationDumpMode {
39    /// Dump no locations.
40    None,
41    /// Only output locations: line and column.
42    Loc,
43    /// Only output byte ranges.
44    Range,
45    /// Output both locations and byte ranges.
46    LocAndRange,
47}
48
49/// Whether to include the `"raw"` property where available. Mirrors `ESTreeRawProp`.
50#[derive(Clone, Copy, PartialEq, Eq, Debug)]
51pub enum ESTreeRawProp {
52    /// Omit `"raw"`.
53    Exclude,
54    /// Emit `"raw"` where available — today `NumericLiteral`, and only when
55    /// the dumper has a `SourceErrorManager` to read the source text from.
56    Include,
57}
58
59/// Depth limit mirroring C++ `depthCounterGuard(128)`.
60const MAX_DEPTH: usize = 128;
61
62/// The dumper. `'a` borrows the emitter/atoms/sm/filter; node refs are passed
63/// per-call (generic over their own lifetime).
64pub struct ESTreeJSONDumper<'a, 'w> {
65    json: &'a mut JSONEmitter<'w>,
66    atoms: &'a AtomTable,
67    sm: Option<&'a SourceErrorManager>,
68    mode: ESTreeDumpMode,
69    loc_mode: LocationDumpMode,
70    raw_prop: ESTreeRawProp,
71    include_source_locs: Option<&'a HashSet<NodeKind>>,
72    depth: usize,
73}
74
75impl<'a, 'w> ESTreeJSONDumper<'a, 'w> {
76    /// Whether `DUMP_KEY_VALUE_PAIR` would skip an empty field.
77    fn skip_empty(&self, is_empty: bool, ignore_if_empty: bool) -> bool {
78        if !is_empty {
79            return false;
80        }
81        match self.mode {
82            ESTreeDumpMode::Compact => true,
83            ESTreeDumpMode::HideEmpty => ignore_if_empty,
84            ESTreeDumpMode::DumpAll => false,
85        }
86    }
87
88    // --- field_* helpers, called from the generated Node::dump_children. ---
89
90    pub(crate) fn field_node<'n>(&mut self, key: &str, node: Option<&'n Node<'n>>, ignore: bool) {
91        if self.skip_empty(node.is_none(), ignore) {
92            return;
93        }
94        self.json.emit_key(key);
95        self.dump_node_ptr(node);
96    }
97
98    pub(crate) fn field_list<'n>(&mut self, key: &str, list: NodeList<'n>, ignore: bool) {
99        if self.skip_empty(list.is_empty(), ignore) {
100            return;
101        }
102        self.json.emit_key(key);
103        self.dump_node_list(list);
104    }
105
106    pub(crate) fn field_bool(&mut self, key: &str, val: bool, ignore: bool) {
107        // isEmpty(NodeBoolean) == !val
108        if self.skip_empty(!val, ignore) {
109            return;
110        }
111        self.json.emit_key(key);
112        self.json.emit_bool(val);
113    }
114
115    pub(crate) fn field_number(&mut self, key: &str, val: f64, ignore: bool) {
116        // isEmpty(NodeNumber) == false (never empty).
117        if self.skip_empty(false, ignore) {
118            return;
119        }
120        self.json.emit_key(key);
121        self.json.emit_f64(val);
122    }
123
124    pub(crate) fn field_label(&mut self, key: &str, label: NodeLabel, ignore: bool) {
125        // isEmpty(NodeLabel) == false (never empty).
126        if self.skip_empty(false, ignore) {
127            return;
128        }
129        self.json.emit_key(key);
130        self.dump_label(label);
131    }
132
133    // --- dumpNode overloads. ---
134
135    fn dump_node_ptr<'n>(&mut self, node: Option<&'n Node<'n>>) {
136        let node = match node {
137            Some(n) => n,
138            None => {
139                self.json.emit_null_value();
140                return;
141            }
142        };
143        self.depth += 1;
144        if self.depth > MAX_DEPTH {
145            // Port of the StackOverflowGuard overflow path: emit `null` and
146            // stop recursing. C++ also calls `sm_->error(...)` here, but our
147            // dumper holds a shared `&SourceErrorManager` (it resolves coords),
148            // so we drop the diagnostic — the 128-depth guard is a safety net,
149            // not a tested surface (see the module doc / plan deviation #2).
150            self.json.emit_null_value();
151            self.depth -= 1;
152            return;
153        }
154        self.visit(node);
155        self.depth -= 1;
156    }
157
158    fn dump_node_list<'n>(&mut self, list: NodeList<'n>) {
159        self.json.open_array();
160        for n in list.iter() {
161            self.dump_node_ptr(Some(n));
162        }
163        self.json.close_array();
164    }
165
166    fn dump_label(&mut self, label: AtomBytes) {
167        if label == INVALID_ATOM_BYTES {
168            self.json.emit_null_value();
169            return;
170        }
171        let bytes = self.atoms.bytes(label);
172        let units = hermes_support::utf8::convert_utf8_with_surrogates_to_utf16(bytes);
173        self.json.emit_u16(&units);
174    }
175
176    // --- visit + locations + raw. ---
177
178    fn visit<'n>(&mut self, node: &'n Node<'n>) {
179        self.json.open_dict();
180        self.json.emit_key("type");
181        self.json.emit_str(node.node_type_str());
182        node.dump_children(self);
183        if node.kind() == NodeKind::NumericLiteral && self.raw_prop == ESTreeRawProp::Include {
184            self.dump_raw(node);
185        }
186        self.print_source_location(node);
187        self.json.close_dict();
188    }
189
190    /// NumericLiteral `"raw"` — the source text. Requires `sm` (offset model
191    /// has no location pointer); omitted when `sm` is None (documented
192    /// deviation #1).
193    fn dump_raw<'n>(&mut self, node: &'n Node<'n>) {
194        let sm = match self.sm {
195            Some(sm) => sm,
196            None => return,
197        };
198        let r = node.range();
199        if !range_is_valid(r) {
200            return;
201        }
202        let buf = sm.find_buffer_for_loc(r.start);
203        // Skip `raw` rather than panic if the range is out of the buffer's
204        // bounds (only reachable with synthetic/malformed ranges; parser output
205        // is always in-buffer).
206        let bytes = match buf.bytes().get(r.start.offset as usize..r.end.offset as usize) {
207            Some(b) => b,
208            None => return,
209        };
210        self.json.emit_key("raw");
211        // Numeric-literal source text is ASCII; route through the WTF-8 codec
212        // for uniformity with C++ primitiveEmitString.
213        let units = hermes_support::utf8::convert_utf8_with_surrogates_to_utf16(bytes);
214        self.json.emit_u16(&units);
215    }
216
217    fn print_source_location<'n>(&mut self, node: &'n Node<'n>) {
218        if self.loc_mode == LocationDumpMode::None {
219            return;
220        }
221        if let Some(set) = self.include_source_locs {
222            if !set.contains(&node.kind()) {
223                return;
224            }
225        }
226        let sm = match self.sm {
227            Some(sm) => sm,
228            None => return,
229        };
230        let r = node.range();
231        if !range_is_valid(r) {
232            return;
233        }
234        // Mirror C++ `printSourceLocation`: if either endpoint fails to resolve
235        // (`findBufferLineAndLoc` returns false), skip the whole loc+range block.
236        // Our offset model can't fail to resolve an in-buffer offset, so the
237        // analog is an offset past the buffer's content length. Both endpoints
238        // share a buffer (guaranteed by `range_is_valid`).
239        let buf = sm.find_buffer_for_loc(r.start);
240        let buf_len = buf.bytes().len();
241        if r.start.offset as usize > buf_len || r.end.offset as usize > buf_len {
242            return;
243        }
244        let start = sm.find_coords(r.start);
245        let end = sm.find_coords(r.end);
246
247        if matches!(
248            self.loc_mode,
249            LocationDumpMode::Loc | LocationDumpMode::LocAndRange
250        ) {
251            self.json.emit_key("loc");
252            self.json.open_dict();
253            self.json.emit_key("start");
254            self.json.open_dict();
255            self.json.emit_key("line");
256            self.json.emit_u64(start.line as u64);
257            self.json.emit_key("column");
258            self.json.emit_u64(start.col as u64);
259            self.json.close_dict();
260            self.json.emit_key("end");
261            self.json.open_dict();
262            self.json.emit_key("line");
263            self.json.emit_u64(end.line as u64);
264            self.json.emit_key("column");
265            self.json.emit_u64(end.col as u64);
266            self.json.close_dict();
267            self.json.close_dict();
268        }
269
270        if matches!(
271            self.loc_mode,
272            LocationDumpMode::Range | LocationDumpMode::LocAndRange
273        ) {
274            self.json.emit_key("range");
275            self.json.open_array();
276            dump_sm_range_json(self.json, r);
277            self.json.close_array();
278        }
279    }
280}
281
282/// Whether a range is set (mirrors C++ `SMRange::isValid()`). In the offset
283/// model an `SMLoc` always carries a buffer, so we treat a range as valid when
284/// its endpoints are in the same buffer and ordered.
285fn range_is_valid(r: SMRange) -> bool {
286    r.start.source == r.end.source && r.start.offset <= r.end.offset
287}
288
289/// Emit a range as the two buffer-relative offsets. Port of `dumpSMRangeJSON`
290/// (the caller wraps these in an array). In the offset model the offsets are the
291/// values directly. Kept `pub` to mirror the C++ public `dumpSMRangeJSON`
292/// (declared in `ESTreeJSONDumper.h`).
293pub fn dump_sm_range_json(json: &mut JSONEmitter, rng: SMRange) {
294    json.emit_u64(rng.start.offset as u64);
295    json.emit_u64(rng.end.offset as u64);
296}
297
298// --- public entry points (mirror the C++ dumpESTreeJSON overloads). ---
299
300/// Dump `root` to `out` without locations. Mirrors the no-`sm`
301/// `dumpESTreeJSON(os, root, pretty, mode)` — `"raw"` is omitted (no buffer).
302pub fn dump_estree_json<'n>(
303    out: &mut String,
304    root: &'n Node<'n>,
305    pretty: bool,
306    mode: ESTreeDumpMode,
307    atoms: &AtomTable,
308) {
309    let mut json = JSONEmitter::new(out, pretty);
310    {
311        let mut d = ESTreeJSONDumper {
312            json: &mut json,
313            atoms,
314            sm: None,
315            mode,
316            loc_mode: LocationDumpMode::None,
317            raw_prop: ESTreeRawProp::Include,
318            include_source_locs: None,
319            depth: 0,
320        };
321        d.dump_node_ptr(Some(root));
322    }
323    json.end_jsonl();
324}
325
326/// Dump `root` with a source manager and a location mode. Mirrors the
327/// `dumpESTreeJSON(os, root, pretty, mode, sm, locMode, rawProp)` overload.
328#[allow(clippy::too_many_arguments)]
329pub fn dump_estree_json_with_sm<'n>(
330    out: &mut String,
331    root: &'n Node<'n>,
332    pretty: bool,
333    mode: ESTreeDumpMode,
334    sm: &SourceErrorManager,
335    loc_mode: LocationDumpMode,
336    raw_prop: ESTreeRawProp,
337    atoms: &AtomTable,
338) {
339    let mut json = JSONEmitter::new(out, pretty);
340    {
341        let mut d = ESTreeJSONDumper {
342            json: &mut json,
343            atoms,
344            sm: Some(sm),
345            mode,
346            loc_mode,
347            raw_prop,
348            include_source_locs: None,
349            depth: 0,
350        };
351        d.dump_node_ptr(Some(root));
352    }
353    json.end_jsonl();
354}