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_diagnostics::OxcDiagnostic;
10use oxc_parser::{ParseOptions, Parser, ParserReturn};
11use oxc_span::{GetSpan, SourceType};
12
13use self_cell::self_cell;
14
15struct ProgramOwner {
16    source: Box<str>,
17    allocator: Allocator,
18    source_type: SourceType,
19    options: ParseOptions,
20}
21
22self_cell! {
23    struct ParsedProgramCell {
24        owner: ProgramOwner,
25
26        #[covariant]
27        dependent: ParserReturn,
28    }
29}
30
31struct ExpressionOwner {
32    source: Box<str>,
33    allocator: Allocator,
34    source_type: SourceType,
35}
36
37struct ParsedExpressionData<'a> {
38    expression: Expression<'a>,
39}
40
41self_cell! {
42    struct ParsedExpressionCell {
43        owner: ExpressionOwner,
44
45        #[covariant]
46        dependent: ParsedExpressionData,
47    }
48}
49
50/// Reusable OXC-backed JavaScript/TypeScript program handle.
51///
52/// This owns the source text and arena allocator required by the parsed OXC
53/// AST so downstream tools can inspect the AST without reparsing or converting
54/// through ESTree JSON.
55pub struct ParsedJsProgram {
56    cell: ParsedProgramCell,
57}
58
59impl std::fmt::Debug for ParsedJsProgram {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("ParsedJsProgram")
62            .field("source", &self.source())
63            .field("source_type", &self.source_type())
64            .field("panicked", &self.panicked())
65            .field("error_count", &self.errors().len())
66            .finish()
67    }
68}
69
70impl PartialEq for ParsedJsProgram {
71    fn eq(&self, other: &Self) -> bool {
72        self.source() == other.source() && self.source_type() == other.source_type()
73    }
74}
75
76impl Eq for ParsedJsProgram {}
77
78impl ParsedJsProgram {
79    /// Parse JavaScript or TypeScript source into a program AST.
80    #[must_use]
81    pub fn parse(source: impl Into<Box<str>>, source_type: SourceType) -> Self {
82        Self::parse_with_options(source, source_type, ParseOptions::default())
83    }
84
85    /// Parse with explicit OXC parser options.
86    #[must_use]
87    pub fn parse_with_options(
88        source: impl Into<Box<str>>,
89        source_type: SourceType,
90        options: ParseOptions,
91    ) -> Self {
92        let owner = ProgramOwner {
93            source: source.into(),
94            allocator: Allocator::default(),
95            source_type,
96            options,
97        };
98        let cell = ParsedProgramCell::new(owner, |owner| {
99            Parser::new(&owner.allocator, owner.source.as_ref(), owner.source_type)
100                .with_options(owner.options)
101                .parse()
102        });
103        Self { cell }
104    }
105
106    /// Return the original source text.
107    #[must_use]
108    pub fn source(&self) -> &str {
109        self.cell.borrow_owner().source.as_ref()
110    }
111
112    /// Return the OXC source type used for parsing.
113    #[must_use]
114    pub fn source_type(&self) -> SourceType {
115        self.cell.borrow_owner().source_type
116    }
117
118    /// Return the parsed OXC program AST.
119    #[must_use]
120    pub fn program(&self) -> &Program<'_> {
121        &self.cell.borrow_dependent().program
122    }
123
124    /// Return any parse errors (the program may still be partially valid).
125    pub fn errors(&self) -> &[OxcDiagnostic] {
126        &self.cell.borrow_dependent().errors
127    }
128
129    /// Return `true` if the parser panicked during parsing.
130    #[must_use]
131    pub fn panicked(&self) -> bool {
132        self.cell.borrow_dependent().panicked
133    }
134
135    /// Return `true` if the source uses Flow type annotations.
136    #[must_use]
137    pub fn is_flow_language(&self) -> bool {
138        self.cell.borrow_dependent().is_flow_language
139    }
140
141    /// Access the full parser return for consumers that need module records,
142    /// irregular whitespaces, or other parser metadata.
143    #[must_use]
144    pub fn parser_return(&self) -> &ParserReturn<'_> {
145        self.cell.borrow_dependent()
146    }
147}
148
149/// Reusable OXC-backed JavaScript/TypeScript expression handle.
150///
151/// This owns the source text and arena allocator required by the parsed OXC
152/// AST so downstream tools can inspect a template/script expression without
153/// reparsing.
154pub struct ParsedJsExpression {
155    cell: ParsedExpressionCell,
156}
157
158impl std::fmt::Debug for ParsedJsExpression {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.debug_struct("ParsedJsExpression")
161            .field("source", &self.source())
162            .field("source_type", &self.source_type())
163            .finish()
164    }
165}
166
167impl PartialEq for ParsedJsExpression {
168    fn eq(&self, other: &Self) -> bool {
169        self.source() == other.source() && self.source_type() == other.source_type()
170    }
171}
172
173impl Eq for ParsedJsExpression {}
174
175impl ParsedJsExpression {
176    /// Parse a single JavaScript or TypeScript expression.
177    pub fn parse(
178        source: impl Into<Box<str>>,
179        source_type: SourceType,
180    ) -> Result<Self, Box<[OxcDiagnostic]>> {
181        let owner = ExpressionOwner {
182            source: source.into(),
183            allocator: Allocator::default(),
184            source_type,
185        };
186        let cell = ParsedExpressionCell::try_new(owner, |owner| {
187            Parser::new(&owner.allocator, owner.source.as_ref(), owner.source_type)
188                .parse_expression()
189                .map(|expression| ParsedExpressionData { expression })
190                .map_err(|errors| errors.into_boxed_slice())
191        })?;
192        Ok(Self { cell })
193    }
194
195    /// Return the original source text.
196    #[must_use]
197    pub fn source(&self) -> &str {
198        self.cell.borrow_owner().source.as_ref()
199    }
200
201    /// Return the OXC source type used for parsing.
202    #[must_use]
203    pub fn source_type(&self) -> SourceType {
204        self.cell.borrow_owner().source_type
205    }
206
207    /// Return the parsed OXC expression AST node.
208    #[must_use]
209    pub fn expression(&self) -> &Expression<'_> {
210        &self.cell.borrow_dependent().expression
211    }
212}
213
214/// Reusable OXC-backed binding pattern handle.
215///
216/// Svelte stores certain binding and parameter positions in the same logical
217/// expression slot as ordinary expressions. This handle keeps those nodes in
218/// OXC form without routing through ESTree compatibility trees.
219pub struct ParsedJsPattern {
220    source: Box<str>,
221    wrapper: Arc<ParsedJsExpression>,
222}
223
224impl std::fmt::Debug for ParsedJsPattern {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        f.debug_struct("ParsedJsPattern")
227            .field("source", &self.source())
228            .finish()
229    }
230}
231
232/// Reusable OXC-backed formal parameter list handle.
233///
234/// This preserves richer parameter information like rest/default/type metadata
235/// while still exposing each parameter binding pattern without reparsing.
236pub struct ParsedJsParameters {
237    source: Box<str>,
238    wrapper: Arc<ParsedJsExpression>,
239}
240
241impl std::fmt::Debug for ParsedJsParameters {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        f.debug_struct("ParsedJsParameters")
244            .field("source", &self.source())
245            .field("parameter_count", &self.parameters().items.len())
246            .field("has_rest", &self.parameters().rest.is_some())
247            .finish()
248    }
249}
250
251impl PartialEq for ParsedJsParameters {
252    fn eq(&self, other: &Self) -> bool {
253        self.source() == other.source()
254    }
255}
256
257impl Eq for ParsedJsParameters {}
258
259impl ParsedJsParameters {
260    pub fn parse(source: impl Into<Box<str>>) -> Result<Self, Box<[OxcDiagnostic]>> {
261        let source = source.into();
262        let wrapper_source = format!("({})=>{{}}", source);
263        let wrapper = Arc::new(ParsedJsExpression::parse(
264            wrapper_source,
265            SourceType::ts().with_module(true),
266        )?);
267        let _ = Self::parameters_from_wrapper(&wrapper).ok_or_else(|| {
268            vec![OxcDiagnostic::error(
269                "failed to recover formal parameters from wrapper",
270            )]
271            .into_boxed_slice()
272        })?;
273        Ok(Self { source, wrapper })
274    }
275
276    #[must_use]
277    pub fn source(&self) -> &str {
278        self.source.as_ref()
279    }
280
281    #[must_use]
282    pub fn parameters(&self) -> &FormalParameters<'_> {
283        Self::parameters_from_wrapper(&self.wrapper).expect("validated parsed parameters")
284    }
285
286    #[must_use]
287    pub fn parameter(&self, index: usize) -> Option<&FormalParameter<'_>> {
288        self.parameters().items.get(index)
289    }
290
291    #[must_use]
292    pub fn rest_parameter(&self) -> Option<&FormalParameterRest<'_>> {
293        self.parameters().rest.as_deref()
294    }
295
296    fn parameters_from_wrapper(wrapper: &ParsedJsExpression) -> Option<&FormalParameters<'_>> {
297        match wrapper.expression() {
298            Expression::ArrowFunctionExpression(function) => Some(&function.params),
299            _ => None,
300        }
301    }
302}
303
304impl PartialEq for ParsedJsPattern {
305    fn eq(&self, other: &Self) -> bool {
306        self.source() == other.source()
307    }
308}
309
310impl Eq for ParsedJsPattern {}
311
312impl ParsedJsPattern {
313    pub fn parse(source: impl Into<Box<str>>) -> Result<Self, Box<[OxcDiagnostic]>> {
314        let source = source.into();
315        let wrapper_source = format!("({})=>{{}}", source);
316        let wrapper = Arc::new(ParsedJsExpression::parse(
317            wrapper_source,
318            SourceType::ts().with_module(true),
319        )?);
320
321        let _ = Self::pattern_from_wrapper(&wrapper).ok_or_else(|| {
322            vec![OxcDiagnostic::error(
323                "failed to recover binding pattern from wrapper",
324            )]
325            .into_boxed_slice()
326        })?;
327
328        Ok(Self { source, wrapper })
329    }
330
331    #[must_use]
332    pub fn source(&self) -> &str {
333        self.source.as_ref()
334    }
335
336    #[must_use]
337    pub fn pattern(&self) -> &BindingPattern<'_> {
338        Self::pattern_from_wrapper(&self.wrapper).expect("validated parsed pattern")
339    }
340
341    fn pattern_from_wrapper(wrapper: &ParsedJsExpression) -> Option<&BindingPattern<'_>> {
342        match wrapper.expression() {
343            Expression::ArrowFunctionExpression(function) => function
344                .params
345                .items
346                .first()
347                .map(|parameter| &parameter.pattern),
348            _ => None,
349        }
350    }
351}
352
353impl ParsedJsProgram {
354    #[must_use]
355    pub fn statement(&self, index: usize) -> Option<&Statement<'_>> {
356        self.program().body.get(index)
357    }
358
359    #[must_use]
360    pub fn statement_source(&self, index: usize) -> Option<&str> {
361        let statement = self.statement(index)?;
362        let span = statement.span();
363        self.source()
364            .get(span.start as usize..span.end as usize)
365    }
366
367    #[must_use]
368    pub fn variable_declaration(&self, index: usize) -> Option<&VariableDeclaration<'_>> {
369        match self.statement(index)? {
370            Statement::VariableDeclaration(declaration) => Some(declaration),
371            Statement::ExportNamedDeclaration(declaration) => match declaration.declaration.as_ref() {
372                Some(oxc_ast::ast::Declaration::VariableDeclaration(declaration)) => Some(declaration),
373                _ => None,
374            },
375            _ => None,
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use oxc_ast::ast::{BindingPattern, Expression, Statement};
383    use oxc_span::SourceType;
384
385    use super::{ParsedJsExpression, ParsedJsPattern, ParsedJsProgram};
386
387    #[test]
388    fn parsed_js_program_exposes_reusable_oxc_program() {
389        let parsed = ParsedJsProgram::parse("export const answer = 42;", SourceType::mjs());
390
391        assert_eq!(parsed.source(), "export const answer = 42;");
392        assert!(parsed.errors().is_empty());
393        assert!(!parsed.panicked());
394        assert!(matches!(
395            parsed.program().body.first(),
396            Some(Statement::ExportNamedDeclaration(_))
397        ));
398    }
399
400    #[test]
401    fn parsed_js_expression_exposes_reusable_oxc_expression() {
402        let parsed = ParsedJsExpression::parse("count + 1", SourceType::ts().with_module(true))
403            .expect("expression should parse");
404
405        assert_eq!(parsed.source(), "count + 1");
406        assert!(matches!(
407            parsed.expression(),
408            Expression::BinaryExpression(_)
409        ));
410    }
411
412    #[test]
413    fn parsed_js_expression_returns_oxc_errors_on_invalid_input() {
414        let errors = ParsedJsExpression::parse("foo(", SourceType::ts().with_module(true))
415            .err()
416            .expect("expression should fail");
417
418        assert!(!errors.is_empty());
419    }
420
421    #[test]
422    fn parsed_js_pattern_exposes_reusable_oxc_pattern() {
423        let parsed =
424            ParsedJsPattern::parse("{ count, items: [item] }").expect("pattern should parse");
425
426        assert!(matches!(parsed.pattern(), BindingPattern::ObjectPattern(_)));
427    }
428}