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                source_text: &fm.src,
568                node_env,
569            };
570            let span = tracing::trace_span!("transforms");
571            async {
572                for transform in transforms.iter() {
573                    helpers = transform
574                        .apply(&mut parsed_program, &transform_context, helpers)
575                        .await?;
576                }
577                anyhow::Ok(())
578            }
579            .instrument(span)
580            .await?;
581
582            if parser_handler.has_errors() {
583                let messages = if let Some(error) = collector_parse.last_emitted_issue() {
584                    // The emitter created in here only uses StyledString::Text
585                    if let StyledString::Text(xx) = &*error.await?.message.await? {
586                        Some(vec![xx.clone()])
587                    } else {
588                        None
589                    }
590                } else {
591                    None
592                };
593                let messages = Some(messages.unwrap_or_else(|| vec![fm.src.clone().into()]));
594                return Ok(ParseResult::Unparsable { messages });
595            }
596
597            let helpers = Helpers::from_data(helpers);
598            HELPERS.set(&helpers, || {
599                parsed_program.mutate(swc_core::ecma::transforms::base::helpers::inject_helpers(
600                    unresolved_mark,
601                ));
602            });
603
604            let eval_context = EvalContext::new(
605                Some(&parsed_program),
606                unresolved_mark,
607                top_level_mark,
608                Arc::new(var_with_ts_declare),
609                Some(&comments),
610            );
611
612            let (comments, source_mapping_url) =
613                ImmutableComments::new_with_source_mapping_url(comments);
614
615            Ok::<ParseResult, anyhow::Error>(ParseResult::Ok {
616                program: parsed_program,
617                comments: Arc::new(comments),
618                eval_context,
619                // Temporary globals as the current one can't be moved yet, since they are
620                // borrowed
621                globals: Arc::new(Globals::new()),
622                source_map,
623                source_mapping_url: source_mapping_url.map(|s| s.into()),
624                program_source,
625            })
626        },
627        |f, cx| GLOBALS.set(globals_ref, || HANDLER.set(&handler, || f.poll(cx))),
628    )
629    .await?;
630    if let ParseResult::Ok {
631        globals: ref mut g, ..
632    } = result
633    {
634        // Assign the correct globals
635        *g = globals;
636    }
637    collector.emit(loose_errors).await?;
638    collector_parse.emit(loose_errors).await?;
639    Ok(result.cell())
640}
641
642#[turbo_tasks::value]
643struct ReadSourceIssue {
644    source: IssueSource,
645    error: RcStr,
646    severity: IssueSeverity,
647}
648
649#[async_trait]
650#[turbo_tasks::value_impl]
651impl Issue for ReadSourceIssue {
652    async fn file_path(&self) -> Result<FileSystemPath> {
653        self.source.file_path().await
654    }
655
656    async fn title(&self) -> Result<StyledString> {
657        Ok(StyledString::Text(rcstr!(
658            "Reading source code for parsing failed"
659        )))
660    }
661
662    async fn description(&self) -> Result<Option<StyledString>> {
663        Ok(Some(StyledString::Text(
664            format!(
665                "An unexpected error happened while trying to read the source code to parse: {}",
666                self.error
667            )
668            .into(),
669        )))
670    }
671
672    fn severity(&self) -> IssueSeverity {
673        self.severity
674    }
675
676    fn stage(&self) -> IssueStage {
677        IssueStage::Load
678    }
679
680    fn source(&self) -> Option<IssueSource> {
681        Some(self.source)
682    }
683}
684
685struct VarDeclWithTsDeclareCollector {
686    id_with_no_ts_declare: FxHashSet<Id>,
687    id_with_ts_declare: FxHashSet<Id>,
688}
689
690impl VarDeclWithTsDeclareCollector {
691    fn collect<N: VisitWith<VarDeclWithTsDeclareCollector>>(n: &N) -> FxHashSet<Id> {
692        let mut collector = VarDeclWithTsDeclareCollector {
693            id_with_no_ts_declare: Default::default(),
694            id_with_ts_declare: Default::default(),
695        };
696        n.visit_with(&mut collector);
697        collector
698            .id_with_ts_declare
699            .retain(|id| !collector.id_with_no_ts_declare.contains(id));
700        collector.id_with_ts_declare
701    }
702
703    fn handle_pat(&mut self, pat: &Pat, declare: bool) {
704        match pat {
705            Pat::Ident(binding_ident) => {
706                if declare {
707                    self.id_with_ts_declare.insert(binding_ident.to_id());
708                } else {
709                    self.id_with_no_ts_declare.insert(binding_ident.to_id());
710                }
711            }
712            Pat::Array(array_pat) => {
713                for pat in array_pat.elems.iter().flatten() {
714                    self.handle_pat(pat, declare);
715                }
716            }
717            Pat::Object(object_pat) => {
718                for prop in object_pat.props.iter() {
719                    match prop {
720                        ObjectPatProp::KeyValue(key_value_pat_prop) => {
721                            self.handle_pat(&key_value_pat_prop.value, declare);
722                        }
723                        ObjectPatProp::Assign(assign_pat_prop) => {
724                            if declare {
725                                self.id_with_ts_declare.insert(assign_pat_prop.key.to_id());
726                            } else {
727                                self.id_with_no_ts_declare
728                                    .insert(assign_pat_prop.key.to_id());
729                            }
730                        }
731                        _ => {}
732                    }
733                }
734            }
735            _ => {}
736        }
737    }
738}
739
740impl Visit for VarDeclWithTsDeclareCollector {
741    noop_visit_type!();
742
743    fn visit_var_decl(&mut self, node: &VarDecl) {
744        for decl in node.decls.iter() {
745            self.handle_pat(&decl.name, node.declare);
746        }
747    }
748
749    fn visit_ts_module_decl(&mut self, node: &TsModuleDecl) {
750        if node.declare
751            && let TsModuleName::Ident(id) = &node.id
752        {
753            self.id_with_ts_declare.insert(id.to_id());
754        }
755    }
756}
757
758/// Re-parses a module directly from saved bytes, bypassing `source.content()`.
759///
760/// Used by `failsafe_parse` to serve the last good AST when the live file has a syntax error.
761pub async fn parse_from_rope(
762    rope: Rope,
763    source: ResolvedVc<Box<dyn Source>>,
764    ty: EcmascriptModuleAssetType,
765    transforms: ResolvedVc<EcmascriptInputTransforms>,
766    node_env: RcStr,
767) -> Result<Vc<ParseResult>> {
768    let ident_vc = source.ident();
769    let ident_ref = ident_vc.await?;
770    let ident = &*ident_vc.to_string().await?;
771    let file_path_hash = hash_xxh3_hash64(ident) as u128;
772    let query = ident_ref.query.clone();
773    let transforms = &*transforms.await?;
774    parse_file_content(
775        rope,
776        &ident_ref.path,
777        ident,
778        query,
779        file_path_hash,
780        source,
781        ty,
782        transforms,
783        node_env,
784        false,
785        false,
786    )
787    .await
788}
789
790#[cfg(test)]
791mod tests {
792    use swc_core::{
793        common::{FileName, GLOBALS, SourceMap, sync::Lrc},
794        ecma::parser::{Parser, Syntax, TsSyntax, lexer::Lexer},
795    };
796
797    use super::VarDeclWithTsDeclareCollector;
798
799    fn parse_and_collect(code: &str) -> Vec<String> {
800        GLOBALS.set(&Default::default(), || {
801            let cm: Lrc<SourceMap> = Default::default();
802            let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
803
804            let lexer = Lexer::new(
805                Syntax::Typescript(TsSyntax {
806                    tsx: false,
807                    decorators: true,
808                    ..Default::default()
809                }),
810                Default::default(),
811                (&*fm).into(),
812                None,
813            );
814
815            let mut parser = Parser::new_from(lexer);
816            let module = parser.parse_module().expect("Failed to parse");
817
818            let ids = VarDeclWithTsDeclareCollector::collect(&module);
819            let mut result: Vec<_> = ids.iter().map(|id| id.0.to_string()).collect();
820            result.sort();
821            result
822        })
823    }
824
825    #[test]
826    fn test_collect_declare_const() {
827        let ids = parse_and_collect("declare const Foo: number;");
828        assert_eq!(ids, vec!["Foo"]);
829    }
830
831    #[test]
832    fn test_collect_declare_global() {
833        let ids = parse_and_collect("declare global {}");
834        assert_eq!(ids, vec!["global"]);
835    }
836
837    #[test]
838    fn test_collect_declare_global_with_content() {
839        let ids = parse_and_collect(
840            r#"
841            declare global {interface Window {foo: string;}}
842            "#,
843        );
844        assert_eq!(ids, vec!["global"]);
845    }
846
847    #[test]
848    fn test_collect_multiple_declares() {
849        let ids = parse_and_collect(
850            r#"
851            declare const Foo: number;
852            declare global {}
853            declare const Bar: string;
854            "#,
855        );
856        assert_eq!(ids, vec!["Bar", "Foo", "global"]);
857    }
858
859    #[test]
860    fn test_no_collect_non_declare() {
861        let ids = parse_and_collect("const Foo = 1;");
862        assert!(ids.is_empty());
863    }
864
865    #[test]
866    fn test_collect_declare_namespace() {
867        // `declare namespace Foo {}` should also be collected
868        let ids = parse_and_collect("declare namespace Foo {}");
869        assert_eq!(ids, vec!["Foo"]);
870    }
871}