Skip to main content

turbopack_ecmascript/
parse.rs

1use std::{future::Future, sync::Arc};
2
3use anyhow::{Context, Result};
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, turbofmt, 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        // ast-grep-ignore: no-context-turbofmt
288        Err(error) => Err(error.context(turbofmt!("failed to parse {}", source.ident()).await?)),
289    }
290}
291
292async fn parse_internal(
293    source: ResolvedVc<Box<dyn Source>>,
294    ty: EcmascriptModuleAssetType,
295    transforms: ResolvedVc<EcmascriptInputTransforms>,
296    loose_errors: bool,
297    inline_helpers: bool,
298) -> Result<Vc<ParseResult>> {
299    let content = source.content();
300    let fs_path = source.ident().path().owned().await?;
301    let ident = &*source.ident().to_string().await?;
302    let file_path_hash = hash_xxh3_hash64(&*source.ident().to_string().await?) as u128;
303    let content = match content.await {
304        Ok(content) => content,
305        Err(error) => {
306            let error: RcStr = PrettyPrintError(&error).to_string().into();
307            ReadSourceIssue {
308                source: IssueSource::from_source_only(source),
309                error: error.clone(),
310                severity: if loose_errors {
311                    IssueSeverity::Warning
312                } else {
313                    IssueSeverity::Error
314                },
315            }
316            .resolved_cell()
317            .emit();
318
319            return Ok(ParseResult::Unparsable {
320                messages: Some(vec![error]),
321            }
322            .cell());
323        }
324    };
325    Ok(match &*content {
326        AssetContent::File(file) => match &*file.await? {
327            FileContent::NotFound => ParseResult::NotFound.cell(),
328            FileContent::Content(file) => {
329                match BytesStr::from_utf8(file.content().clone().into_bytes()) {
330                    Ok(string) => {
331                        let transforms = &*transforms.await?;
332                        match parse_file_content(
333                            string,
334                            &fs_path,
335                            ident,
336                            source.ident().await?.query.clone(),
337                            file_path_hash,
338                            source,
339                            ty,
340                            transforms,
341                            loose_errors,
342                            inline_helpers,
343                        )
344                        .await
345                        {
346                            Ok(result) => result,
347                            Err(e) => {
348                                // ast-grep-ignore: no-context-turbofmt
349                                return Err(e).context(
350                                    turbofmt!(
351                                        "Transforming and/or parsing of {} failed",
352                                        source.ident()
353                                    )
354                                    .await?,
355                                );
356                            }
357                        }
358                    }
359                    Err(error) => {
360                        let error: RcStr = PrettyPrintError(
361                            &anyhow::anyhow!(error).context("failed to convert rope into string"),
362                        )
363                        .to_string()
364                        .into();
365                        ReadSourceIssue {
366                            // Technically we could supply byte offsets to the issue source, but
367                            // that would cause another utf8 error to be produced when we
368                            // attempt to infer line/column
369                            // offsets
370                            source: IssueSource::from_source_only(source),
371                            error: error.clone(),
372                            severity: if loose_errors {
373                                IssueSeverity::Warning
374                            } else {
375                                IssueSeverity::Error
376                            },
377                        }
378                        .resolved_cell()
379                        .emit();
380                        ParseResult::Unparsable {
381                            messages: Some(vec![error]),
382                        }
383                        .cell()
384                    }
385                }
386            }
387        },
388        AssetContent::Redirect { .. } => ParseResult::Unparsable { messages: None }.cell(),
389    })
390}
391
392async fn parse_file_content(
393    string: BytesStr,
394    fs_path: &FileSystemPath,
395    ident: &str,
396    query: RcStr,
397    file_path_hash: u128,
398    source: ResolvedVc<Box<dyn Source>>,
399    ty: EcmascriptModuleAssetType,
400    transforms: &[EcmascriptInputTransform],
401    loose_errors: bool,
402    inline_helpers: bool,
403) -> Result<Vc<ParseResult>> {
404    let source_map: Arc<swc_core::common::SourceMap> = Default::default();
405    let (emitter, collector) = IssueEmitter::new(
406        source,
407        source_map.clone(),
408        Some(rcstr!("Ecmascript file had an error")),
409    );
410    let handler = Handler::with_emitter(true, false, Box::new(emitter));
411
412    let (emitter, collector_parse) = IssueEmitter::new(
413        source,
414        source_map.clone(),
415        Some(rcstr!("Parsing ecmascript source code failed")),
416    );
417    let parser_handler = Handler::with_emitter(true, false, Box::new(emitter));
418    let globals = Arc::new(Globals::new());
419    let globals_ref = &globals;
420
421    let mut result = WrapFuture::new(
422        async {
423            let file_name = FileName::Custom(ident.to_string());
424            let fm = source_map.new_source_file(file_name.clone().into(), string);
425
426            let comments = SwcComments::default();
427
428            let mut parsed_program = {
429                let lexer = Lexer::new(
430                    match ty {
431                        EcmascriptModuleAssetType::Ecmascript
432                        | EcmascriptModuleAssetType::EcmascriptExtensionless => {
433                            Syntax::Es(EsSyntax {
434                                jsx: true,
435                                fn_bind: true,
436                                decorators: true,
437                                decorators_before_export: true,
438                                export_default_from: true,
439                                import_attributes: true,
440                                allow_super_outside_method: true,
441                                allow_return_outside_function: true,
442                                auto_accessors: true,
443                                explicit_resource_management: true,
444                            })
445                        }
446                        EcmascriptModuleAssetType::Typescript { tsx, .. } => {
447                            Syntax::Typescript(TsSyntax {
448                                decorators: true,
449                                dts: false,
450                                tsx,
451                                ..Default::default()
452                            })
453                        }
454                        EcmascriptModuleAssetType::TypescriptDeclaration => {
455                            Syntax::Typescript(TsSyntax {
456                                decorators: true,
457                                dts: true,
458                                tsx: false,
459                                ..Default::default()
460                            })
461                        }
462                    },
463                    EsVersion::latest(),
464                    StringInput::from(&*fm),
465                    Some(&comments),
466                );
467
468                let mut parser = Parser::new_from(lexer);
469                let span = tracing::trace_span!("swc_parse").entered();
470                let program_result = parser.parse_program();
471                drop(span);
472
473                let mut has_errors = vec![];
474                for e in parser.take_errors() {
475                    let mut e = e.into_diagnostic(&parser_handler);
476                    has_errors.extend(e.message.iter().map(|m| m.0.as_str().into()));
477                    e.emit();
478                }
479
480                if !has_errors.is_empty() {
481                    return Ok(ParseResult::Unparsable {
482                        messages: Some(has_errors),
483                    });
484                }
485
486                match program_result {
487                    Ok(parsed_program) => parsed_program,
488                    Err(e) => {
489                        let mut e = e.into_diagnostic(&parser_handler);
490                        let messages = e.message.iter().map(|m| m.0.as_str().into()).collect();
491
492                        e.emit();
493
494                        return Ok(ParseResult::Unparsable {
495                            messages: Some(messages),
496                        });
497                    }
498                }
499            };
500
501            let unresolved_mark = Mark::new();
502            let top_level_mark = Mark::new();
503
504            let is_typescript = matches!(
505                ty,
506                EcmascriptModuleAssetType::Typescript { .. }
507                    | EcmascriptModuleAssetType::TypescriptDeclaration
508            );
509
510            let helpers = Helpers::new(!inline_helpers);
511            let span = tracing::trace_span!("swc_resolver").entered();
512
513            parsed_program.visit_mut_with(&mut resolver(
514                unresolved_mark,
515                top_level_mark,
516                is_typescript,
517            ));
518            drop(span);
519
520            let span = tracing::trace_span!("swc_lint").entered();
521
522            let lint_config = LintConfig::default();
523            let rules = lints::rules::all(LintParams {
524                program: &parsed_program,
525                lint_config: &lint_config,
526                unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
527                top_level_ctxt: SyntaxContext::empty().apply_mark(top_level_mark),
528                es_version: EsVersion::latest(),
529                source_map: source_map.clone(),
530            });
531
532            parsed_program.mutate(lints::rules::lint_pass(rules));
533            drop(span);
534
535            HELPERS.set(&helpers, || {
536                parsed_program.mutate(explicit_resource_management());
537            });
538
539            let var_with_ts_declare = if is_typescript {
540                VarDeclWithTsDeclareCollector::collect(&parsed_program)
541            } else {
542                FxHashSet::default()
543            };
544
545            let mut helpers = helpers.data();
546            let transform_context = TransformContext {
547                comments: &comments,
548                source_map: &source_map,
549                top_level_mark,
550                unresolved_mark,
551                file_path_str: &fs_path.path,
552                file_name_str: fs_path.file_name(),
553                file_name_hash: file_path_hash,
554                query_str: query,
555                file_path: fs_path.clone(),
556                source,
557            };
558            let span = tracing::trace_span!("transforms");
559            async {
560                for transform in transforms.iter() {
561                    helpers = transform
562                        .apply(&mut parsed_program, &transform_context, helpers)
563                        .await?;
564                }
565                anyhow::Ok(())
566            }
567            .instrument(span)
568            .await?;
569
570            if parser_handler.has_errors() {
571                let messages = if let Some(error) = collector_parse.last_emitted_issue() {
572                    // The emitter created in here only uses StyledString::Text
573                    if let StyledString::Text(xx) = &*error.await?.message.await? {
574                        Some(vec![xx.clone()])
575                    } else {
576                        None
577                    }
578                } else {
579                    None
580                };
581                let messages = Some(messages.unwrap_or_else(|| vec![fm.src.clone().into()]));
582                return Ok(ParseResult::Unparsable { messages });
583            }
584
585            let helpers = Helpers::from_data(helpers);
586            HELPERS.set(&helpers, || {
587                parsed_program.mutate(swc_core::ecma::transforms::base::helpers::inject_helpers(
588                    unresolved_mark,
589                ));
590            });
591
592            let eval_context = EvalContext::new(
593                Some(&parsed_program),
594                unresolved_mark,
595                top_level_mark,
596                Arc::new(var_with_ts_declare),
597                Some(&comments),
598                Some(source),
599            );
600
601            let (comments, source_mapping_url) =
602                ImmutableComments::new_with_source_mapping_url(comments);
603
604            Ok::<ParseResult, anyhow::Error>(ParseResult::Ok {
605                program: parsed_program,
606                comments: Arc::new(comments),
607                eval_context,
608                // Temporary globals as the current one can't be moved yet, since they are
609                // borrowed
610                globals: Arc::new(Globals::new()),
611                source_map,
612                source_mapping_url: source_mapping_url.map(|s| s.into()),
613            })
614        },
615        |f, cx| GLOBALS.set(globals_ref, || HANDLER.set(&handler, || f.poll(cx))),
616    )
617    .await?;
618    if let ParseResult::Ok {
619        globals: ref mut g, ..
620    } = result
621    {
622        // Assign the correct globals
623        *g = globals;
624    }
625    collector.emit(loose_errors).await?;
626    collector_parse.emit(loose_errors).await?;
627    Ok(result.cell())
628}
629
630#[turbo_tasks::value]
631struct ReadSourceIssue {
632    source: IssueSource,
633    error: RcStr,
634    severity: IssueSeverity,
635}
636
637#[turbo_tasks::value_impl]
638impl Issue for ReadSourceIssue {
639    #[turbo_tasks::function]
640    fn file_path(&self) -> Vc<FileSystemPath> {
641        self.source.file_path()
642    }
643
644    #[turbo_tasks::function]
645    fn title(&self) -> Vc<StyledString> {
646        StyledString::Text(rcstr!("Reading source code for parsing failed")).cell()
647    }
648
649    #[turbo_tasks::function]
650    fn description(&self) -> Vc<OptionStyledString> {
651        Vc::cell(Some(
652            StyledString::Text(
653                format!(
654                    "An unexpected error happened while trying to read the source code to parse: \
655                     {}",
656                    self.error
657                )
658                .into(),
659            )
660            .resolved_cell(),
661        ))
662    }
663
664    fn severity(&self) -> IssueSeverity {
665        self.severity
666    }
667
668    #[turbo_tasks::function]
669    fn stage(&self) -> Vc<IssueStage> {
670        IssueStage::Load.cell()
671    }
672
673    #[turbo_tasks::function]
674    fn source(&self) -> Vc<OptionIssueSource> {
675        Vc::cell(Some(self.source))
676    }
677}
678
679struct VarDeclWithTsDeclareCollector {
680    id_with_no_ts_declare: FxHashSet<Id>,
681    id_with_ts_declare: FxHashSet<Id>,
682}
683
684impl VarDeclWithTsDeclareCollector {
685    fn collect<N: VisitWith<VarDeclWithTsDeclareCollector>>(n: &N) -> FxHashSet<Id> {
686        let mut collector = VarDeclWithTsDeclareCollector {
687            id_with_no_ts_declare: Default::default(),
688            id_with_ts_declare: Default::default(),
689        };
690        n.visit_with(&mut collector);
691        collector
692            .id_with_ts_declare
693            .retain(|id| !collector.id_with_no_ts_declare.contains(id));
694        collector.id_with_ts_declare
695    }
696
697    fn handle_pat(&mut self, pat: &Pat, declare: bool) {
698        match pat {
699            Pat::Ident(binding_ident) => {
700                if declare {
701                    self.id_with_ts_declare.insert(binding_ident.to_id());
702                } else {
703                    self.id_with_no_ts_declare.insert(binding_ident.to_id());
704                }
705            }
706            Pat::Array(array_pat) => {
707                for pat in array_pat.elems.iter().flatten() {
708                    self.handle_pat(pat, declare);
709                }
710            }
711            Pat::Object(object_pat) => {
712                for prop in object_pat.props.iter() {
713                    match prop {
714                        ObjectPatProp::KeyValue(key_value_pat_prop) => {
715                            self.handle_pat(&key_value_pat_prop.value, declare);
716                        }
717                        ObjectPatProp::Assign(assign_pat_prop) => {
718                            if declare {
719                                self.id_with_ts_declare.insert(assign_pat_prop.key.to_id());
720                            } else {
721                                self.id_with_no_ts_declare
722                                    .insert(assign_pat_prop.key.to_id());
723                            }
724                        }
725                        _ => {}
726                    }
727                }
728            }
729            _ => {}
730        }
731    }
732}
733
734impl Visit for VarDeclWithTsDeclareCollector {
735    noop_visit_type!();
736
737    fn visit_var_decl(&mut self, node: &VarDecl) {
738        for decl in node.decls.iter() {
739            self.handle_pat(&decl.name, node.declare);
740        }
741    }
742
743    fn visit_ts_module_decl(&mut self, node: &TsModuleDecl) {
744        if node.declare
745            && let TsModuleName::Ident(id) = &node.id
746        {
747            self.id_with_ts_declare.insert(id.to_id());
748        }
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use swc_core::{
755        common::{FileName, GLOBALS, SourceMap, sync::Lrc},
756        ecma::parser::{Parser, Syntax, TsSyntax, lexer::Lexer},
757    };
758
759    use super::VarDeclWithTsDeclareCollector;
760
761    fn parse_and_collect(code: &str) -> Vec<String> {
762        GLOBALS.set(&Default::default(), || {
763            let cm: Lrc<SourceMap> = Default::default();
764            let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
765
766            let lexer = Lexer::new(
767                Syntax::Typescript(TsSyntax {
768                    tsx: false,
769                    decorators: true,
770                    ..Default::default()
771                }),
772                Default::default(),
773                (&*fm).into(),
774                None,
775            );
776
777            let mut parser = Parser::new_from(lexer);
778            let module = parser.parse_module().expect("Failed to parse");
779
780            let ids = VarDeclWithTsDeclareCollector::collect(&module);
781            let mut result: Vec<_> = ids.iter().map(|id| id.0.to_string()).collect();
782            result.sort();
783            result
784        })
785    }
786
787    #[test]
788    fn test_collect_declare_const() {
789        let ids = parse_and_collect("declare const Foo: number;");
790        assert_eq!(ids, vec!["Foo"]);
791    }
792
793    #[test]
794    fn test_collect_declare_global() {
795        let ids = parse_and_collect("declare global {}");
796        assert_eq!(ids, vec!["global"]);
797    }
798
799    #[test]
800    fn test_collect_declare_global_with_content() {
801        let ids = parse_and_collect(
802            r#"
803            declare global {
804                interface Window {
805                    foo: string;
806                }
807            }
808            "#,
809        );
810        assert_eq!(ids, vec!["global"]);
811    }
812
813    #[test]
814    fn test_collect_multiple_declares() {
815        let ids = parse_and_collect(
816            r#"
817            declare const Foo: number;
818            declare global {}
819            declare const Bar: string;
820            "#,
821        );
822        assert_eq!(ids, vec!["Bar", "Foo", "global"]);
823    }
824
825    #[test]
826    fn test_no_collect_non_declare() {
827        let ids = parse_and_collect("const Foo = 1;");
828        assert!(ids.is_empty());
829    }
830
831    #[test]
832    fn test_collect_declare_namespace() {
833        // `declare namespace Foo {}` should also be collected
834        let ids = parse_and_collect("declare namespace Foo {}");
835        assert_eq!(ids, vec!["Foo"]);
836    }
837}