Skip to main content

turbopack_ecmascript/
parse.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Result};
4use async_trait::async_trait;
5use bytes_str::BytesStr;
6use rustc_hash::{FxHashMap, FxHashSet};
7use swc_core::{
8    atoms::Atom,
9    base::SwcComments,
10    common::{
11        BytePos, FileName, GLOBALS, Globals, LineCol, Mark, SyntaxContext,
12        errors::{HANDLER, Handler},
13        input::StringInput,
14        source_map::{Files, SourceMapGenConfig, build_source_map},
15    },
16    ecma::{
17        ast::{
18            EsVersion, Id, Ident, IdentName, ObjectPatProp, Pat, Program, TsModuleDecl,
19            TsModuleName, VarDecl,
20        },
21        lints::{self, config::LintConfig, rules::LintParams},
22        parser::{EsSyntax, Parser, Syntax, TsSyntax, lexer::Lexer},
23        transforms::{
24            base::{
25                helpers::{HELPERS, Helpers},
26                resolver,
27            },
28            proposal::explicit_resource_management::explicit_resource_management,
29        },
30        visit::{Visit, VisitMutWith, VisitWith, noop_visit_type},
31    },
32};
33use tracing::{Instrument, instrument};
34use turbo_rcstr::{RcStr, rcstr};
35use turbo_tasks::{PrettyPrintError, ResolvedVc, ValueToString, Vc, turbofmt, util::WrapFuture};
36use turbo_tasks_fs::{FileContent, FileSystemPath, rope::Rope};
37use turbo_tasks_hash::hash_xxh3_hash64;
38use turbopack_core::{
39    SOURCE_URL_PROTOCOL,
40    asset::{Asset, AssetContent},
41    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
42    source::Source,
43    source_map::utils::add_default_ignore_list,
44};
45use turbopack_swc_utils::emitter::IssueEmitter;
46
47use super::EcmascriptModuleAssetType;
48use crate::{
49    EcmascriptInputTransform,
50    analyzer::graph::EvalContext,
51    magic_identifier,
52    swc_comments::ImmutableComments,
53    transform::{EcmascriptInputTransforms, TransformContext},
54};
55
56/// Collects identifier names and their byte positions from an AST.
57/// This is used to populate the `names` field in source maps.
58/// Based on swc_compiler_base::IdentCollector.
59pub struct IdentCollector {
60    names_vec: Vec<(BytePos, Atom)>,
61    /// Stack of current class names for mapping constructors to class names
62    class_stack: Vec<Atom>,
63}
64
65impl IdentCollector {
66    /// Converts the collected identifiers into a map keyed by the start position of the identifier
67    pub fn into_map(self) -> FxHashMap<BytePos, Atom> {
68        FxHashMap::from_iter(self.names_vec)
69    }
70}
71impl Default for IdentCollector {
72    fn default() -> Self {
73        Self {
74            names_vec: Vec::with_capacity(128),
75            class_stack: Vec::new(),
76        }
77    }
78}
79
80/// Unmangles a Turbopack magic identifier, returning the original name or the input if not mangled
81fn unmangle_atom(name: &Atom) -> Atom {
82    magic_identifier::unmangle(name)
83        .map(Atom::from)
84        .unwrap_or_else(|| name.clone())
85}
86
87impl Visit for IdentCollector {
88    noop_visit_type!();
89
90    fn visit_ident(&mut self, ident: &Ident) {
91        // Skip dummy spans - these are synthetic/generated identifiers
92        if !ident.span.lo.is_dummy() {
93            // we can get away with just the `lo` positions since identifiers cannot overlap.
94            self.names_vec
95                .push((ident.span.lo, unmangle_atom(&ident.sym)));
96        }
97    }
98
99    fn visit_ident_name(&mut self, ident: &IdentName) {
100        if ident.span.lo.is_dummy() {
101            return;
102        }
103
104        // Map constructor names to the class name
105        let mut sym = &ident.sym;
106        if ident.sym == "constructor" {
107            if let Some(class_name) = self.class_stack.last() {
108                sym = class_name;
109            } else {
110                // If no class name in stack, skip the constructor mapping
111                return;
112            }
113        }
114
115        self.names_vec.push((ident.span.lo, unmangle_atom(sym)));
116    }
117
118    fn visit_class_decl(&mut self, decl: &swc_core::ecma::ast::ClassDecl) {
119        // Push class name onto stack
120        self.class_stack.push(decl.ident.sym.clone());
121
122        // Visit the identifier and class
123        self.visit_ident(&decl.ident);
124        self.visit_class(&decl.class);
125
126        // Pop class name from stack
127        self.class_stack.pop();
128    }
129
130    fn visit_class_expr(&mut self, expr: &swc_core::ecma::ast::ClassExpr) {
131        // Push class name onto stack if it exists
132        if let Some(ref ident) = expr.ident {
133            self.class_stack.push(ident.sym.clone());
134            self.visit_ident(ident);
135        }
136
137        // Visit the class body
138        self.visit_class(&expr.class);
139
140        // Pop class name from stack if it was pushed
141        if expr.ident.is_some() {
142            self.class_stack.pop();
143        }
144    }
145}
146
147#[turbo_tasks::value(shared, serialization = "skip", eq = "manual", cell = "new")]
148#[allow(clippy::large_enum_variant)]
149pub enum ParseResult {
150    Ok {
151        #[turbo_tasks(debug_ignore, trace_ignore)]
152        program: Program,
153        #[turbo_tasks(debug_ignore, trace_ignore)]
154        comments: Arc<ImmutableComments>,
155        #[turbo_tasks(debug_ignore, trace_ignore)]
156        eval_context: EvalContext,
157        #[turbo_tasks(debug_ignore, trace_ignore)]
158        globals: Arc<Globals>,
159        #[turbo_tasks(debug_ignore, trace_ignore)]
160        source_map: Arc<swc_core::common::SourceMap>,
161        source_mapping_url: Option<RcStr>,
162        /// Raw bytes of the source that produced this parse, captured atomically
163        /// with the AST. `failsafe_parse` uses this to recover good parses in development on
164        /// error.
165        #[turbo_tasks(debug_ignore, trace_ignore)]
166        program_source: Rope,
167    },
168    Unparsable {
169        messages: Option<Vec<RcStr>>,
170    },
171    NotFound,
172}
173
174/// `original_source_maps_complete` indicates whether the `original_source_maps` cover the whole
175/// map, i.e. whether every module that ended up in `mappings` had an original sourcemap.
176#[instrument(level = "info", name = "generate source map", skip_all)]
177pub fn generate_js_source_map<'a>(
178    files_map: &impl Files,
179    mappings: Vec<(BytePos, LineCol)>,
180    original_source_maps: impl IntoIterator<Item = &'a Rope>,
181    original_source_maps_complete: bool,
182    inline_sources_content: bool,
183    names: FxHashMap<BytePos, Atom>,
184) -> Result<Rope> {
185    let original_source_maps = original_source_maps
186        .into_iter()
187        .map(|map| map.to_bytes())
188        .collect::<Vec<_>>();
189    let original_source_maps = original_source_maps
190        .iter()
191        .map(|map| Ok(swc_sourcemap::lazy::decode(map)?.into_source_map()?))
192        .collect::<Result<Vec<_>>>()?;
193
194    let fast_path_single_original_source_map =
195        original_source_maps.len() == 1 && original_source_maps_complete;
196
197    let mut new_mappings = build_source_map(
198        files_map,
199        &mappings,
200        None,
201        &InlineSourcesContentConfig {
202            // If we are going to adjust the source map, we are going to throw the source contents
203            // of this source map away regardless.
204            //
205            // In other words, we don't need the content of `B` in source map chain of A -> B -> C.
206            // We only need the source content of `A`, and a way to map the content of `B` back to
207            // `A`, while constructing the final source map, `C`.
208            inline_sources_content: inline_sources_content && !fast_path_single_original_source_map,
209            names,
210        },
211    );
212
213    if original_source_maps.is_empty() {
214        // We don't convert sourcemap::SourceMap into raw_sourcemap::SourceMap because we don't
215        // need to adjust mappings
216
217        add_default_ignore_list(&mut new_mappings);
218
219        let mut result = vec![];
220        new_mappings.to_writer(&mut result)?;
221        Ok(Rope::from(result))
222    } else if fast_path_single_original_source_map {
223        let mut map = original_source_maps.into_iter().next().unwrap();
224        // TODO: Make this more efficient
225        map.adjust_mappings(new_mappings);
226
227        // TODO: Enable this when we have a way to handle the ignore list
228        // add_default_ignore_list(&mut map);
229        let map = map.into_raw_sourcemap();
230        let result = serde_json::to_vec(&map)?;
231        Ok(Rope::from(result))
232    } else {
233        let mut map = new_mappings.adjust_mappings_from_multiple(original_source_maps);
234
235        add_default_ignore_list(&mut map);
236
237        let mut result = vec![];
238        map.to_writer(&mut result)?;
239        Ok(Rope::from(result))
240    }
241}
242
243/// A config to generate a source map which includes the source content of every
244/// source file. SWC doesn't inline sources content by default when generating a
245/// sourcemap, so we need to provide a custom config to do it.
246pub struct InlineSourcesContentConfig {
247    inline_sources_content: bool,
248    names: FxHashMap<BytePos, Atom>,
249}
250
251impl SourceMapGenConfig for InlineSourcesContentConfig {
252    fn file_name_to_source(&self, f: &FileName) -> String {
253        match f {
254            FileName::Custom(s) => {
255                format!("{SOURCE_URL_PROTOCOL}///{s}")
256            }
257            _ => f.to_string(),
258        }
259    }
260
261    fn inline_sources_content(&self, _f: &FileName) -> bool {
262        self.inline_sources_content
263    }
264
265    fn name_for_bytepos(&self, pos: BytePos) -> Option<&str> {
266        self.names.get(&pos).map(|v| &**v)
267    }
268}
269
270#[turbo_tasks::function]
271pub async fn parse(
272    source: ResolvedVc<Box<dyn Source>>,
273    ty: EcmascriptModuleAssetType,
274    transforms: ResolvedVc<EcmascriptInputTransforms>,
275    node_env: RcStr,
276    is_external_tracing: bool,
277    inline_helpers: bool,
278) -> Result<Vc<ParseResult>> {
279    let span = tracing::info_span!(
280        "parse ecmascript",
281        name = display(source.ident().to_string().await?),
282        ty = display(&ty)
283    );
284
285    match parse_internal(
286        source,
287        ty,
288        transforms,
289        node_env,
290        is_external_tracing,
291        inline_helpers,
292    )
293    .instrument(span)
294    .await
295    {
296        Ok(result) => Ok(result),
297        // ast-grep-ignore: no-context-turbofmt
298        Err(error) => Err(error.context(turbofmt!("failed to parse {}", source.ident()).await?)),
299    }
300}
301
302async fn parse_internal(
303    source: ResolvedVc<Box<dyn Source>>,
304    ty: EcmascriptModuleAssetType,
305    transforms: ResolvedVc<EcmascriptInputTransforms>,
306    node_env: RcStr,
307    loose_errors: bool,
308    inline_helpers: bool,
309) -> Result<Vc<ParseResult>> {
310    let content = source.content();
311    let source_ident = source.ident().await?;
312    let fs_path = &source_ident.path;
313    let ident = &*source.ident().to_string().await?;
314    let file_path_hash = hash_xxh3_hash64(ident) as u128;
315    let content = match content.await {
316        Ok(content) => content,
317        Err(error) => {
318            let error: RcStr = PrettyPrintError(&error).to_string().into();
319            ReadSourceIssue {
320                source: IssueSource::from_source_only(source),
321                error: error.clone(),
322                severity: if loose_errors {
323                    IssueSeverity::Warning
324                } else {
325                    IssueSeverity::Error
326                },
327            }
328            .resolved_cell()
329            .emit();
330
331            return Ok(ParseResult::Unparsable {
332                messages: Some(vec![error]),
333            }
334            .cell());
335        }
336    };
337    Ok(match &*content {
338        AssetContent::File(file) => match &*file.await? {
339            FileContent::NotFound => ParseResult::NotFound.cell(),
340            FileContent::Content(file) => {
341                let transforms = &*transforms.await?;
342                match parse_file_content(
343                    file.content().clone(),
344                    fs_path,
345                    ident,
346                    source_ident.query.clone(),
347                    file_path_hash,
348                    source,
349                    ty,
350                    transforms,
351                    node_env.clone(),
352                    loose_errors,
353                    inline_helpers,
354                )
355                .await
356                {
357                    Ok(result) => result,
358                    Err(e) => {
359                        // ast-grep-ignore: no-context-turbofmt
360                        return Err(e).context(
361                            turbofmt!("Transforming and/or parsing of {} failed", source.ident())
362                                .await?,
363                        );
364                    }
365                }
366            }
367        },
368        AssetContent::Redirect { .. } => ParseResult::Unparsable { messages: None }.cell(),
369    })
370}
371
372async fn parse_file_content(
373    program_source: Rope,
374    fs_path: &FileSystemPath,
375    ident: &str,
376    query: RcStr,
377    file_path_hash: u128,
378    source: ResolvedVc<Box<dyn Source>>,
379    ty: EcmascriptModuleAssetType,
380    transforms: &[EcmascriptInputTransform],
381    node_env: RcStr,
382    loose_errors: bool,
383    inline_helpers: bool,
384) -> Result<Vc<ParseResult>> {
385    let string = match BytesStr::from_utf8(program_source.clone().into_bytes()) {
386        Ok(s) => s,
387        Err(error) => {
388            let error: RcStr = PrettyPrintError(
389                &anyhow::anyhow!(error).context("failed to convert rope into string"),
390            )
391            .to_string()
392            .into();
393            ReadSourceIssue {
394                // Technically we could supply byte offsets to the issue source, but
395                // that would cause another utf8 error to be produced when we
396                // attempt to infer line/column
397                // offsets
398                source: IssueSource::from_source_only(source),
399                error: error.clone(),
400                severity: if loose_errors {
401                    IssueSeverity::Warning
402                } else {
403                    IssueSeverity::Error
404                },
405            }
406            .resolved_cell()
407            .emit();
408            return Ok(ParseResult::Unparsable {
409                messages: Some(vec![error]),
410            }
411            .cell());
412        }
413    };
414    let source_map: Arc<swc_core::common::SourceMap> = Default::default();
415    let (emitter, collector) = IssueEmitter::new(
416        source,
417        source_map.clone(),
418        Some(rcstr!("Ecmascript file had an error")),
419    );
420    let handler = Handler::with_emitter(true, false, Box::new(emitter));
421
422    let (emitter, collector_parse) = IssueEmitter::new(
423        source,
424        source_map.clone(),
425        Some(rcstr!("Parsing ecmascript source code failed")),
426    );
427    let parser_handler = Handler::with_emitter(true, false, Box::new(emitter));
428    let globals = Arc::new(Globals::new());
429    let globals_ref = &globals;
430
431    let mut result = WrapFuture::new(
432        async {
433            let file_name = FileName::Custom(ident.to_string());
434            let fm = source_map.new_source_file(file_name.clone().into(), string);
435
436            let comments = SwcComments::default();
437
438            let mut parsed_program = {
439                let lexer = Lexer::new(
440                    match ty {
441                        EcmascriptModuleAssetType::Ecmascript
442                        | EcmascriptModuleAssetType::EcmascriptExtensionless => {
443                            Syntax::Es(EsSyntax {
444                                jsx: true,
445                                fn_bind: true,
446                                decorators: true,
447                                decorators_before_export: true,
448                                export_default_from: true,
449                                import_attributes: true,
450                                allow_super_outside_method: true,
451                                allow_return_outside_function: true,
452                                auto_accessors: true,
453                                explicit_resource_management: true,
454                            })
455                        }
456                        EcmascriptModuleAssetType::Typescript { tsx, .. } => {
457                            Syntax::Typescript(TsSyntax {
458                                decorators: true,
459                                dts: false,
460                                tsx,
461                                ..Default::default()
462                            })
463                        }
464                        EcmascriptModuleAssetType::TypescriptDeclaration => {
465                            Syntax::Typescript(TsSyntax {
466                                decorators: true,
467                                dts: true,
468                                tsx: false,
469                                ..Default::default()
470                            })
471                        }
472                    },
473                    EsVersion::latest(),
474                    StringInput::from(&*fm),
475                    Some(&comments),
476                );
477
478                let mut parser = Parser::new_from(lexer);
479                let span = tracing::trace_span!("swc_parse").entered();
480                let program_result = parser.parse_program();
481                drop(span);
482
483                let mut has_errors = vec![];
484                for e in parser.take_errors() {
485                    let mut e = e.into_diagnostic(&parser_handler);
486                    has_errors.extend(e.message.iter().map(|m| m.0.as_str().into()));
487                    e.emit();
488                }
489
490                if !has_errors.is_empty() {
491                    return Ok(ParseResult::Unparsable {
492                        messages: Some(has_errors),
493                    });
494                }
495
496                match program_result {
497                    Ok(parsed_program) => parsed_program,
498                    Err(e) => {
499                        let mut e = e.into_diagnostic(&parser_handler);
500                        let messages = e.message.iter().map(|m| m.0.as_str().into()).collect();
501
502                        e.emit();
503
504                        return Ok(ParseResult::Unparsable {
505                            messages: Some(messages),
506                        });
507                    }
508                }
509            };
510
511            let unresolved_mark = Mark::new();
512            let top_level_mark = Mark::new();
513
514            let is_typescript = matches!(
515                ty,
516                EcmascriptModuleAssetType::Typescript { .. }
517                    | EcmascriptModuleAssetType::TypescriptDeclaration
518            );
519
520            let helpers = Helpers::new(!inline_helpers);
521            let span = tracing::trace_span!("swc_resolver").entered();
522
523            parsed_program.visit_mut_with(&mut resolver(
524                unresolved_mark,
525                top_level_mark,
526                is_typescript,
527            ));
528            drop(span);
529
530            let span = tracing::trace_span!("swc_lint").entered();
531
532            let lint_config = LintConfig::default();
533            let rules = lints::rules::all(LintParams {
534                program: &parsed_program,
535                lint_config: &lint_config,
536                unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
537                top_level_ctxt: SyntaxContext::empty().apply_mark(top_level_mark),
538                es_version: EsVersion::latest(),
539                source_map: source_map.clone(),
540            });
541
542            parsed_program.mutate(lints::rules::lint_pass(rules));
543            drop(span);
544
545            HELPERS.set(&helpers, || {
546                parsed_program.mutate(explicit_resource_management());
547            });
548
549            let var_with_ts_declare = if is_typescript {
550                VarDeclWithTsDeclareCollector::collect(&parsed_program)
551            } else {
552                FxHashSet::default()
553            };
554
555            let mut helpers = helpers.data();
556            let transform_context = TransformContext {
557                comments: &comments,
558                source_map: &source_map,
559                top_level_mark,
560                unresolved_mark,
561                file_path_str: &fs_path.path,
562                file_name_str: fs_path.file_name(),
563                file_name_hash: file_path_hash,
564                query_str: query,
565                file_path: fs_path.clone(),
566                source,
567                node_env,
568            };
569            let span = tracing::trace_span!("transforms");
570            async {
571                for transform in transforms.iter() {
572                    helpers = transform
573                        .apply(&mut parsed_program, &transform_context, helpers)
574                        .await?;
575                }
576                anyhow::Ok(())
577            }
578            .instrument(span)
579            .await?;
580
581            if parser_handler.has_errors() {
582                let messages = if let Some(error) = collector_parse.last_emitted_issue() {
583                    // The emitter created in here only uses StyledString::Text
584                    if let StyledString::Text(xx) = &*error.await?.message.await? {
585                        Some(vec![xx.clone()])
586                    } else {
587                        None
588                    }
589                } else {
590                    None
591                };
592                let messages = Some(messages.unwrap_or_else(|| vec![fm.src.clone().into()]));
593                return Ok(ParseResult::Unparsable { messages });
594            }
595
596            let helpers = Helpers::from_data(helpers);
597            HELPERS.set(&helpers, || {
598                parsed_program.mutate(swc_core::ecma::transforms::base::helpers::inject_helpers(
599                    unresolved_mark,
600                ));
601            });
602
603            let eval_context = EvalContext::new(
604                Some(&parsed_program),
605                unresolved_mark,
606                top_level_mark,
607                Arc::new(var_with_ts_declare),
608                Some(&comments),
609            );
610
611            let (comments, source_mapping_url) =
612                ImmutableComments::new_with_source_mapping_url(comments);
613
614            Ok::<ParseResult, anyhow::Error>(ParseResult::Ok {
615                program: parsed_program,
616                comments: Arc::new(comments),
617                eval_context,
618                // Temporary globals as the current one can't be moved yet, since they are
619                // borrowed
620                globals: Arc::new(Globals::new()),
621                source_map,
622                source_mapping_url: source_mapping_url.map(|s| s.into()),
623                program_source,
624            })
625        },
626        |f, cx| GLOBALS.set(globals_ref, || HANDLER.set(&handler, || f.poll(cx))),
627    )
628    .await?;
629    if let ParseResult::Ok {
630        globals: ref mut g, ..
631    } = result
632    {
633        // Assign the correct globals
634        *g = globals;
635    }
636    collector.emit(loose_errors).await?;
637    collector_parse.emit(loose_errors).await?;
638    Ok(result.cell())
639}
640
641#[turbo_tasks::value]
642struct ReadSourceIssue {
643    source: IssueSource,
644    error: RcStr,
645    severity: IssueSeverity,
646}
647
648#[async_trait]
649#[turbo_tasks::value_impl]
650impl Issue for ReadSourceIssue {
651    async fn file_path(&self) -> Result<FileSystemPath> {
652        self.source.file_path().await
653    }
654
655    async fn title(&self) -> Result<StyledString> {
656        Ok(StyledString::Text(rcstr!(
657            "Reading source code for parsing failed"
658        )))
659    }
660
661    async fn description(&self) -> Result<Option<StyledString>> {
662        Ok(Some(StyledString::Text(
663            format!(
664                "An unexpected error happened while trying to read the source code to parse: {}",
665                self.error
666            )
667            .into(),
668        )))
669    }
670
671    fn severity(&self) -> IssueSeverity {
672        self.severity
673    }
674
675    fn stage(&self) -> IssueStage {
676        IssueStage::Load
677    }
678
679    fn source(&self) -> Option<IssueSource> {
680        Some(self.source)
681    }
682}
683
684struct VarDeclWithTsDeclareCollector {
685    id_with_no_ts_declare: FxHashSet<Id>,
686    id_with_ts_declare: FxHashSet<Id>,
687}
688
689impl VarDeclWithTsDeclareCollector {
690    fn collect<N: VisitWith<VarDeclWithTsDeclareCollector>>(n: &N) -> FxHashSet<Id> {
691        let mut collector = VarDeclWithTsDeclareCollector {
692            id_with_no_ts_declare: Default::default(),
693            id_with_ts_declare: Default::default(),
694        };
695        n.visit_with(&mut collector);
696        collector
697            .id_with_ts_declare
698            .retain(|id| !collector.id_with_no_ts_declare.contains(id));
699        collector.id_with_ts_declare
700    }
701
702    fn handle_pat(&mut self, pat: &Pat, declare: bool) {
703        match pat {
704            Pat::Ident(binding_ident) => {
705                if declare {
706                    self.id_with_ts_declare.insert(binding_ident.to_id());
707                } else {
708                    self.id_with_no_ts_declare.insert(binding_ident.to_id());
709                }
710            }
711            Pat::Array(array_pat) => {
712                for pat in array_pat.elems.iter().flatten() {
713                    self.handle_pat(pat, declare);
714                }
715            }
716            Pat::Object(object_pat) => {
717                for prop in object_pat.props.iter() {
718                    match prop {
719                        ObjectPatProp::KeyValue(key_value_pat_prop) => {
720                            self.handle_pat(&key_value_pat_prop.value, declare);
721                        }
722                        ObjectPatProp::Assign(assign_pat_prop) => {
723                            if declare {
724                                self.id_with_ts_declare.insert(assign_pat_prop.key.to_id());
725                            } else {
726                                self.id_with_no_ts_declare
727                                    .insert(assign_pat_prop.key.to_id());
728                            }
729                        }
730                        _ => {}
731                    }
732                }
733            }
734            _ => {}
735        }
736    }
737}
738
739impl Visit for VarDeclWithTsDeclareCollector {
740    noop_visit_type!();
741
742    fn visit_var_decl(&mut self, node: &VarDecl) {
743        for decl in node.decls.iter() {
744            self.handle_pat(&decl.name, node.declare);
745        }
746    }
747
748    fn visit_ts_module_decl(&mut self, node: &TsModuleDecl) {
749        if node.declare
750            && let TsModuleName::Ident(id) = &node.id
751        {
752            self.id_with_ts_declare.insert(id.to_id());
753        }
754    }
755}
756
757/// Re-parses a module directly from saved bytes, bypassing `source.content()`.
758///
759/// Used by `failsafe_parse` to serve the last good AST when the live file has a syntax error.
760pub async fn parse_from_rope(
761    rope: Rope,
762    source: ResolvedVc<Box<dyn Source>>,
763    ty: EcmascriptModuleAssetType,
764    transforms: ResolvedVc<EcmascriptInputTransforms>,
765    node_env: RcStr,
766) -> Result<Vc<ParseResult>> {
767    let ident_vc = source.ident();
768    let ident_ref = ident_vc.await?;
769    let ident = &*ident_vc.to_string().await?;
770    let file_path_hash = hash_xxh3_hash64(ident) as u128;
771    let query = ident_ref.query.clone();
772    let transforms = &*transforms.await?;
773    parse_file_content(
774        rope,
775        &ident_ref.path,
776        ident,
777        query,
778        file_path_hash,
779        source,
780        ty,
781        transforms,
782        node_env,
783        false,
784        false,
785    )
786    .await
787}
788
789#[cfg(test)]
790mod tests {
791    use swc_core::{
792        common::{FileName, GLOBALS, SourceMap, sync::Lrc},
793        ecma::parser::{Parser, Syntax, TsSyntax, lexer::Lexer},
794    };
795
796    use super::VarDeclWithTsDeclareCollector;
797
798    fn parse_and_collect(code: &str) -> Vec<String> {
799        GLOBALS.set(&Default::default(), || {
800            let cm: Lrc<SourceMap> = Default::default();
801            let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
802
803            let lexer = Lexer::new(
804                Syntax::Typescript(TsSyntax {
805                    tsx: false,
806                    decorators: true,
807                    ..Default::default()
808                }),
809                Default::default(),
810                (&*fm).into(),
811                None,
812            );
813
814            let mut parser = Parser::new_from(lexer);
815            let module = parser.parse_module().expect("Failed to parse");
816
817            let ids = VarDeclWithTsDeclareCollector::collect(&module);
818            let mut result: Vec<_> = ids.iter().map(|id| id.0.to_string()).collect();
819            result.sort();
820            result
821        })
822    }
823
824    #[test]
825    fn test_collect_declare_const() {
826        let ids = parse_and_collect("declare const Foo: number;");
827        assert_eq!(ids, vec!["Foo"]);
828    }
829
830    #[test]
831    fn test_collect_declare_global() {
832        let ids = parse_and_collect("declare global {}");
833        assert_eq!(ids, vec!["global"]);
834    }
835
836    #[test]
837    fn test_collect_declare_global_with_content() {
838        let ids = parse_and_collect(
839            r#"
840            declare global {interface Window {foo: string;}}
841            "#,
842        );
843        assert_eq!(ids, vec!["global"]);
844    }
845
846    #[test]
847    fn test_collect_multiple_declares() {
848        let ids = parse_and_collect(
849            r#"
850            declare const Foo: number;
851            declare global {}
852            declare const Bar: string;
853            "#,
854        );
855        assert_eq!(ids, vec!["Bar", "Foo", "global"]);
856    }
857
858    #[test]
859    fn test_no_collect_non_declare() {
860        let ids = parse_and_collect("const Foo = 1;");
861        assert!(ids.is_empty());
862    }
863
864    #[test]
865    fn test_collect_declare_namespace() {
866        // `declare namespace Foo {}` should also be collected
867        let ids = parse_and_collect("declare namespace Foo {}");
868        assert_eq!(ids, vec!["Foo"]);
869    }
870}