1use std::path::Path;
13use std::sync::LazyLock;
14
15use oxc_allocator::Allocator;
16use oxc_ast_visit::Visit;
17use oxc_parser::Parser;
18use oxc_span::SourceType;
19use rustc_hash::{FxHashMap, FxHashSet};
20
21use crate::asset_url::normalize_asset_url;
22use crate::parse::{
23 compute_auto_import_candidates, compute_import_binding_usage, compute_semantic_usage,
24};
25use crate::sfc_template::{SfcKind, collect_template_usage_with_bound_targets};
26use crate::source_map::ExtractionResult;
27use crate::visitor::ModuleInfoExtractor;
28use crate::{ImportInfo, ImportedName, ModuleInfo};
29use fallow_types::discover::FileId;
30use fallow_types::extract::{FunctionComplexity, byte_offset_to_line_col, compute_line_offsets};
31use oxc_span::Span;
32
33static SCRIPT_BLOCK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
36 crate::static_regex(
37 r#"(?is)<script\b(?P<attrs>(?:[^>"']|"[^"]*"|'[^']*')*)>(?P<body>[\s\S]*?)</script>"#,
38 )
39});
40
41static LANG_ATTR_RE: LazyLock<regex::Regex> =
43 LazyLock::new(|| crate::static_regex(r#"lang\s*=\s*["'](\w+)["']"#));
44
45static SRC_ATTR_RE: LazyLock<regex::Regex> =
48 LazyLock::new(|| crate::static_regex(r#"(?:^|\s)src\s*=\s*["']([^"']+)["']"#));
49
50static SETUP_ATTR_RE: LazyLock<regex::Regex> =
52 LazyLock::new(|| crate::static_regex(r"(?:^|\s)setup(?:\s|$)"));
53
54static CONTEXT_MODULE_ATTR_RE: LazyLock<regex::Regex> =
56 LazyLock::new(|| crate::static_regex(r#"context\s*=\s*["']module["']"#));
57
58static VUE_GENERIC_ATTR_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
62 crate::static_regex(r#"(?:^|\s)generic\s*=\s*"([^"]*)"|(?:^|\s)generic\s*=\s*'([^']*)'"#)
63});
64
65static SVELTE_GENERICS_ATTR_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
68 crate::static_regex(r#"(?:^|\s)generics\s*=\s*"([^"]*)"|(?:^|\s)generics\s*=\s*'([^']*)'"#)
69});
70
71static HTML_COMMENT_RE: LazyLock<regex::Regex> =
73 LazyLock::new(|| crate::static_regex(r"(?s)<!--.*?-->"));
74
75static PROPS_ATTRS_SPREAD_RE: LazyLock<regex::Regex> =
80 LazyLock::new(|| crate::static_regex(r#"v-bind\s*=\s*["'](?:\$attrs|\$props|props)["']"#));
81
82static SVELTE_TEMPLATE_DATA_WHOLE_USE_RE: LazyLock<regex::Regex> =
90 LazyLock::new(|| crate::static_regex(r"(?:=\s*\{\s*data\s*\}|\{\s*\.\.\.\s*data\s*\})"));
91
92static TEMPLATE_EMIT_CALL_RE: LazyLock<regex::Regex> =
102 LazyLock::new(|| crate::static_regex(r#"([\w$]+)\s*\(\s*(?:'([\w:-]*)'|"([\w:-]*)"|(\S))"#));
103
104static STYLE_BLOCK_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
108 crate::static_regex(
109 r#"(?is)<style\b(?P<attrs>(?:[^>"']|"[^"]*"|'[^']*')*)>(?P<body>[\s\S]*?)</style>"#,
110 )
111});
112
113static TEMPLATE_ASSET_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
128 crate::static_regex(
129 r#"(?si)<(?:img|source|video|audio|track|embed)\b(?:[^>"']|"[^"]*"|'[^']*')*?\s(?:src|poster)\s*=\s*(?:"((?:\./|\.\./)[^"<>{}?#\s]*)"|'((?:\./|\.\./)[^'<>{}?#\s]*)')"#,
130 )
131});
132
133fn mask_non_markup_regions(source: &str) -> String {
137 let mut masked = source.to_string();
138 for re in [&*SCRIPT_BLOCK_RE, &*STYLE_BLOCK_RE, &*HTML_COMMENT_RE] {
139 masked = re
140 .replace_all(&masked, |caps: ®ex::Captures<'_>| {
141 " ".repeat(caps[0].len())
142 })
143 .into_owned();
144 }
145 masked
146}
147
148fn collect_template_asset_refs(source: &str) -> Vec<(String, Span)> {
151 let masked = mask_non_markup_regions(source);
152 let mut refs = Vec::new();
153 for caps in TEMPLATE_ASSET_RE.captures_iter(&masked) {
154 let Some(value) = caps.get(1).or_else(|| caps.get(2)) else {
155 continue;
156 };
157 let raw = value.as_str();
158 if raw.is_empty() {
159 continue;
160 }
161 refs.push((
162 normalize_asset_url(raw),
163 Span::new(value.start() as u32, value.end() as u32),
164 ));
165 }
166 refs
167}
168
169pub struct SfcScript {
171 pub body: String,
173 pub is_typescript: bool,
175 pub is_jsx: bool,
177 pub byte_offset: usize,
179 pub src: Option<String>,
181 pub src_span: Option<Span>,
183 pub is_setup: bool,
185 pub is_context_module: bool,
187 pub generic_attr: Option<String>,
191}
192
193pub fn extract_sfc_scripts(source: &str) -> Vec<SfcScript> {
195 let comment_ranges: Vec<(usize, usize)> = HTML_COMMENT_RE
196 .find_iter(source)
197 .map(|m| (m.start(), m.end()))
198 .collect();
199
200 SCRIPT_BLOCK_RE
201 .captures_iter(source)
202 .filter(|cap| {
203 let start = cap.get(0).map_or(0, |m| m.start());
204 !comment_ranges
205 .iter()
206 .any(|&(cs, ce)| start >= cs && start < ce)
207 })
208 .map(|cap| {
209 let attrs = cap.name("attrs").map_or("", |m| m.as_str());
210 let body_match = cap.name("body");
211 let byte_offset = body_match.map_or(0, |m| m.start());
212 let body = body_match.map_or("", |m| m.as_str()).to_string();
213 let lang = LANG_ATTR_RE
214 .captures(attrs)
215 .and_then(|c| c.get(1))
216 .map(|m| m.as_str());
217 let is_typescript = matches!(lang, Some("ts" | "tsx"));
218 let is_jsx = matches!(lang, Some("tsx" | "jsx"));
219 let src = SRC_ATTR_RE
220 .captures(attrs)
221 .and_then(|c| c.get(1))
222 .map(|m| m.as_str().to_string());
223 let attrs_start = cap.name("attrs").map_or(0, |m| m.start());
224 let src_span = SRC_ATTR_RE.captures(attrs).and_then(|c| c.get(1)).map(|m| {
225 Span::new(
226 (attrs_start + m.start()) as u32,
227 (attrs_start + m.end()) as u32,
228 )
229 });
230 let is_setup = SETUP_ATTR_RE.is_match(attrs);
231 let is_context_module = CONTEXT_MODULE_ATTR_RE.is_match(attrs);
232 let generic_attr = VUE_GENERIC_ATTR_RE
233 .captures(attrs)
234 .or_else(|| SVELTE_GENERICS_ATTR_RE.captures(attrs))
235 .and_then(|cap| cap.get(1).or_else(|| cap.get(2)))
236 .map(|m| m.as_str().to_string())
237 .filter(|value| !value.trim().is_empty());
238 SfcScript {
239 body,
240 is_typescript,
241 is_jsx,
242 byte_offset,
243 src,
244 src_span,
245 is_setup,
246 is_context_module,
247 generic_attr,
248 }
249 })
250 .collect()
251}
252
253pub struct SfcStyle {
255 pub body: String,
257 pub lang: Option<String>,
260 pub src: Option<String>,
262 pub src_span: Option<Span>,
264 pub byte_offset: usize,
266}
267
268pub fn extract_sfc_styles(source: &str) -> Vec<SfcStyle> {
275 let comment_ranges: Vec<(usize, usize)> = HTML_COMMENT_RE
276 .find_iter(source)
277 .map(|m| (m.start(), m.end()))
278 .collect();
279
280 STYLE_BLOCK_RE
281 .captures_iter(source)
282 .filter(|cap| {
283 let start = cap.get(0).map_or(0, |m| m.start());
284 !comment_ranges
285 .iter()
286 .any(|&(cs, ce)| start >= cs && start < ce)
287 })
288 .map(|cap| {
289 let attrs = cap.name("attrs").map_or("", |m| m.as_str());
290 let body = cap.name("body").map_or("", |m| m.as_str()).to_string();
291 let byte_offset = cap.name("body").map_or(0, |m| m.start());
292 let lang = LANG_ATTR_RE
293 .captures(attrs)
294 .and_then(|c| c.get(1))
295 .map(|m| m.as_str().to_string());
296 let src = SRC_ATTR_RE
297 .captures(attrs)
298 .and_then(|c| c.get(1))
299 .map(|m| m.as_str().to_string());
300 let attrs_start = cap.name("attrs").map_or(0, |m| m.start());
301 let src_span = SRC_ATTR_RE.captures(attrs).and_then(|c| c.get(1)).map(|m| {
302 Span::new(
303 (attrs_start + m.start()) as u32,
304 (attrs_start + m.end()) as u32,
305 )
306 });
307 SfcStyle {
308 body,
309 lang,
310 src,
311 src_span,
312 byte_offset,
313 }
314 })
315 .collect()
316}
317
318#[must_use]
320pub fn is_sfc_file(path: &Path) -> bool {
321 path.extension()
322 .and_then(|e| e.to_str())
323 .is_some_and(|ext| ext == "vue" || ext == "svelte")
324}
325
326pub(crate) fn parse_sfc_to_module(
328 file_id: FileId,
329 path: &Path,
330 source: &str,
331 content_hash: u64,
332 need_complexity: bool,
333) -> ModuleInfo {
334 let scripts = extract_sfc_scripts(source);
335 let styles = extract_sfc_styles(source);
336 let kind = sfc_kind(path);
337 let mut combined = empty_sfc_module(file_id, source, content_hash);
338 let mut template_visible_imports: FxHashSet<String> = FxHashSet::default();
339 let mut template_visible_bound_targets: FxHashMap<String, String> = FxHashMap::default();
340 let mut props_return_binding: Option<String> = None;
341 let mut emit_return_binding: Option<String> = None;
342
343 for script in &scripts {
344 merge_script_into_module(&mut SfcScriptMergeInput {
345 kind,
346 script,
347 combined: &mut combined,
348 template_visible_imports: &mut template_visible_imports,
349 template_visible_bound_targets: &mut template_visible_bound_targets,
350 props_return_binding: &mut props_return_binding,
351 emit_return_binding: &mut emit_return_binding,
352 need_complexity,
353 });
354 }
355
356 for style in &styles {
357 merge_style_into_module(style, &mut combined);
358 }
359
360 if kind == SfcKind::Vue
364 && !combined.component_props.is_empty()
365 && PROPS_ATTRS_SPREAD_RE.is_match(source)
366 {
367 combined.has_props_attrs_fallthrough = true;
368 }
369
370 apply_template_usage(
371 kind,
372 source,
373 &template_visible_imports,
374 &template_visible_bound_targets,
375 props_return_binding.as_deref(),
376 kind == SfcKind::Svelte && is_sveltekit_route_data_component(path),
377 &mut combined,
378 );
379
380 if kind == SfcKind::Vue && !combined.component_emits.is_empty() {
384 apply_template_emit_usage(source, emit_return_binding.as_deref(), &mut combined);
385 }
386
387 for (specifier, span) in collect_template_asset_refs(source) {
391 combined.imports.push(ImportInfo {
392 source: specifier,
393 imported_name: ImportedName::SideEffect,
394 local_name: String::new(),
395 is_type_only: false,
396 from_style: false,
397 span,
398 source_span: span,
399 });
400 }
401
402 combined.unused_import_bindings.sort_unstable();
403 combined.unused_import_bindings.dedup();
404 combined.type_referenced_import_bindings.sort_unstable();
405 combined.type_referenced_import_bindings.dedup();
406 combined.value_referenced_import_bindings.sort_unstable();
407 combined.value_referenced_import_bindings.dedup();
408 combined.auto_import_candidates.sort_unstable();
409 combined.auto_import_candidates.dedup();
410
411 combined
412}
413
414fn sfc_kind(path: &Path) -> SfcKind {
415 if path.extension().and_then(|ext| ext.to_str()) == Some("vue") {
416 SfcKind::Vue
417 } else {
418 SfcKind::Svelte
419 }
420}
421
422fn is_sveltekit_route_data_component(path: &Path) -> bool {
433 let Some(stem) = path
434 .file_name()
435 .and_then(|name| name.to_str())
436 .and_then(|name| name.strip_suffix(".svelte"))
437 else {
438 return false;
439 };
440 ["+page", "+layout"].iter().any(|prefix| {
441 stem.strip_prefix(prefix)
442 .is_some_and(|rest| rest.is_empty() || rest.starts_with('@'))
443 })
444}
445
446fn empty_sfc_module(file_id: FileId, source: &str, content_hash: u64) -> ModuleInfo {
447 let parsed = crate::suppress::parse_suppressions_from_source(source);
448
449 ModuleInfo {
450 file_id,
451 exports: Vec::new(),
452 imports: Vec::new(),
453 re_exports: Vec::new(),
454 dynamic_imports: Vec::new(),
455 dynamic_import_patterns: Vec::new(),
456 require_calls: Vec::new(),
457 package_path_references: Vec::new(),
458 member_accesses: Vec::new(),
459 whole_object_uses: Vec::new(),
460 has_cjs_exports: false,
461 has_angular_component_template_url: false,
462 content_hash,
463 suppressions: parsed.suppressions,
464 unknown_suppression_kinds: parsed.unknown_kinds,
465 unused_import_bindings: Vec::new(),
466 type_referenced_import_bindings: Vec::new(),
467 value_referenced_import_bindings: Vec::new(),
468 line_offsets: compute_line_offsets(source),
469 complexity: Vec::new(),
470 flag_uses: Vec::new(),
471 class_heritage: vec![],
472 injection_tokens: vec![],
473 local_type_declarations: Vec::new(),
474 public_signature_type_references: Vec::new(),
475 namespace_object_aliases: Vec::new(),
476 iconify_prefixes: Vec::new(),
477 iconify_icon_names: Vec::new(),
478 auto_import_candidates: Vec::new(),
479 directives: Vec::new(),
480 client_only_dynamic_import_spans: Vec::new(),
481 security_sinks: Vec::new(),
482 security_sinks_skipped: 0,
483 security_unresolved_callee_sites: Vec::new(),
484 tainted_bindings: Vec::new(),
485 sanitized_sink_args: Vec::new(),
486 security_control_sites: Vec::new(),
487 callee_uses: Vec::new(),
488 misplaced_directives: Vec::new(),
489 di_key_sites: Vec::new(),
490 has_dynamic_provide: false,
491 referenced_import_bindings: Vec::new(),
492 component_props: Vec::new(),
493 has_props_attrs_fallthrough: false,
494 has_define_expose: false,
495 has_define_model: false,
496 has_unharvestable_props: false,
497 component_emits: Vec::new(),
498 has_unharvestable_emits: false,
499 has_dynamic_emit: false,
500 has_emit_whole_object_use: false,
501 load_return_keys: Vec::new(),
502 has_unharvestable_load: false,
503 has_load_data_whole_use: false,
504 has_page_data_store_whole_use: false,
505 component_functions: Vec::new(),
506 react_props: Vec::new(),
507 hook_uses: Vec::new(),
508 render_edges: Vec::new(),
509 }
510}
511
512struct SfcScriptMergeInput<'a> {
513 kind: SfcKind,
514 script: &'a SfcScript,
515 combined: &'a mut ModuleInfo,
516 template_visible_imports: &'a mut FxHashSet<String>,
517 template_visible_bound_targets: &'a mut FxHashMap<String, String>,
518 props_return_binding: &'a mut Option<String>,
519 emit_return_binding: &'a mut Option<String>,
520 need_complexity: bool,
521}
522
523fn merge_script_into_module(input: &mut SfcScriptMergeInput<'_>) {
524 if input.kind == SfcKind::Vue
525 && let Some(src) = &input.script.src
526 {
527 add_script_src_import(input.combined, src, input.script.src_span);
528 }
529
530 let allocator = Allocator::default();
531 let parser_return = Parser::new(
532 &allocator,
533 &input.script.body,
534 source_type_for_script(input.script),
535 )
536 .parse();
537 let mut extractor = ModuleInfoExtractor::new();
538 extractor.visit_program(&parser_return.program);
539 let extraction = ExtractionResult::contiguous(&input.script.body, input.script.byte_offset);
540 extractor.remap_spans_with(|span| extraction.remap_span(span));
541 extractor.resolve_typed_destructure_bindings();
542
543 let augmented_body = build_generic_attr_probe_source(input.script);
544 let empty_template_used = rustc_hash::FxHashSet::default();
545 let (binding_usage, auto_import_candidates) = if let Some(augmented) = augmented_body.as_deref()
546 {
547 let augmented_return =
548 Parser::new(&allocator, augmented, source_type_for_script(input.script)).parse();
549 (
550 compute_import_binding_usage(
551 &augmented_return.program,
552 &extractor.imports,
553 &empty_template_used,
554 ),
555 compute_auto_import_candidates(&parser_return.program),
556 )
557 } else {
558 let semantic_usage = compute_semantic_usage(
559 &parser_return.program,
560 &extractor.imports,
561 &empty_template_used,
562 );
563 (
564 semantic_usage.import_binding_usage,
565 semantic_usage.auto_import_candidates,
566 )
567 };
568 input
569 .combined
570 .unused_import_bindings
571 .extend(binding_usage.unused.iter().cloned());
572 input
573 .combined
574 .type_referenced_import_bindings
575 .extend(binding_usage.type_referenced.iter().cloned());
576 input
577 .combined
578 .value_referenced_import_bindings
579 .extend(binding_usage.value_referenced.iter().cloned());
580 input
581 .combined
582 .auto_import_candidates
583 .extend(auto_import_candidates);
584 if input.need_complexity {
585 input
586 .combined
587 .complexity
588 .extend(translate_script_complexity(
589 input.script,
590 &parser_return.program,
591 &input.combined.line_offsets,
592 ));
593 }
594
595 if input.kind == SfcKind::Vue && input.script.is_setup {
599 let harvest = crate::sfc_props::harvest_define_props(&parser_return.program);
600 if harvest.has_unharvestable_props {
601 input.combined.has_unharvestable_props = true;
602 }
603 if harvest.has_props_attrs_fallthrough {
604 input.combined.has_props_attrs_fallthrough = true;
605 }
606 if harvest.has_define_expose {
607 input.combined.has_define_expose = true;
608 }
609 if harvest.has_define_model {
610 input.combined.has_define_model = true;
611 }
612 if let Some(binding) = harvest.props_return_binding {
613 *input.props_return_binding = Some(binding);
614 }
615 for mut prop in harvest.props {
616 prop.span_start += input.script.byte_offset as u32;
617 input.combined.component_props.push(prop);
618 }
619
620 let emit_harvest = crate::sfc_props::harvest_define_emits(&parser_return.program);
624 if emit_harvest.has_unharvestable_emits {
625 input.combined.has_unharvestable_emits = true;
626 }
627 if emit_harvest.has_dynamic_emit {
628 input.combined.has_dynamic_emit = true;
629 }
630 if emit_harvest.has_emit_whole_object_use {
631 input.combined.has_emit_whole_object_use = true;
632 }
633 if let Some(binding) = emit_harvest.emit_binding {
634 *input.emit_return_binding = Some(binding);
635 }
636 for mut emit in emit_harvest.emits {
637 emit.span_start += input.script.byte_offset as u32;
638 input.combined.component_emits.push(emit);
639 }
640 }
641
642 if is_template_visible_script(input.kind, input.script) {
643 input.template_visible_imports.extend(
644 extractor
645 .imports
646 .iter()
647 .filter(|import| !import.local_name.is_empty())
648 .map(|import| import.local_name.clone()),
649 );
650 input.template_visible_bound_targets.extend(
651 extractor
652 .binding_target_names()
653 .iter()
654 .filter(|(local, _)| !local.starts_with("this."))
655 .map(|(local, target)| (local.clone(), target.clone())),
656 );
657 }
658
659 extractor.merge_into(input.combined);
660}
661
662fn translate_script_complexity(
663 script: &SfcScript,
664 program: &oxc_ast::ast::Program<'_>,
665 sfc_line_offsets: &[u32],
666) -> Vec<FunctionComplexity> {
667 let script_line_offsets = compute_line_offsets(&script.body);
668 let mut complexity =
669 crate::complexity::compute_complexity(program, &script.body, &script_line_offsets);
670 let (body_start_line, body_start_col) =
671 byte_offset_to_line_col(sfc_line_offsets, script.byte_offset as u32);
672
673 for function in &mut complexity {
674 function.line = body_start_line + function.line.saturating_sub(1);
675 if function.line == body_start_line {
676 function.col += body_start_col;
677 }
678 }
679
680 complexity
681}
682
683fn add_script_src_import(module: &mut ModuleInfo, source: &str, source_span: Option<Span>) {
684 let span = source_span.unwrap_or_default();
685 module.imports.push(ImportInfo {
686 source: normalize_asset_url(source),
687 imported_name: ImportedName::SideEffect,
688 local_name: String::new(),
689 is_type_only: false,
690 from_style: false,
691 span,
692 source_span: span,
693 });
694}
695
696fn style_lang_is_scss(lang: Option<&str>) -> bool {
702 matches!(lang, Some("scss" | "sass"))
703}
704
705fn style_lang_is_css_like(lang: Option<&str>) -> bool {
706 lang.is_none() || matches!(lang, Some("css"))
707}
708
709fn merge_style_into_module(style: &SfcStyle, combined: &mut ModuleInfo) {
710 if let Some(src) = &style.src {
711 let span = style.src_span.unwrap_or_default();
712 combined.imports.push(ImportInfo {
713 source: normalize_asset_url(src),
714 imported_name: ImportedName::SideEffect,
715 local_name: String::new(),
716 is_type_only: false,
717 from_style: true,
718 span,
719 source_span: span,
720 });
721 }
722
723 let lang = style.lang.as_deref();
724 let is_scss = style_lang_is_scss(lang);
725 let is_css_like = style_lang_is_css_like(lang);
726 if !is_scss && !is_css_like {
727 return;
728 }
729
730 for source in crate::css::extract_css_import_sources(&style.body, is_scss) {
731 let source_span = Span::new(
732 style.byte_offset as u32 + source.span.start,
733 style.byte_offset as u32 + source.span.end,
734 );
735 combined.imports.push(ImportInfo {
736 source: source.normalized,
737 imported_name: if source.is_plugin {
738 ImportedName::Default
739 } else {
740 ImportedName::SideEffect
741 },
742 local_name: String::new(),
743 is_type_only: false,
744 from_style: true,
745 span: source_span,
746 source_span,
747 });
748 }
749}
750
751fn source_type_for_script(script: &SfcScript) -> SourceType {
752 match (script.is_typescript, script.is_jsx) {
753 (true, true) => SourceType::tsx(),
754 (true, false) => SourceType::ts(),
755 (false, true) => SourceType::jsx(),
756 (false, false) => SourceType::mjs(),
757 }
758}
759
760fn build_generic_attr_probe_source(script: &SfcScript) -> Option<String> {
766 let constraint = script.generic_attr.as_deref()?.trim();
767 if constraint.is_empty() {
768 return None;
769 }
770 Some(format!(
771 "{}\n;type __FALLOW_GENERIC_ATTR_PROBE<{}> = unknown;\n",
772 script.body, constraint,
773 ))
774}
775
776fn apply_template_usage(
777 kind: SfcKind,
778 source: &str,
779 template_visible_imports: &FxHashSet<String>,
780 template_visible_bound_targets: &FxHashMap<String, String>,
781 props_return_binding: Option<&str>,
782 credit_load_data: bool,
783 combined: &mut ModuleInfo,
784) {
785 let mut credited: FxHashSet<String> = template_visible_imports.clone();
792 if credit_load_data {
805 credited.insert("data".to_string());
806 if SVELTE_TEMPLATE_DATA_WHOLE_USE_RE.is_match(source) {
811 combined.has_load_data_whole_use = true;
812 }
813 }
814 if !combined.component_props.is_empty() {
815 for prop in &combined.component_props {
816 credited.insert(prop.name.clone());
819 credited.insert(prop.local.clone());
820 }
821 credited.insert("$props".to_string());
824 if let Some(binding) = props_return_binding {
825 credited.insert(binding.to_string());
826 }
827 }
828
829 let template_usage = if credit_load_data && template_visible_bound_targets.contains_key("data")
840 {
841 let mut filtered = template_visible_bound_targets.clone();
842 filtered.remove("data");
843 collect_template_usage_with_bound_targets(kind, source, &credited, &filtered)
844 } else {
845 collect_template_usage_with_bound_targets(
846 kind,
847 source,
848 &credited,
849 template_visible_bound_targets,
850 )
851 };
852
853 if !combined.component_props.is_empty() {
858 let member_used: FxHashSet<&str> = template_usage
859 .member_accesses
860 .iter()
861 .filter(|access| {
862 access.object == "$props"
863 || props_return_binding.is_some_and(|binding| access.object == binding)
864 })
865 .map(|access| access.member.as_str())
866 .collect();
867 for prop in &mut combined.component_props {
868 if template_usage.used_bindings.contains(&prop.name)
869 || template_usage.used_bindings.contains(&prop.local)
870 || member_used.contains(prop.name.as_str())
871 {
872 prop.used_in_template = true;
873 }
874 }
875 }
876
877 if let Some(binding) = props_return_binding
884 && (template_usage.used_bindings.contains(binding)
885 || template_usage
886 .whole_object_uses
887 .iter()
888 .any(|used| used == binding))
889 {
890 combined.has_props_attrs_fallthrough = true;
891 }
892
893 combined
894 .unused_import_bindings
895 .retain(|binding| !template_usage.used_bindings.contains(binding));
896 combined
897 .member_accesses
898 .extend(template_usage.member_accesses);
899 combined
900 .whole_object_uses
901 .extend(template_usage.whole_object_uses);
902 combined
903 .security_sinks
904 .extend(template_usage.security_sinks);
905 if !template_usage.unresolved_tag_names.is_empty() {
906 let mut names: Vec<String> = template_usage.unresolved_tag_names.into_iter().collect();
907 names.sort_unstable();
908 combined.auto_import_candidates.extend(names);
909 combined.auto_import_candidates.dedup();
910 }
911}
912
913fn apply_template_emit_usage(
931 source: &str,
932 emit_return_binding: Option<&str>,
933 combined: &mut ModuleInfo,
934) {
935 let masked = mask_non_markup_regions(source);
936 let mut used: FxHashSet<String> = FxHashSet::default();
937 let mut dynamic = false;
938
939 for caps in TEMPLATE_EMIT_CALL_RE.captures_iter(&masked) {
940 let Some(callee) = caps.get(1) else {
941 continue;
942 };
943 let callee = callee.as_str();
944 let is_emit_call =
945 callee == "$emit" || emit_return_binding.is_some_and(|binding| callee == binding);
946 if !is_emit_call {
947 continue;
948 }
949 if let Some(event) = caps.get(2).or_else(|| caps.get(3)) {
950 used.insert(event.as_str().to_string());
953 } else if caps.get(4).is_some() {
954 dynamic = true;
957 }
958 }
959
960 if dynamic {
961 combined.has_dynamic_emit = true;
962 }
963 if !used.is_empty() {
964 for emit in &mut combined.component_emits {
965 if used.contains(&emit.name) {
966 emit.used = true;
967 }
968 }
969 }
970}
971
972fn is_template_visible_script(kind: SfcKind, script: &SfcScript) -> bool {
973 match kind {
974 SfcKind::Vue => script.is_setup,
975 SfcKind::Svelte => !script.is_context_module,
976 }
977}
978
979#[cfg(all(test, not(miri)))]
980mod tests {
981 use super::*;
982
983 #[test]
984 fn is_sfc_file_vue() {
985 assert!(is_sfc_file(Path::new("App.vue")));
986 }
987
988 #[test]
989 fn is_sfc_file_svelte() {
990 assert!(is_sfc_file(Path::new("Counter.svelte")));
991 }
992
993 #[test]
994 fn is_sfc_file_rejects_ts() {
995 assert!(!is_sfc_file(Path::new("utils.ts")));
996 }
997
998 #[test]
999 fn is_sfc_file_rejects_jsx() {
1000 assert!(!is_sfc_file(Path::new("App.jsx")));
1001 }
1002
1003 #[test]
1004 fn is_sfc_file_rejects_astro() {
1005 assert!(!is_sfc_file(Path::new("Layout.astro")));
1006 }
1007
1008 #[test]
1009 fn single_plain_script() {
1010 let scripts = extract_sfc_scripts("<script>const x = 1;</script>");
1011 assert_eq!(scripts.len(), 1);
1012 assert_eq!(scripts[0].body, "const x = 1;");
1013 assert!(!scripts[0].is_typescript);
1014 assert!(!scripts[0].is_jsx);
1015 assert!(scripts[0].src.is_none());
1016 }
1017
1018 #[test]
1019 fn single_ts_script() {
1020 let scripts = extract_sfc_scripts(r#"<script lang="ts">const x: number = 1;</script>"#);
1021 assert_eq!(scripts.len(), 1);
1022 assert!(scripts[0].is_typescript);
1023 assert!(!scripts[0].is_jsx);
1024 }
1025
1026 #[test]
1027 fn single_tsx_script() {
1028 let scripts = extract_sfc_scripts(r#"<script lang="tsx">const el = <div />;</script>"#);
1029 assert_eq!(scripts.len(), 1);
1030 assert!(scripts[0].is_typescript);
1031 assert!(scripts[0].is_jsx);
1032 }
1033
1034 #[test]
1035 fn single_jsx_script() {
1036 let scripts = extract_sfc_scripts(r#"<script lang="jsx">const el = <div />;</script>"#);
1037 assert_eq!(scripts.len(), 1);
1038 assert!(!scripts[0].is_typescript);
1039 assert!(scripts[0].is_jsx);
1040 }
1041
1042 #[test]
1043 fn two_script_blocks() {
1044 let source = r#"
1045<script lang="ts">
1046export default {};
1047</script>
1048<script setup lang="ts">
1049const count = 0;
1050</script>
1051"#;
1052 let scripts = extract_sfc_scripts(source);
1053 assert_eq!(scripts.len(), 2);
1054 assert!(scripts[0].body.contains("export default"));
1055 assert!(scripts[1].body.contains("count"));
1056 }
1057
1058 #[test]
1059 fn script_setup_extracted() {
1060 let scripts =
1061 extract_sfc_scripts(r#"<script setup lang="ts">import { ref } from 'vue';</script>"#);
1062 assert_eq!(scripts.len(), 1);
1063 assert!(scripts[0].body.contains("import"));
1064 assert!(scripts[0].is_typescript);
1065 }
1066
1067 #[test]
1068 fn script_src_detected() {
1069 let scripts = extract_sfc_scripts(r#"<script src="./component.ts" lang="ts"></script>"#);
1070 assert_eq!(scripts.len(), 1);
1071 assert_eq!(scripts[0].src.as_deref(), Some("./component.ts"));
1072 }
1073
1074 #[test]
1075 fn data_src_not_treated_as_src() {
1076 let scripts =
1077 extract_sfc_scripts(r#"<script lang="ts" data-src="./nope.ts">const x = 1;</script>"#);
1078 assert_eq!(scripts.len(), 1);
1079 assert!(scripts[0].src.is_none());
1080 }
1081
1082 #[test]
1083 fn script_inside_html_comment_filtered() {
1084 let source = r#"
1085<!-- <script lang="ts">import { bad } from 'bad';</script> -->
1086<script lang="ts">import { good } from 'good';</script>
1087"#;
1088 let scripts = extract_sfc_scripts(source);
1089 assert_eq!(scripts.len(), 1);
1090 assert!(scripts[0].body.contains("good"));
1091 }
1092
1093 #[test]
1094 fn spanning_comment_filters_script() {
1095 let source = r#"
1096<!-- disabled:
1097<script lang="ts">import { bad } from 'bad';</script>
1098-->
1099<script lang="ts">const ok = true;</script>
1100"#;
1101 let scripts = extract_sfc_scripts(source);
1102 assert_eq!(scripts.len(), 1);
1103 assert!(scripts[0].body.contains("ok"));
1104 }
1105
1106 #[test]
1107 fn string_containing_comment_markers_not_corrupted() {
1108 let source = r#"
1109<script setup lang="ts">
1110const marker = "<!-- not a comment -->";
1111import { ref } from 'vue';
1112</script>
1113"#;
1114 let scripts = extract_sfc_scripts(source);
1115 assert_eq!(scripts.len(), 1);
1116 assert!(scripts[0].body.contains("import"));
1117 }
1118
1119 #[test]
1120 fn generic_attr_with_angle_bracket() {
1121 let source =
1122 r#"<script setup lang="ts" generic="T extends Foo<Bar>">const x = 1;</script>"#;
1123 let scripts = extract_sfc_scripts(source);
1124 assert_eq!(scripts.len(), 1);
1125 assert_eq!(scripts[0].body, "const x = 1;");
1126 }
1127
1128 #[test]
1129 fn nested_generic_attr() {
1130 let source = r#"<script setup lang="ts" generic="T extends Map<string, Set<number>>">const x = 1;</script>"#;
1131 let scripts = extract_sfc_scripts(source);
1132 assert_eq!(scripts.len(), 1);
1133 assert_eq!(scripts[0].body, "const x = 1;");
1134 }
1135
1136 #[test]
1137 fn lang_single_quoted() {
1138 let scripts = extract_sfc_scripts("<script lang='ts'>const x = 1;</script>");
1139 assert_eq!(scripts.len(), 1);
1140 assert!(scripts[0].is_typescript);
1141 }
1142
1143 #[test]
1144 fn uppercase_script_tag() {
1145 let scripts = extract_sfc_scripts(r#"<SCRIPT lang="ts">const x = 1;</SCRIPT>"#);
1146 assert_eq!(scripts.len(), 1);
1147 assert!(scripts[0].is_typescript);
1148 }
1149
1150 #[test]
1151 fn no_script_block() {
1152 let scripts = extract_sfc_scripts("<template><div>Hello</div></template>");
1153 assert!(scripts.is_empty());
1154 }
1155
1156 #[test]
1157 fn empty_script_body() {
1158 let scripts = extract_sfc_scripts(r#"<script lang="ts"></script>"#);
1159 assert_eq!(scripts.len(), 1);
1160 assert!(scripts[0].body.is_empty());
1161 }
1162
1163 #[test]
1164 fn whitespace_only_script() {
1165 let scripts = extract_sfc_scripts("<script lang=\"ts\">\n \n</script>");
1166 assert_eq!(scripts.len(), 1);
1167 assert!(scripts[0].body.trim().is_empty());
1168 }
1169
1170 #[test]
1171 fn byte_offset_is_set() {
1172 let source = r#"<template><div/></template><script lang="ts">code</script>"#;
1173 let scripts = extract_sfc_scripts(source);
1174 assert_eq!(scripts.len(), 1);
1175 let offset = scripts[0].byte_offset;
1176 assert_eq!(&source[offset..offset + 4], "code");
1177 }
1178
1179 #[test]
1180 fn script_with_extra_attributes() {
1181 let scripts = extract_sfc_scripts(
1182 r#"<script lang="ts" id="app" type="module" data-custom="val">const x = 1;</script>"#,
1183 );
1184 assert_eq!(scripts.len(), 1);
1185 assert!(scripts[0].is_typescript);
1186 assert!(scripts[0].src.is_none());
1187 }
1188
1189 #[test]
1190 fn multiple_script_blocks_exports_combined() {
1191 let source = r#"
1192<script lang="ts">
1193export const version = '1.0';
1194</script>
1195<script setup lang="ts">
1196import { ref } from 'vue';
1197const count = ref(0);
1198</script>
1199"#;
1200 let info = parse_sfc_to_module(FileId(0), Path::new("Dual.vue"), source, 0, false);
1201 assert!(
1202 info.exports
1203 .iter()
1204 .any(|e| matches!(&e.name, crate::ExportName::Named(n) if n == "version")),
1205 "export from <script> block should be extracted"
1206 );
1207 assert!(
1208 info.imports.iter().any(|i| i.source == "vue"),
1209 "import from <script setup> block should be extracted"
1210 );
1211 }
1212
1213 #[test]
1214 fn lang_tsx_detected_as_typescript_jsx() {
1215 let scripts =
1216 extract_sfc_scripts(r#"<script lang="tsx">const el = <div>{x}</div>;</script>"#);
1217 assert_eq!(scripts.len(), 1);
1218 assert!(scripts[0].is_typescript, "lang=tsx should be typescript");
1219 assert!(scripts[0].is_jsx, "lang=tsx should be jsx");
1220 }
1221
1222 #[test]
1223 fn multiline_html_comment_filters_all_script_blocks_inside() {
1224 let source = r#"
1225<!--
1226 This whole section is disabled:
1227 <script lang="ts">import { bad1 } from 'bad1';</script>
1228 <script lang="ts">import { bad2 } from 'bad2';</script>
1229-->
1230<script lang="ts">import { good } from 'good';</script>
1231"#;
1232 let scripts = extract_sfc_scripts(source);
1233 assert_eq!(scripts.len(), 1);
1234 assert!(scripts[0].body.contains("good"));
1235 }
1236
1237 #[test]
1238 fn script_src_generates_side_effect_import() {
1239 let info = parse_sfc_to_module(
1240 FileId(0),
1241 Path::new("External.vue"),
1242 r#"<script src="./external-logic.ts" lang="ts"></script>"#,
1243 0,
1244 false,
1245 );
1246 assert!(
1247 info.imports
1248 .iter()
1249 .any(|i| i.source == "./external-logic.ts"
1250 && matches!(i.imported_name, ImportedName::SideEffect)),
1251 "script src should generate a side-effect import"
1252 );
1253 }
1254
1255 #[test]
1256 fn parse_sfc_no_script_returns_empty_module() {
1257 let info = parse_sfc_to_module(
1258 FileId(0),
1259 Path::new("Empty.vue"),
1260 "<template><div>Hello</div></template>",
1261 42,
1262 false,
1263 );
1264 assert!(info.imports.is_empty());
1265 assert!(info.exports.is_empty());
1266 assert_eq!(info.content_hash, 42);
1267 assert_eq!(info.file_id, FileId(0));
1268 }
1269
1270 #[test]
1271 fn parse_sfc_has_line_offsets() {
1272 let info = parse_sfc_to_module(
1273 FileId(0),
1274 Path::new("LineOffsets.vue"),
1275 r#"<script lang="ts">const x = 1;</script>"#,
1276 0,
1277 false,
1278 );
1279 assert!(!info.line_offsets.is_empty());
1280 }
1281
1282 #[test]
1283 fn parse_sfc_has_suppressions() {
1284 let info = parse_sfc_to_module(
1285 FileId(0),
1286 Path::new("Suppressions.vue"),
1287 r#"<script lang="ts">
1288// fallow-ignore-file
1289export const foo = 1;
1290</script>"#,
1291 0,
1292 false,
1293 );
1294 assert!(!info.suppressions.is_empty());
1295 }
1296
1297 #[test]
1298 fn source_type_jsx_detection() {
1299 let scripts = extract_sfc_scripts(r#"<script lang="jsx">const el = <div />;</script>"#);
1300 assert_eq!(scripts.len(), 1);
1301 assert!(!scripts[0].is_typescript);
1302 assert!(scripts[0].is_jsx);
1303 }
1304
1305 #[test]
1306 fn source_type_plain_js_detection() {
1307 let scripts = extract_sfc_scripts("<script>const x = 1;</script>");
1308 assert_eq!(scripts.len(), 1);
1309 assert!(!scripts[0].is_typescript);
1310 assert!(!scripts[0].is_jsx);
1311 }
1312
1313 #[test]
1314 fn is_sfc_file_rejects_no_extension() {
1315 assert!(!is_sfc_file(Path::new("Makefile")));
1316 }
1317
1318 #[test]
1319 fn is_sfc_file_rejects_mdx() {
1320 assert!(!is_sfc_file(Path::new("post.mdx")));
1321 }
1322
1323 #[test]
1324 fn is_sfc_file_rejects_css() {
1325 assert!(!is_sfc_file(Path::new("styles.css")));
1326 }
1327
1328 #[test]
1329 fn multiple_script_blocks_both_have_offsets() {
1330 let source = r#"<script lang="ts">const a = 1;</script>
1331<script setup lang="ts">const b = 2;</script>"#;
1332 let scripts = extract_sfc_scripts(source);
1333 assert_eq!(scripts.len(), 2);
1334 let offset0 = scripts[0].byte_offset;
1335 let offset1 = scripts[1].byte_offset;
1336 assert_eq!(
1337 &source[offset0..offset0 + "const a = 1;".len()],
1338 "const a = 1;"
1339 );
1340 assert_eq!(
1341 &source[offset1..offset1 + "const b = 2;".len()],
1342 "const b = 2;"
1343 );
1344 }
1345
1346 #[test]
1347 fn script_with_src_and_lang() {
1348 let scripts = extract_sfc_scripts(r#"<script src="./logic.ts" lang="tsx"></script>"#);
1349 assert_eq!(scripts.len(), 1);
1350 assert_eq!(scripts[0].src.as_deref(), Some("./logic.ts"));
1351 assert!(scripts[0].is_typescript);
1352 assert!(scripts[0].is_jsx);
1353 }
1354
1355 #[test]
1356 fn extract_style_block_lang_scss() {
1357 let source = r#"<template/><style lang="scss">@import 'Foo';</style>"#;
1358 let styles = extract_sfc_styles(source);
1359 assert_eq!(styles.len(), 1);
1360 assert_eq!(styles[0].lang.as_deref(), Some("scss"));
1361 assert!(styles[0].body.contains("@import"));
1362 assert!(styles[0].src.is_none());
1363 }
1364
1365 #[test]
1366 fn extract_style_block_with_src() {
1367 let source = r#"<style src="./theme.scss" lang="scss"></style>"#;
1368 let styles = extract_sfc_styles(source);
1369 assert_eq!(styles.len(), 1);
1370 assert_eq!(styles[0].src.as_deref(), Some("./theme.scss"));
1371 assert_eq!(styles[0].lang.as_deref(), Some("scss"));
1372 }
1373
1374 #[test]
1375 fn extract_style_block_plain_no_lang() {
1376 let source = r"<style>.foo { color: red; }</style>";
1377 let styles = extract_sfc_styles(source);
1378 assert_eq!(styles.len(), 1);
1379 assert!(styles[0].lang.is_none());
1380 }
1381
1382 #[test]
1383 fn extract_multiple_style_blocks() {
1384 let source = r#"<style lang="scss">@import 'a';</style>
1385<style scoped lang="scss">@import 'b';</style>"#;
1386 let styles = extract_sfc_styles(source);
1387 assert_eq!(styles.len(), 2);
1388 }
1389
1390 #[test]
1391 fn style_block_inside_html_comment_filtered() {
1392 let source = r#"<!-- <style lang="scss">@import 'bad';</style> -->
1393<style lang="scss">@import 'good';</style>"#;
1394 let styles = extract_sfc_styles(source);
1395 assert_eq!(styles.len(), 1);
1396 assert!(styles[0].body.contains("good"));
1397 }
1398
1399 #[test]
1400 fn parse_sfc_extracts_style_imports_with_from_style_flag() {
1401 let info = parse_sfc_to_module(
1402 FileId(0),
1403 Path::new("Foo.vue"),
1404 r#"<template/><style lang="scss">@import 'Foo';</style>"#,
1405 0,
1406 false,
1407 );
1408 let style_import = info
1409 .imports
1410 .iter()
1411 .find(|i| i.source == "./Foo")
1412 .expect("scss @import 'Foo' should be normalized to ./Foo");
1413 assert!(
1414 style_import.from_style,
1415 "imports from <style> blocks must carry from_style=true so the resolver \
1416 enables SCSS partial fallback for the SFC importer"
1417 );
1418 assert!(matches!(
1419 style_import.imported_name,
1420 ImportedName::SideEffect
1421 ));
1422 }
1423
1424 #[test]
1425 fn parse_sfc_extracts_style_plugin_as_default_import() {
1426 let info = parse_sfc_to_module(
1427 FileId(0),
1428 Path::new("Foo.vue"),
1429 r#"<template/><style>@plugin "./tailwind-plugin.js";</style>"#,
1430 0,
1431 false,
1432 );
1433 let plugin_import = info
1434 .imports
1435 .iter()
1436 .find(|i| i.source == "./tailwind-plugin.js")
1437 .expect("style @plugin should create an import");
1438 assert!(plugin_import.from_style);
1439 assert!(matches!(plugin_import.imported_name, ImportedName::Default));
1440 }
1441
1442 #[test]
1443 fn parse_sfc_extracts_style_src_with_from_style_flag() {
1444 let info = parse_sfc_to_module(
1445 FileId(0),
1446 Path::new("Bar.vue"),
1447 r#"<style src="./Bar.scss" lang="scss"></style>"#,
1448 0,
1449 false,
1450 );
1451 let style_src = info
1452 .imports
1453 .iter()
1454 .find(|i| i.source == "./Bar.scss")
1455 .expect("<style src=\"./Bar.scss\"> should produce a side-effect import");
1456 assert!(style_src.from_style);
1457 }
1458
1459 #[test]
1460 fn parse_sfc_skips_unsupported_style_lang_body_but_keeps_src() {
1461 let info = parse_sfc_to_module(
1462 FileId(0),
1463 Path::new("Baz.vue"),
1464 r#"<style lang="postcss" src="./Baz.pcss">@custom-rule "skipped";</style>"#,
1465 0,
1466 false,
1467 );
1468 assert!(
1469 info.imports.iter().any(|i| i.source == "./Baz.pcss"),
1470 "src reference should still be seeded for unsupported lang"
1471 );
1472 assert!(
1473 !info.imports.iter().any(|i| i.source.contains("skipped")),
1474 "postcss body should not be scanned for @import directives"
1475 );
1476 }
1477
1478 fn asset_refs(source: &str) -> Vec<String> {
1479 super::collect_template_asset_refs(source)
1480 .into_iter()
1481 .map(|(s, _)| s)
1482 .collect()
1483 }
1484
1485 #[test]
1486 fn captures_static_relative_template_asset_refs() {
1487 assert_eq!(
1488 asset_refs(r#"<template><img src="./logo.png" /></template>"#),
1489 vec!["./logo.png".to_string()]
1490 );
1491 assert_eq!(
1492 asset_refs(r#"<source src="../media/clip.mp4">"#),
1493 vec!["../media/clip.mp4".to_string()]
1494 );
1495 assert_eq!(
1496 asset_refs(r#"<video poster="./thumb.jpg"></video>"#),
1497 vec!["./thumb.jpg".to_string()]
1498 );
1499 }
1500
1501 #[test]
1502 fn skips_dynamic_alias_root_remote_and_query_asset_refs() {
1503 assert!(asset_refs(r#"<img :src="logo" />"#).is_empty());
1505 assert!(asset_refs(r#"<img v-bind:src="logo" />"#).is_empty());
1506 assert!(asset_refs(r#"<img bind:src="logo" />"#).is_empty());
1507 assert!(asset_refs(r"<img src={logo} />").is_empty());
1508 assert!(asset_refs(r#"<img data-src="./x.png" />"#).is_empty());
1509 assert!(asset_refs(r#"<img src="@/assets/x.png" />"#).is_empty());
1511 assert!(asset_refs(r#"<img src="/logo.png" />"#).is_empty());
1512 assert!(asset_refs(r#"<img src="https://cdn/x.png" />"#).is_empty());
1513 assert!(asset_refs(r#"<img src="./x.png?inline" />"#).is_empty());
1515 assert!(asset_refs(r#"<img src="{{ logo }}" />"#).is_empty());
1517 }
1518
1519 #[test]
1520 fn skips_custom_component_src_prop() {
1521 assert!(asset_refs(r#"<MyImage src="./x.png" />"#).is_empty());
1523 assert!(asset_refs(r#"<AppIcon src="../icons/y.svg" />"#).is_empty());
1524 }
1525
1526 #[test]
1527 fn skips_asset_refs_inside_script_style_and_comments() {
1528 assert!(asset_refs(r#"<script>const x = "<img src='./a.png'>"</script>"#).is_empty());
1530 assert!(asset_refs(r#"<style>/* <img src="./b.png"> */ .x{}</style>"#).is_empty());
1531 assert!(asset_refs(r#"<!-- <img src="./c.png" /> -->"#).is_empty());
1532 }
1533
1534 #[test]
1535 fn parse_sfc_emits_template_asset_as_side_effect_import() {
1536 let info = parse_sfc_to_module(
1537 FileId(0),
1538 Path::new("Hero.vue"),
1539 r#"<template><img src="./hero.png" /></template><script>let x=1</script>"#,
1540 0,
1541 false,
1542 );
1543 assert!(
1544 info.imports.iter().any(|i| i.source == "./hero.png"
1545 && matches!(i.imported_name, ImportedName::SideEffect)
1546 && !i.from_style),
1547 "template <img src> should seed a SideEffect import: {:?}",
1548 info.imports
1549 );
1550 }
1551}