Skip to main content

svelte_syntax/
js.rs

1use std::sync::Arc;
2
3use oxc_allocator::Allocator;
4
5use oxc_ast::ast::{
6    BindingPattern, Expression, FormalParameter, FormalParameterRest, FormalParameters, Program,
7    Statement, VariableDeclaration,
8};
9use oxc_ast::CommentKind;
10use oxc_diagnostics::OxcDiagnostic;
11use oxc_parser::{ParseOptions, Parser, ParserReturn};
12use oxc_span::{GetSpan, SourceType};
13
14use self_cell::self_cell;
15
16struct ProgramOwner {
17    source: Box<str>,
18    allocator: Allocator,
19    source_type: SourceType,
20    options: ParseOptions,
21}
22
23self_cell! {
24    struct ParsedProgramCell {
25        owner: ProgramOwner,
26
27        #[covariant]
28        dependent: ParserReturn,
29    }
30}
31
32struct ExpressionOwner {
33    source: Box<str>,
34    allocator: Allocator,
35    source_type: SourceType,
36}
37
38struct ParsedExpressionData<'a> {
39    expression: Expression<'a>,
40}
41
42self_cell! {
43    struct ParsedExpressionCell {
44        owner: ExpressionOwner,
45
46        #[covariant]
47        dependent: ParsedExpressionData,
48    }
49}
50
51/// Reusable OXC-backed JavaScript/TypeScript program handle.
52///
53/// This owns the source text and arena allocator required by the parsed OXC
54/// AST so downstream tools can inspect the AST without reparsing or converting
55/// through ESTree JSON.
56pub struct JsProgram {
57    cell: ParsedProgramCell,
58}
59
60impl std::fmt::Debug for JsProgram {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("JsProgram")
63            .field("source", &self.source())
64            .field("source_type", &self.source_type())
65            .field("panicked", &self.panicked())
66            .field("error_count", &self.errors().len())
67            .finish()
68    }
69}
70
71impl PartialEq for JsProgram {
72    fn eq(&self, other: &Self) -> bool {
73        self.source() == other.source() && self.source_type() == other.source_type()
74    }
75}
76
77impl Eq for JsProgram {}
78
79// SAFETY: JsProgram owns all its data (Box<str>, Allocator, parsed AST).
80// The !Send/!Sync comes from self_cell's self-referential borrow, but the
81// underlying data is fully owned and not shared across threads unsafely.
82unsafe impl Send for JsProgram {}
83unsafe impl Sync for JsProgram {}
84
85impl JsProgram {
86    /// Parse JavaScript or TypeScript source into a program AST.
87    #[must_use]
88    pub fn parse(source: impl Into<Box<str>>, source_type: SourceType) -> Self {
89        Self::parse_with_options(source, source_type, ParseOptions::default())
90    }
91
92    /// Parse with explicit OXC parser options.
93    #[must_use]
94    pub fn parse_with_options(
95        source: impl Into<Box<str>>,
96        source_type: SourceType,
97        options: ParseOptions,
98    ) -> Self {
99        let owner = ProgramOwner {
100            source: source.into(),
101            allocator: Allocator::default(),
102            source_type,
103            options,
104        };
105        let cell = ParsedProgramCell::new(owner, |owner| {
106            Parser::new(&owner.allocator, owner.source.as_ref(), owner.source_type)
107                .with_options(owner.options)
108                .parse()
109        });
110        Self { cell }
111    }
112
113    /// Return the original source text.
114    #[must_use]
115    pub fn source(&self) -> &str {
116        self.cell.borrow_owner().source.as_ref()
117    }
118
119    /// Return the OXC source type used for parsing.
120    #[must_use]
121    pub fn source_type(&self) -> SourceType {
122        self.cell.borrow_owner().source_type
123    }
124
125    /// Return the parsed OXC program AST.
126    #[must_use]
127    pub fn program(&self) -> &Program<'_> {
128        &self.cell.borrow_dependent().program
129    }
130
131    /// Return any parse errors (the program may still be partially valid).
132    pub fn errors(&self) -> &[OxcDiagnostic] {
133        &self.cell.borrow_dependent().errors
134    }
135
136    /// Return `true` if the parser panicked during parsing.
137    #[must_use]
138    pub fn panicked(&self) -> bool {
139        self.cell.borrow_dependent().panicked
140    }
141
142    /// Return `true` if the source uses Flow type annotations.
143    #[must_use]
144    pub fn is_flow_language(&self) -> bool {
145        self.cell.borrow_dependent().is_flow_language
146    }
147
148    /// Access the full parser return for consumers that need module records,
149    /// irregular whitespaces, or other parser metadata.
150    #[must_use]
151    pub fn parser_return(&self) -> &ParserReturn<'_> {
152        self.cell.borrow_dependent()
153    }
154
155    /// Serialize the full Program AST to ESTree JSON, with span offsets adjusted
156    /// by `offset` (typically `content_start` from the Script node), and `loc`
157    /// fields injected.
158    ///
159    /// `full_source` is the full .svelte source.
160    /// `offset` is `content_start` (byte position where script content begins).
161    /// `script_tag_end` is the byte position after the closing `</script>` tag.
162    #[must_use]
163    pub fn to_estree_json(&self, full_source: &str, offset: usize, script_tag_end: usize) -> String {
164        use oxc_estree::ESTree;
165
166        let program = self.program();
167        let raw_json = if self.source_type().is_typescript() {
168            let mut ser = oxc_estree::CompactTSSerializer::new(false);
169            program.serialize(&mut ser);
170            ser.into_string()
171        } else {
172            let mut ser = oxc_estree::CompactJSSerializer::new(false);
173            program.serialize(&mut ser);
174            ser.into_string()
175        };
176
177        let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&raw_json) else {
178            return raw_json;
179        };
180
181        // Fix TS-serializer-specific issues
182        if self.source_type().is_typescript() {
183            fix_template_element_spans(&mut value);
184            // TS serializer's Program span starts at the first token, not at 0.
185            // Fix to match JS serializer behavior (start=0, covering all content).
186            if let serde_json::Value::Object(ref mut map) = value {
187                if crate::estree::node_type(map) == "Program" {
188                    map.insert("start".to_string(), serde_json::json!(0));
189                }
190            }
191        }
192
193        // Compute the starting line number for the script content by finding
194        // what line `offset` falls on in the full source. Acorn uses this as
195        // the starting line for loc computation within the script content.
196        let start_line = line_at_offset(full_source, offset);
197
198        let content = self.source();
199        adjust_program_json(&mut value, content, offset, start_line);
200
201        // Fix Program's loc.end to point at the </script> tag end (upstream behavior)
202        // and add leadingComments/trailingComments from OXC's comment data.
203        if let serde_json::Value::Object(map) = &mut value
204            && crate::estree::node_type(map) == "Program"
205        {
206            if let Some(serde_json::Value::Object(loc)) = map.get_mut("loc") {
207                let (end_line, end_col) = line_column_at_offset(full_source, script_tag_end, 1);
208                loc.insert("end".to_string(), serde_json::json!({
209                    "line": end_line,
210                    "column": end_col
211                }));
212            }
213
214            // Build comment entries from OXC program comments
215            let comment_entries: Vec<(u32, u32, serde_json::Value)> = program
216                .comments
217                .iter()
218                .map(|comment| {
219                    let kind_str = match comment.kind {
220                        CommentKind::Line => "Line",
221                        CommentKind::SingleLineBlock | CommentKind::MultiLineBlock => "Block",
222                    };
223                    let content_span = comment.content_span();
224                    let value_text =
225                        &content[content_span.start as usize..content_span.end as usize];
226                    let abs_start = comment.span.start as usize + offset;
227                    let abs_end = comment.span.end as usize + offset;
228                    (
229                        comment.span.start + offset as u32,
230                        comment.span.end + offset as u32,
231                        serde_json::json!({
232                            "type": kind_str,
233                            "value": value_text,
234                            "start": abs_start,
235                            "end": abs_end
236                        }),
237                    )
238                })
239                .collect();
240
241            // Attach comments to AST nodes using acorn-style algorithm
242            attach_comments_to_json_tree(&mut value, &comment_entries, full_source);
243        }
244
245        serde_json::to_string(&value).unwrap_or(raw_json)
246    }
247}
248
249/// Acorn-style comment attachment: recursively walk the JSON AST and attach
250/// leading/trailing comments to the nearest sibling nodes in container arrays
251/// (Program.body, BlockStatement.body, ArrayExpression.elements, ObjectExpression.properties).
252pub(crate) fn attach_comments_to_json_tree(
253    root: &mut serde_json::Value,
254    comments: &[(u32, u32, serde_json::Value)],
255    content: &str,
256) {
257    if comments.is_empty() {
258        return;
259    }
260
261    // Recursively find containers (arrays of AST nodes) and attach comments to their children.
262    fn walk(value: &mut serde_json::Value, comments: &[(u32, u32, serde_json::Value)], content: &str) {
263        let serde_json::Value::Object(map) = value else {
264            return;
265        };
266
267        // Determine which child arrays to process for comment attachment
268        let node_type = map
269            .get("type")
270            .and_then(|v| v.as_str())
271            .unwrap_or("");
272        let container_keys: &[&str] = match node_type {
273            "Program" | "BlockStatement" | "StaticBlock" | "ClassBody" => &["body"],
274            "SwitchCase" => &["consequent"],
275            "ArrayExpression" => &["elements"],
276            "ObjectExpression" => &["properties"],
277            _ => &[],
278        };
279
280        // For Program with empty body, attach all comments as trailingComments
281        if node_type == "Program" {
282            let body_empty = map
283                .get("body")
284                .and_then(|v| v.as_array())
285                .is_none_or(|a| a.is_empty());
286            if body_empty {
287                let node_start = map.get("start").and_then(|v| v.as_u64()).unwrap_or(0);
288                let node_end = map.get("end").and_then(|v| v.as_u64()).unwrap_or(u64::MAX);
289                let trailing: Vec<serde_json::Value> = comments
290                    .iter()
291                    .filter(|(cs, ce, _)| *cs as u64 >= node_start && (*ce as u64) <= node_end)
292                    .map(|(_, _, e)| e.clone())
293                    .collect();
294                if !trailing.is_empty() {
295                    map.insert(
296                        "trailingComments".to_string(),
297                        serde_json::Value::Array(trailing),
298                    );
299                }
300                return;
301            }
302        }
303
304        // Get this node's span to scope comments
305        let node_start = map.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
306        let node_end = map.get("end").and_then(|v| v.as_u64()).unwrap_or(u32::MAX as u64) as u32;
307
308        for key in container_keys {
309            if let Some(serde_json::Value::Array(children)) = map.get_mut(*key) {
310                attach_comments_to_siblings(children, comments, content, node_start, node_end);
311            }
312        }
313
314        // Recurse into all child values
315        for v in map.values_mut() {
316            match v {
317                serde_json::Value::Object(_) => walk(v, comments, content),
318                serde_json::Value::Array(arr) => {
319                    for item in arr.iter_mut() {
320                        walk(item, comments, content);
321                    }
322                }
323                _ => {}
324            }
325        }
326    }
327
328    walk(root, comments, content);
329}
330
331/// Attach comments to sibling nodes in a container array.
332/// Only considers comments within `container_start..container_end`.
333fn attach_comments_to_siblings(
334    children: &mut [serde_json::Value],
335    comments: &[(u32, u32, serde_json::Value)],
336    content: &str,
337    container_start: u32,
338    container_end: u32,
339) {
340    if children.is_empty() || comments.is_empty() {
341        return;
342    }
343
344    // Collect (start, end) for each child
345    let positions: Vec<(u64, u64)> = children
346        .iter()
347        .map(|c| {
348            let s = c.get("start").and_then(|v| v.as_u64()).unwrap_or(0);
349            let e = c.get("end").and_then(|v| v.as_u64()).unwrap_or(0);
350            (s, e)
351        })
352        .collect();
353
354    // For each child, collect leading and trailing comments
355    let n = children.len();
356    let mut child_leading: Vec<Vec<serde_json::Value>> = vec![Vec::new(); n];
357    let mut child_trailing: Vec<Vec<serde_json::Value>> = vec![Vec::new(); n];
358
359    for &(c_start, c_end, ref entry) in comments {
360        // Only consider comments within the container's span
361        if c_start < container_start || c_end > container_end {
362            continue;
363        }
364
365        let c_start_u64 = c_start as u64;
366        let c_end_u64 = c_end as u64;
367
368        // Find which gap this comment falls in
369        // Before first child
370        if c_end_u64 <= positions[0].0 {
371            child_leading[0].push(entry.clone());
372            continue;
373        }
374        // After last child
375        if c_start_u64 >= positions[n - 1].1 {
376            child_trailing[n - 1].push(entry.clone());
377            continue;
378        }
379        // Between children
380        for i in 0..n - 1 {
381            if c_start_u64 >= positions[i].1 && c_end_u64 <= positions[i + 1].0 {
382                // Comment is between child[i] and child[i+1].
383                // Heuristic: if comment is on the same line as child[i]'s end, it's trailing.
384                // Otherwise, it's leading of child[i+1].
385                let child_end_pos = positions[i].1 as usize;
386                let comment_start_pos = c_start as usize;
387                let same_line = child_end_pos <= content.len()
388                    && comment_start_pos <= content.len()
389                    && !content[child_end_pos..comment_start_pos].contains('\n');
390                if same_line {
391                    child_trailing[i].push(entry.clone());
392                } else {
393                    child_leading[i + 1].push(entry.clone());
394                }
395                break;
396            }
397        }
398        // If comment is inside a child (not in a gap), skip — it will be handled recursively
399    }
400
401    // Attach to JSON nodes
402    for (i, child) in children.iter_mut().enumerate() {
403        if let serde_json::Value::Object(map) = child {
404            if !child_leading[i].is_empty() {
405                map.insert(
406                    "leadingComments".to_string(),
407                    serde_json::Value::Array(child_leading[i].clone()),
408                );
409            }
410            if !child_trailing[i].is_empty() {
411                map.insert(
412                    "trailingComments".to_string(),
413                    serde_json::Value::Array(child_trailing[i].clone()),
414                );
415            }
416        }
417    }
418}
419
420use crate::estree::{SpanAdjustConfig, adjust_spans_and_loc, line_at_offset, line_column_at_offset};
421
422/// Recursively adjust span offsets by `offset` and inject `loc` fields.
423/// `content_source` is the script content text, `start_line` is the 1-based
424/// line number in the full source where the content begins.
425fn adjust_program_json(
426    value: &mut serde_json::Value,
427    content_source: &str,
428    offset: usize,
429    start_line: usize,
430) {
431    adjust_spans_and_loc(value, &SpanAdjustConfig {
432        offset: offset as i64,
433        source: content_source,
434        base_line: start_line,
435        column_offset: 0,
436        with_character: false,
437        program_mode: true,
438    });
439}
440
441/// Recursively adjust span offsets by `offset` and inject `loc` fields for expressions.
442/// `column_offset` is added to loc columns for nodes not on the first line
443/// (matching upstream's behavior for destructured patterns).
444/// Like `adjust_expression_json_inner` but with explicit column offset.
445pub(crate) fn adjust_expression_json_with_column_offset(
446    value: &mut serde_json::Value,
447    full_source: &str,
448    offset: i64,
449    column_offset: usize,
450) {
451    adjust_expression_json_inner(value, full_source, offset, column_offset, false);
452}
453
454/// Like `adjust_expression_json_with_column_offset` but also adds `character` to loc.
455pub(crate) fn adjust_expression_json_with_character(
456    value: &mut serde_json::Value,
457    full_source: &str,
458    offset: i64,
459) {
460    adjust_expression_json_inner(value, full_source, offset, 0, true);
461}
462
463fn adjust_expression_json_inner(
464    value: &mut serde_json::Value,
465    full_source: &str,
466    offset: i64,
467    column_offset: usize,
468    with_character: bool,
469) {
470    adjust_spans_and_loc(value, &SpanAdjustConfig {
471        offset,
472        source: full_source,
473        base_line: 1,
474        column_offset,
475        with_character,
476        program_mode: false,
477    });
478}
479
480/// Re-export for callers that reference `crate::js::fix_template_element_spans`.
481pub(crate) use crate::estree::fix_template_element_spans;
482
483/// Re-export for callers that reference `crate::js::unwrap_parenthesized_expression`.
484pub(crate) use crate::estree::unwrap_parenthesized_expression;
485
486/// Reusable OXC-backed JavaScript/TypeScript expression handle.
487///
488/// This owns the source text and arena allocator required by the parsed OXC
489/// AST so downstream tools can inspect a template/script expression without
490/// reparsing.
491pub struct JsExpression {
492    cell: ParsedExpressionCell,
493}
494
495impl std::fmt::Debug for JsExpression {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        f.debug_struct("JsExpression")
498            .field("source", &self.source())
499            .field("source_type", &self.source_type())
500            .finish()
501    }
502}
503
504impl PartialEq for JsExpression {
505    fn eq(&self, other: &Self) -> bool {
506        self.source() == other.source() && self.source_type() == other.source_type()
507    }
508}
509
510impl Eq for JsExpression {}
511
512// SAFETY: JsExpression owns all its data (Box<str>, Allocator, parsed AST).
513// The !Send/!Sync comes from self_cell's self-referential borrow, but the
514// underlying data is fully owned and not shared across threads unsafely.
515unsafe impl Send for JsExpression {}
516unsafe impl Sync for JsExpression {}
517
518impl JsExpression {
519    /// Parse a single JavaScript or TypeScript expression.
520    pub fn parse(
521        source: impl Into<Box<str>>,
522        source_type: SourceType,
523    ) -> Result<Self, Box<[OxcDiagnostic]>> {
524        let owner = ExpressionOwner {
525            source: source.into(),
526            allocator: Allocator::default(),
527            source_type,
528        };
529        let cell = ParsedExpressionCell::try_new(owner, |owner| {
530            Parser::new(&owner.allocator, owner.source.as_ref(), owner.source_type)
531                .parse_expression()
532                .map(|expression| ParsedExpressionData { expression })
533                .map_err(|errors| errors.into_boxed_slice())
534        })?;
535        Ok(Self { cell })
536    }
537
538    /// Return the original source text.
539    #[must_use]
540    pub fn source(&self) -> &str {
541        self.cell.borrow_owner().source.as_ref()
542    }
543
544    /// Return the OXC source type used for parsing.
545    #[must_use]
546    pub fn source_type(&self) -> SourceType {
547        self.cell.borrow_owner().source_type
548    }
549
550    /// Return the parsed OXC expression AST node.
551    #[must_use]
552    pub fn expression(&self) -> &Expression<'_> {
553        &self.cell.borrow_dependent().expression
554    }
555}
556
557/// Reusable OXC-backed binding pattern handle.
558///
559/// Svelte stores certain binding and parameter positions in the same logical
560/// expression slot as ordinary expressions. This handle keeps those nodes in
561/// OXC form without routing through ESTree compatibility trees.
562pub struct JsPattern {
563    source: Box<str>,
564    wrapper: Arc<JsExpression>,
565}
566
567impl std::fmt::Debug for JsPattern {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        f.debug_struct("JsPattern")
570            .field("source", &self.source())
571            .finish()
572    }
573}
574
575/// Reusable OXC-backed formal parameter list handle.
576///
577/// This preserves richer parameter information like rest/default/type metadata
578/// while still exposing each parameter binding pattern without reparsing.
579pub struct JsParameters {
580    source: Box<str>,
581    wrapper: Arc<JsExpression>,
582}
583
584impl std::fmt::Debug for JsParameters {
585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586        f.debug_struct("JsParameters")
587            .field("source", &self.source())
588            .field("parameter_count", &self.parameters().items.len())
589            .field("has_rest", &self.parameters().rest.is_some())
590            .finish()
591    }
592}
593
594impl PartialEq for JsParameters {
595    fn eq(&self, other: &Self) -> bool {
596        self.source() == other.source()
597    }
598}
599
600impl Eq for JsParameters {}
601
602impl JsParameters {
603    pub fn parse(source: impl Into<Box<str>>) -> Result<Self, Box<[OxcDiagnostic]>> {
604        let source = source.into();
605        let wrapper_source = format!("({})=>{{}}", source);
606        let wrapper = Arc::new(JsExpression::parse(
607            wrapper_source,
608            SourceType::ts().with_module(true),
609        )?);
610        let _ = Self::parameters_from_wrapper(&wrapper).ok_or_else(|| {
611            vec![OxcDiagnostic::error(
612                "failed to recover formal parameters from wrapper",
613            )]
614            .into_boxed_slice()
615        })?;
616        Ok(Self { source, wrapper })
617    }
618
619    #[must_use]
620    pub fn source(&self) -> &str {
621        self.source.as_ref()
622    }
623
624    #[must_use]
625    pub fn parameters(&self) -> &FormalParameters<'_> {
626        Self::parameters_from_wrapper(&self.wrapper).expect("validated parsed parameters")
627    }
628
629    #[must_use]
630    pub fn parameter(&self, index: usize) -> Option<&FormalParameter<'_>> {
631        self.parameters().items.get(index)
632    }
633
634    #[must_use]
635    pub fn rest_parameter(&self) -> Option<&FormalParameterRest<'_>> {
636        self.parameters().rest.as_deref()
637    }
638
639    fn parameters_from_wrapper(wrapper: &JsExpression) -> Option<&FormalParameters<'_>> {
640        match wrapper.expression() {
641            Expression::ArrowFunctionExpression(function) => Some(&function.params),
642            _ => None,
643        }
644    }
645}
646
647impl PartialEq for JsPattern {
648    fn eq(&self, other: &Self) -> bool {
649        self.source() == other.source()
650    }
651}
652
653impl Eq for JsPattern {}
654
655// SAFETY: JsPattern contains only owned data (Box<str>) and an Arc<JsExpression>
656// which is itself Send+Sync.
657unsafe impl Send for JsPattern {}
658unsafe impl Sync for JsPattern {}
659
660impl JsPattern {
661    pub fn parse(source: impl Into<Box<str>>) -> Result<Self, Box<[OxcDiagnostic]>> {
662        let source = source.into();
663        let wrapper_source = format!("({})=>{{}}", source);
664        let wrapper = Arc::new(JsExpression::parse(
665            wrapper_source,
666            SourceType::ts().with_module(true),
667        )?);
668
669        let _ = Self::pattern_from_wrapper(&wrapper).ok_or_else(|| {
670            vec![OxcDiagnostic::error(
671                "failed to recover binding pattern from wrapper",
672            )]
673            .into_boxed_slice()
674        })?;
675
676        Ok(Self { source, wrapper })
677    }
678
679    #[must_use]
680    pub fn source(&self) -> &str {
681        self.source.as_ref()
682    }
683
684    #[must_use]
685    pub fn pattern(&self) -> &BindingPattern<'_> {
686        Self::pattern_from_wrapper(&self.wrapper).expect("validated parsed pattern")
687    }
688
689    fn pattern_from_wrapper(wrapper: &JsExpression) -> Option<&BindingPattern<'_>> {
690        match wrapper.expression() {
691            Expression::ArrowFunctionExpression(function) => function
692                .params
693                .items
694                .first()
695                .map(|parameter| &parameter.pattern),
696            _ => None,
697        }
698    }
699}
700
701impl JsProgram {
702    #[must_use]
703    pub fn statement(&self, index: usize) -> Option<&Statement<'_>> {
704        self.program().body.get(index)
705    }
706
707    #[must_use]
708    pub fn statement_source(&self, index: usize) -> Option<&str> {
709        let statement = self.statement(index)?;
710        let span = statement.span();
711        self.source()
712            .get(span.start as usize..span.end as usize)
713    }
714
715    #[must_use]
716    pub fn variable_declaration(&self, index: usize) -> Option<&VariableDeclaration<'_>> {
717        match self.statement(index)? {
718            Statement::VariableDeclaration(declaration) => Some(declaration),
719            Statement::ExportNamedDeclaration(declaration) => match declaration.declaration.as_ref() {
720                Some(oxc_ast::ast::Declaration::VariableDeclaration(declaration)) => Some(declaration),
721                _ => None,
722            },
723            _ => None,
724        }
725    }
726}
727
728#[cfg(test)]
729mod tests {
730    use oxc_ast::ast::{BindingPattern, Expression, Statement};
731    use oxc_span::SourceType;
732
733    use super::{JsExpression, JsPattern, JsProgram};
734
735    #[test]
736    fn parsed_js_program_exposes_reusable_oxc_program() {
737        let parsed = JsProgram::parse("export const answer = 42;", SourceType::mjs());
738
739        assert_eq!(parsed.source(), "export const answer = 42;");
740        assert!(parsed.errors().is_empty());
741        assert!(!parsed.panicked());
742        assert!(matches!(
743            parsed.program().body.first(),
744            Some(Statement::ExportNamedDeclaration(_))
745        ));
746    }
747
748    #[test]
749    fn parsed_js_expression_exposes_reusable_oxc_expression() {
750        let parsed = JsExpression::parse("count + 1", SourceType::ts().with_module(true))
751            .expect("expression should parse");
752
753        assert_eq!(parsed.source(), "count + 1");
754        assert!(matches!(
755            parsed.expression(),
756            Expression::BinaryExpression(_)
757        ));
758    }
759
760    #[test]
761    fn parsed_js_expression_returns_oxc_errors_on_invalid_input() {
762        let errors = JsExpression::parse("foo(", SourceType::ts().with_module(true))
763            .err()
764            .expect("expression should fail");
765
766        assert!(!errors.is_empty());
767    }
768
769    #[test]
770    fn parsed_js_pattern_exposes_reusable_oxc_pattern() {
771        let parsed =
772            JsPattern::parse("{ count, items: [item] }").expect("pattern should parse");
773
774        assert!(matches!(parsed.pattern(), BindingPattern::ObjectPattern(_)));
775    }
776}