Skip to main content

turbopack_ecmascript/
parse.rs

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