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_STR,
40    asset::{Asset, AssetContent},
41    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
42    source::Source,
43    source_map::{structured::StructuredSourceMap, 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/// Generates a [`StructuredSourceMap`] for the transformed code, whose `sourcesContent`
175/// entries are individual shared ropes instead of being embedded in the serialized JSON. This
176/// keeps later `sources` URL rewrites and map embedding from copying the source text of every
177/// module. Serialize with [`StructuredSourceMap::to_rope`] where raw bytes are needed.
178///
179/// `original_source_maps_complete` indicates whether the `original_source_maps` cover the whole
180/// map, i.e. whether every module that ended up in `mappings` had an original sourcemap.
181#[instrument(level = "info", name = "generate source map", skip_all)]
182pub fn generate_js_source_map<'a>(
183    files_map: &impl Files,
184    mappings: Vec<(BytePos, LineCol)>,
185    original_source_maps: impl IntoIterator<Item = &'a Rope>,
186    original_source_maps_complete: bool,
187    inline_sources_content: bool,
188    names: FxHashMap<BytePos, Atom>,
189) -> Result<StructuredSourceMap> {
190    let original_source_maps = original_source_maps
191        .into_iter()
192        .map(|map| map.to_bytes())
193        .collect::<Vec<_>>();
194    let original_source_maps = original_source_maps
195        .iter()
196        .map(|map| Ok(swc_sourcemap::lazy::decode(map)?.into_source_map()?))
197        .collect::<Result<Vec<_>>>()?;
198
199    let fast_path_single_original_source_map =
200        original_source_maps.len() == 1 && original_source_maps_complete;
201
202    let mut new_mappings = build_source_map(
203        files_map,
204        &mappings,
205        None,
206        &InlineSourcesContentConfig {
207            // If we are going to adjust the source map, we are going to throw the source contents
208            // of this source map away regardless.
209            //
210            // In other words, we don't need the content of `B` in source map chain of A -> B -> C.
211            // We only need the source content of `A`, and a way to map the content of `B` back to
212            // `A`, while constructing the final source map, `C`.
213            inline_sources_content: inline_sources_content && !fast_path_single_original_source_map,
214            names,
215        },
216    );
217
218    if original_source_maps.is_empty() {
219        // We don't convert sourcemap::SourceMap into raw_sourcemap::SourceMap because we don't
220        // need to adjust mappings
221        add_default_ignore_list(&mut new_mappings);
222        StructuredSourceMap::from_swc_map(new_mappings)
223    } else if fast_path_single_original_source_map {
224        let mut map = original_source_maps.into_iter().next().unwrap();
225        // TODO: Make this more efficient
226        map.adjust_mappings(new_mappings);
227
228        // TODO: Enable this when we have a way to handle the ignore list
229        // add_default_ignore_list(&mut map);
230        let map = map.into_raw_sourcemap();
231        // The fallback covers raw maps with fields the structured form does not know.
232        StructuredSourceMap::from_serialize(&map)
233            .or_else(|_| StructuredSourceMap::from_json_slice(&serde_json::to_vec(&map)?))
234    } else {
235        let mut map = new_mappings.adjust_mappings_from_multiple(original_source_maps);
236
237        add_default_ignore_list(&mut map);
238
239        StructuredSourceMap::from_swc_map(map)
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! here is suboptimal and allocates over and over again.
256                // On a random test next test project this one spot accounted for
257                // 10% of allocations, hence the more verbose approach.
258                let mut out = String::with_capacity(SOURCE_URL_PROTOCOL_STR.len() + 3 + s.len());
259                out.push_str(SOURCE_URL_PROTOCOL_STR);
260                out.push_str("///");
261                out.push_str(s);
262                out
263            }
264            _ => f.to_string(),
265        }
266    }
267
268    fn inline_sources_content(&self, _f: &FileName) -> bool {
269        self.inline_sources_content
270    }
271
272    fn name_for_bytepos(&self, pos: BytePos) -> Option<&str> {
273        self.names.get(&pos).map(|v| &**v)
274    }
275}
276
277#[turbo_tasks::function]
278pub async fn parse(
279    source: ResolvedVc<Box<dyn Source>>,
280    ty: EcmascriptModuleAssetType,
281    transforms: ResolvedVc<EcmascriptInputTransforms>,
282    node_env: RcStr,
283    is_external_tracing: bool,
284    inline_helpers: bool,
285) -> Result<Vc<ParseResult>> {
286    let span = tracing::info_span!(
287        "parse ecmascript",
288        name = display(source.ident().to_string().await?),
289        ty = display(&ty)
290    );
291
292    match parse_internal(
293        source,
294        ty,
295        transforms,
296        node_env,
297        is_external_tracing,
298        inline_helpers,
299    )
300    .instrument(span)
301    .await
302    {
303        Ok(result) => Ok(result),
304        // ast-grep-ignore: no-context-turbofmt
305        Err(error) => Err(error.context(turbofmt!("failed to parse {}", source.ident()).await?)),
306    }
307}
308
309async fn parse_internal(
310    source: ResolvedVc<Box<dyn Source>>,
311    ty: EcmascriptModuleAssetType,
312    transforms: ResolvedVc<EcmascriptInputTransforms>,
313    node_env: RcStr,
314    loose_errors: bool,
315    inline_helpers: bool,
316) -> Result<Vc<ParseResult>> {
317    let content = source.content();
318    let source_ident = source.ident().await?;
319    let fs_path = &source_ident.path;
320    let ident = &*source.ident().to_string().await?;
321    let file_path_hash = hash_xxh3_hash64(ident) as u128;
322    let content = match content.await {
323        Ok(content) => content,
324        Err(error) => {
325            let error: RcStr = PrettyPrintError(&error).to_string().into();
326            ReadSourceIssue {
327                source: IssueSource::from_source_only(source),
328                error: error.clone(),
329                severity: if loose_errors {
330                    IssueSeverity::Warning
331                } else {
332                    IssueSeverity::Error
333                },
334            }
335            .resolved_cell()
336            .emit();
337
338            return Ok(ParseResult::Unparsable {
339                messages: Some(vec![error]),
340            }
341            .cell());
342        }
343    };
344    Ok(match &*content {
345        AssetContent::File(file) => match &*file.await? {
346            FileContent::NotFound => ParseResult::NotFound.cell(),
347            FileContent::Content(file) => {
348                let transforms = &*transforms.await?;
349                match parse_file_content(
350                    file.content().clone(),
351                    fs_path,
352                    ident,
353                    source_ident.query.clone(),
354                    file_path_hash,
355                    source,
356                    ty,
357                    transforms,
358                    node_env.clone(),
359                    loose_errors,
360                    inline_helpers,
361                )
362                .await
363                {
364                    Ok(result) => result,
365                    Err(e) => {
366                        // ast-grep-ignore: no-context-turbofmt
367                        return Err(e).context(
368                            turbofmt!("Transforming and/or parsing of {} failed", source.ident())
369                                .await?,
370                        );
371                    }
372                }
373            }
374        },
375        AssetContent::Redirect(..) => ParseResult::Unparsable { messages: None }.cell(),
376    })
377}
378
379async fn parse_file_content(
380    program_source: Rope,
381    fs_path: &FileSystemPath,
382    ident: &str,
383    query: RcStr,
384    file_path_hash: u128,
385    source: ResolvedVc<Box<dyn Source>>,
386    ty: EcmascriptModuleAssetType,
387    transforms: &[EcmascriptInputTransform],
388    node_env: RcStr,
389    loose_errors: bool,
390    inline_helpers: bool,
391) -> Result<Vc<ParseResult>> {
392    let string = match BytesStr::from_utf8(program_source.clone().into_bytes()) {
393        Ok(s) => s,
394        Err(error) => {
395            let error: RcStr = PrettyPrintError(
396                &anyhow::anyhow!(error).context("failed to convert rope into string"),
397            )
398            .to_string()
399            .into();
400            ReadSourceIssue {
401                // Technically we could supply byte offsets to the issue source, but
402                // that would cause another utf8 error to be produced when we
403                // attempt to infer line/column
404                // offsets
405                source: IssueSource::from_source_only(source),
406                error: error.clone(),
407                severity: if loose_errors {
408                    IssueSeverity::Warning
409                } else {
410                    IssueSeverity::Error
411                },
412            }
413            .resolved_cell()
414            .emit();
415            return Ok(ParseResult::Unparsable {
416                messages: Some(vec![error]),
417            }
418            .cell());
419        }
420    };
421    let source_map: Arc<swc_core::common::SourceMap> = Default::default();
422    let (emitter, collector) = IssueEmitter::new(
423        source,
424        source_map.clone(),
425        Some(rcstr!("Ecmascript file had an error")),
426    );
427    let handler = Handler::with_emitter(true, false, Box::new(emitter));
428
429    let (emitter, collector_parse) = IssueEmitter::new(
430        source,
431        source_map.clone(),
432        Some(rcstr!("Parsing ecmascript source code failed")),
433    );
434    let parser_handler = Handler::with_emitter(true, false, Box::new(emitter));
435    let globals = Arc::new(Globals::new());
436    let globals_ref = &globals;
437
438    let mut result = WrapFuture::new(
439        async {
440            let file_name = FileName::Custom(ident.to_string());
441            let fm = source_map.new_source_file(file_name.clone().into(), string);
442
443            let comments = SwcComments::default();
444
445            let mut parsed_program = {
446                let lexer = Lexer::new(
447                    match ty {
448                        EcmascriptModuleAssetType::Ecmascript
449                        | EcmascriptModuleAssetType::EcmascriptExtensionless => {
450                            Syntax::Es(EsSyntax {
451                                jsx: true,
452                                fn_bind: true,
453                                decorators: true,
454                                decorators_before_export: true,
455                                export_default_from: true,
456                                import_attributes: true,
457                                allow_super_outside_method: true,
458                                allow_return_outside_function: true,
459                                auto_accessors: true,
460                                explicit_resource_management: true,
461                            })
462                        }
463                        EcmascriptModuleAssetType::Typescript { tsx, .. } => {
464                            Syntax::Typescript(TsSyntax {
465                                decorators: true,
466                                dts: false,
467                                tsx,
468                                ..Default::default()
469                            })
470                        }
471                        EcmascriptModuleAssetType::TypescriptDeclaration => {
472                            Syntax::Typescript(TsSyntax {
473                                decorators: true,
474                                dts: true,
475                                tsx: false,
476                                ..Default::default()
477                            })
478                        }
479                    },
480                    EsVersion::latest(),
481                    StringInput::from(&*fm),
482                    Some(&comments),
483                );
484
485                let mut parser = Parser::new_from(lexer);
486                let span = tracing::trace_span!("swc_parse").entered();
487                let program_result = parser.parse_program();
488                drop(span);
489
490                let mut has_errors = vec![];
491                for e in parser.take_errors() {
492                    let mut e = e.into_diagnostic(&parser_handler);
493                    has_errors.extend(e.message.iter().map(|m| m.0.as_str().into()));
494                    e.emit();
495                }
496
497                if !has_errors.is_empty() {
498                    return Ok(ParseResult::Unparsable {
499                        messages: Some(has_errors),
500                    });
501                }
502
503                match program_result {
504                    Ok(parsed_program) => parsed_program,
505                    Err(e) => {
506                        let mut e = e.into_diagnostic(&parser_handler);
507                        let messages = e.message.iter().map(|m| m.0.as_str().into()).collect();
508
509                        e.emit();
510
511                        return Ok(ParseResult::Unparsable {
512                            messages: Some(messages),
513                        });
514                    }
515                }
516            };
517
518            let unresolved_mark = Mark::new();
519            let top_level_mark = Mark::new();
520
521            let is_typescript = matches!(
522                ty,
523                EcmascriptModuleAssetType::Typescript { .. }
524                    | EcmascriptModuleAssetType::TypescriptDeclaration
525            );
526
527            let helpers = Helpers::new(!inline_helpers);
528            let span = tracing::trace_span!("swc_resolver").entered();
529
530            parsed_program.visit_mut_with(&mut resolver(
531                unresolved_mark,
532                top_level_mark,
533                is_typescript,
534            ));
535            drop(span);
536
537            let span = tracing::trace_span!("swc_lint").entered();
538
539            let lint_config = LintConfig::default();
540            let rules = lints::rules::all(LintParams {
541                program: &parsed_program,
542                lint_config: &lint_config,
543                unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
544                top_level_ctxt: SyntaxContext::empty().apply_mark(top_level_mark),
545                es_version: EsVersion::latest(),
546                source_map: source_map.clone(),
547            });
548
549            parsed_program.mutate(lints::rules::lint_pass(rules));
550            drop(span);
551
552            HELPERS.set(&helpers, || {
553                parsed_program.mutate(explicit_resource_management());
554            });
555
556            let var_with_ts_declare = if is_typescript {
557                VarDeclWithTsDeclareCollector::collect(&parsed_program)
558            } else {
559                FxHashSet::default()
560            };
561
562            let mut helpers = helpers.data();
563            let transform_context = TransformContext {
564                comments: &comments,
565                source_map: &source_map,
566                top_level_mark,
567                unresolved_mark,
568                file_path_str: &fs_path.path,
569                file_name_str: fs_path.file_name(),
570                file_name_hash: file_path_hash,
571                query_str: query,
572                file_path: fs_path.clone(),
573                source,
574                source_text: &fm.src,
575                node_env,
576            };
577            let span = tracing::trace_span!("transforms");
578            async {
579                for transform in transforms.iter() {
580                    helpers = transform
581                        .apply(&mut parsed_program, &transform_context, helpers)
582                        .await?;
583                }
584                anyhow::Ok(())
585            }
586            .instrument(span)
587            .await?;
588
589            if parser_handler.has_errors() {
590                let messages = if let Some(error) = collector_parse.last_emitted_issue() {
591                    // The emitter created in here only uses StyledString::Text
592                    if let StyledString::Text(xx) = &*error.await?.message.await? {
593                        Some(vec![xx.clone()])
594                    } else {
595                        None
596                    }
597                } else {
598                    None
599                };
600                let messages = Some(messages.unwrap_or_else(|| vec![fm.src.clone().into()]));
601                return Ok(ParseResult::Unparsable { messages });
602            }
603
604            let helpers = Helpers::from_data(helpers);
605            HELPERS.set(&helpers, || {
606                parsed_program.mutate(swc_core::ecma::transforms::base::helpers::inject_helpers(
607                    unresolved_mark,
608                ));
609            });
610
611            let eval_context = EvalContext::new(
612                Some(&parsed_program),
613                unresolved_mark,
614                top_level_mark,
615                Arc::new(var_with_ts_declare),
616                Some(&comments),
617            );
618
619            let (comments, source_mapping_url) =
620                ImmutableComments::new_with_source_mapping_url(comments);
621
622            Ok::<ParseResult, anyhow::Error>(ParseResult::Ok {
623                program: parsed_program,
624                comments: Arc::new(comments),
625                eval_context,
626                // Temporary globals as the current one can't be moved yet, since they are
627                // borrowed
628                globals: Arc::new(Globals::new()),
629                source_map,
630                source_mapping_url: source_mapping_url.map(|s| s.into()),
631                program_source,
632            })
633        },
634        |f, cx| GLOBALS.set(globals_ref, || HANDLER.set(&handler, || f.poll(cx))),
635    )
636    .await?;
637    if let ParseResult::Ok {
638        globals: ref mut g, ..
639    } = result
640    {
641        // Assign the correct globals
642        *g = globals;
643    }
644    collector.emit(loose_errors).await?;
645    collector_parse.emit(loose_errors).await?;
646    Ok(result.cell())
647}
648
649#[turbo_tasks::value]
650struct ReadSourceIssue {
651    source: IssueSource,
652    error: RcStr,
653    severity: IssueSeverity,
654}
655
656#[async_trait]
657#[turbo_tasks::value_impl]
658impl Issue for ReadSourceIssue {
659    async fn file_path(&self) -> Result<FileSystemPath> {
660        self.source.file_path().await
661    }
662
663    async fn title(&self) -> Result<StyledString> {
664        Ok(StyledString::Text(rcstr!(
665            "Reading source code for parsing failed"
666        )))
667    }
668
669    async fn description(&self) -> Result<Option<StyledString>> {
670        Ok(Some(StyledString::Text(
671            format!(
672                "An unexpected error happened while trying to read the source code to parse: {}",
673                self.error
674            )
675            .into(),
676        )))
677    }
678
679    fn severity(&self) -> IssueSeverity {
680        self.severity
681    }
682
683    fn stage(&self) -> IssueStage {
684        IssueStage::Load
685    }
686
687    fn source(&self) -> Option<IssueSource> {
688        Some(self.source)
689    }
690}
691
692struct VarDeclWithTsDeclareCollector {
693    id_with_no_ts_declare: FxHashSet<Id>,
694    id_with_ts_declare: FxHashSet<Id>,
695}
696
697impl VarDeclWithTsDeclareCollector {
698    fn collect<N: VisitWith<VarDeclWithTsDeclareCollector>>(n: &N) -> FxHashSet<Id> {
699        let mut collector = VarDeclWithTsDeclareCollector {
700            id_with_no_ts_declare: Default::default(),
701            id_with_ts_declare: Default::default(),
702        };
703        n.visit_with(&mut collector);
704        collector
705            .id_with_ts_declare
706            .retain(|id| !collector.id_with_no_ts_declare.contains(id));
707        collector.id_with_ts_declare
708    }
709
710    fn handle_pat(&mut self, pat: &Pat, declare: bool) {
711        match pat {
712            Pat::Ident(binding_ident) => {
713                if declare {
714                    self.id_with_ts_declare.insert(binding_ident.to_id());
715                } else {
716                    self.id_with_no_ts_declare.insert(binding_ident.to_id());
717                }
718            }
719            Pat::Array(array_pat) => {
720                for pat in array_pat.elems.iter().flatten() {
721                    self.handle_pat(pat, declare);
722                }
723            }
724            Pat::Object(object_pat) => {
725                for prop in object_pat.props.iter() {
726                    match prop {
727                        ObjectPatProp::KeyValue(key_value_pat_prop) => {
728                            self.handle_pat(&key_value_pat_prop.value, declare);
729                        }
730                        ObjectPatProp::Assign(assign_pat_prop) => {
731                            if declare {
732                                self.id_with_ts_declare.insert(assign_pat_prop.key.to_id());
733                            } else {
734                                self.id_with_no_ts_declare
735                                    .insert(assign_pat_prop.key.to_id());
736                            }
737                        }
738                        _ => {}
739                    }
740                }
741            }
742            _ => {}
743        }
744    }
745}
746
747impl Visit for VarDeclWithTsDeclareCollector {
748    noop_visit_type!();
749
750    fn visit_var_decl(&mut self, node: &VarDecl) {
751        for decl in node.decls.iter() {
752            self.handle_pat(&decl.name, node.declare);
753        }
754    }
755
756    fn visit_ts_module_decl(&mut self, node: &TsModuleDecl) {
757        if node.declare
758            && let TsModuleName::Ident(id) = &node.id
759        {
760            self.id_with_ts_declare.insert(id.to_id());
761        }
762    }
763}
764
765/// Re-parses a module directly from saved bytes, bypassing `source.content()`.
766///
767/// Used by `failsafe_parse` to serve the last good AST when the live file has a syntax error.
768pub async fn parse_from_rope(
769    rope: Rope,
770    source: ResolvedVc<Box<dyn Source>>,
771    ty: EcmascriptModuleAssetType,
772    transforms: ResolvedVc<EcmascriptInputTransforms>,
773    node_env: RcStr,
774) -> Result<Vc<ParseResult>> {
775    let ident_vc = source.ident();
776    let ident_ref = ident_vc.await?;
777    let ident = &*ident_vc.to_string().await?;
778    let file_path_hash = hash_xxh3_hash64(ident) as u128;
779    let query = ident_ref.query.clone();
780    let transforms = &*transforms.await?;
781    parse_file_content(
782        rope,
783        &ident_ref.path,
784        ident,
785        query,
786        file_path_hash,
787        source,
788        ty,
789        transforms,
790        node_env,
791        false,
792        false,
793    )
794    .await
795}
796
797#[cfg(test)]
798mod tests {
799    use swc_core::{
800        common::{FileName, GLOBALS, SourceMap, sync::Lrc},
801        ecma::parser::{Parser, Syntax, TsSyntax, lexer::Lexer},
802    };
803
804    use super::VarDeclWithTsDeclareCollector;
805
806    fn parse_and_collect(code: &str) -> Vec<String> {
807        GLOBALS.set(&Default::default(), || {
808            let cm: Lrc<SourceMap> = Default::default();
809            let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
810
811            let lexer = Lexer::new(
812                Syntax::Typescript(TsSyntax {
813                    tsx: false,
814                    decorators: true,
815                    ..Default::default()
816                }),
817                Default::default(),
818                (&*fm).into(),
819                None,
820            );
821
822            let mut parser = Parser::new_from(lexer);
823            let module = parser.parse_module().expect("Failed to parse");
824
825            let ids = VarDeclWithTsDeclareCollector::collect(&module);
826            let mut result: Vec<_> = ids.iter().map(|id| id.0.to_string()).collect();
827            result.sort();
828            result
829        })
830    }
831
832    #[test]
833    fn test_collect_declare_const() {
834        let ids = parse_and_collect("declare const Foo: number;");
835        assert_eq!(ids, vec!["Foo"]);
836    }
837
838    #[test]
839    fn test_collect_declare_global() {
840        let ids = parse_and_collect("declare global {}");
841        assert_eq!(ids, vec!["global"]);
842    }
843
844    #[test]
845    fn test_collect_declare_global_with_content() {
846        let ids = parse_and_collect(
847            r#"
848            declare global {interface Window {foo: string;}}
849            "#,
850        );
851        assert_eq!(ids, vec!["global"]);
852    }
853
854    #[test]
855    fn test_collect_multiple_declares() {
856        let ids = parse_and_collect(
857            r#"
858            declare const Foo: number;
859            declare global {}
860            declare const Bar: string;
861            "#,
862        );
863        assert_eq!(ids, vec!["Bar", "Foo", "global"]);
864    }
865
866    #[test]
867    fn test_no_collect_non_declare() {
868        let ids = parse_and_collect("const Foo = 1;");
869        assert!(ids.is_empty());
870    }
871
872    #[test]
873    fn test_collect_declare_namespace() {
874        // `declare namespace Foo {}` should also be collected
875        let ids = parse_and_collect("declare namespace Foo {}");
876        assert_eq!(ids, vec!["Foo"]);
877    }
878}