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