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
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)]
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 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 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 map.adjust_mappings(new_mappings);
227
228 let map = map.into_raw_sourcemap();
231 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
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 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 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 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 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 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 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 *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
765pub 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 let ids = parse_and_collect("declare namespace Foo {}");
876 assert_eq!(ids, vec!["Foo"]);
877 }
878}