1use std::path::Path;
11use std::sync::LazyLock;
12
13use oxc_span::Span;
14
15use crate::asset_url::normalize_asset_url;
16use crate::sfc_template::angular;
17use crate::{
18 AngularTemplateMemberAccessFact, ImportInfo, ImportedName, MemberAccess, ModuleInfo,
19 SemanticFact,
20};
21use fallow_types::discover::FileId;
22
23static HTML_COMMENT_RE: LazyLock<regex::Regex> =
25 LazyLock::new(|| crate::static_regex(r"(?s)<!--.*?-->"));
26
27static SCRIPT_SRC_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
31 crate::static_regex(r#"(?si)<script\b(?:[^>"']|"[^"]*"|'[^']*')*?\bsrc\s*=\s*["']([^"']+)["']"#)
32});
33
34static LINK_HREF_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
38 crate::static_regex(
39 r#"(?si)<link\b(?:[^>"']|"[^"]*"|'[^']*')*?\brel\s*=\s*["'](stylesheet|modulepreload)["'](?:[^>"']|"[^"]*"|'[^']*')*?\bhref\s*=\s*["']([^"']+)["']"#,
40 )
41});
42
43static LINK_HREF_REVERSE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
45 crate::static_regex(
46 r#"(?si)<link\b(?:[^>"']|"[^"]*"|'[^']*')*?\bhref\s*=\s*["']([^"']+)["'](?:[^>"']|"[^"]*"|'[^']*')*?\brel\s*=\s*["'](stylesheet|modulepreload)["']"#,
47 )
48});
49
50pub(crate) fn is_html_file(path: &Path) -> bool {
52 path.extension()
53 .and_then(|e| e.to_str())
54 .is_some_and(|ext| ext == "html")
55}
56
57pub(crate) fn is_remote_url(src: &str) -> bool {
59 src.starts_with("http://")
60 || src.starts_with("https://")
61 || src.starts_with("//")
62 || src.starts_with("data:")
63}
64
65pub(crate) fn is_template_placeholder(value: &str) -> bool {
80 value.contains("{{") || value.contains("###")
81}
82
83pub(crate) fn collect_asset_refs(source: &str) -> Vec<String> {
90 let stripped = HTML_COMMENT_RE.replace_all(source, "");
91 let mut refs: Vec<String> = Vec::new();
92
93 for cap in SCRIPT_SRC_RE.captures_iter(&stripped) {
94 if let Some(m) = cap.get(1) {
95 let src = m.as_str().trim();
96 if !src.is_empty() && !is_remote_url(src) && !is_template_placeholder(src) {
97 refs.push(src.to_string());
98 }
99 }
100 }
101
102 for cap in LINK_HREF_RE.captures_iter(&stripped) {
103 if let Some(m) = cap.get(2) {
104 let href = m.as_str().trim();
105 if !href.is_empty() && !is_remote_url(href) && !is_template_placeholder(href) {
106 refs.push(href.to_string());
107 }
108 }
109 }
110 for cap in LINK_HREF_REVERSE_RE.captures_iter(&stripped) {
111 if let Some(m) = cap.get(1) {
112 let href = m.as_str().trim();
113 if !href.is_empty() && !is_remote_url(href) && !is_template_placeholder(href) {
114 refs.push(href.to_string());
115 }
116 }
117 }
118
119 refs
120}
121
122static CUSTOM_ELEMENT_TAG_RE: std::sync::LazyLock<regex::Regex> =
127 std::sync::LazyLock::new(|| crate::static_regex(r"</?\s*([a-z][a-z0-9]*-[a-z0-9-]*)"));
128
129pub(crate) fn collect_custom_element_tags(source: &str) -> Vec<String> {
135 let stripped = HTML_COMMENT_RE.replace_all(source, "");
136 let mut tags: Vec<String> = Vec::new();
137 for cap in CUSTOM_ELEMENT_TAG_RE.captures_iter(&stripped) {
138 if let Some(m) = cap.get(1) {
139 let tag = m.as_str();
140 if !tags.iter().any(|t| t == tag) {
141 tags.push(tag.to_string());
142 }
143 }
144 }
145 tags
146}
147
148#[cfg(test)]
150pub(crate) fn parse_html_to_module(file_id: FileId, source: &str, content_hash: u64) -> ModuleInfo {
151 parse_html_to_module_with_complexity(file_id, source, content_hash, false)
152}
153
154struct HtmlModuleParts {
157 imports: Vec<ImportInfo>,
158 member_accesses: Vec<MemberAccess>,
159 semantic_facts: Vec<SemanticFact>,
160 security_sinks: Vec<fallow_types::extract::SinkSite>,
161 angular_used_selectors: Vec<String>,
162 has_dynamic_component_render: bool,
163 complexity: Vec<fallow_types::extract::FunctionComplexity>,
164}
165
166fn collect_html_module_parts(source: &str, need_complexity: bool) -> HtmlModuleParts {
170 let mut imports: Vec<ImportInfo> = collect_asset_refs(source)
171 .into_iter()
172 .map(|raw| ImportInfo {
173 source: normalize_asset_url(&raw),
174 imported_name: ImportedName::SideEffect,
175 local_name: String::new(),
176 is_type_only: false,
177 from_style: false,
178 span: Span::default(),
179 source_span: Span::default(),
180 })
181 .collect();
182
183 imports.sort_unstable_by(|a, b| a.source.cmp(&b.source));
184 imports.dedup_by(|a, b| a.source == b.source);
185
186 let angular::AngularTemplateRefs {
187 identifiers,
188 member_accesses: template_member_accesses,
189 security_sinks,
190 } = angular::collect_angular_template_refs(source);
191 let identifiers: Vec<String> = identifiers.into_iter().collect();
192 let semantic_facts: Vec<SemanticFact> = identifiers
193 .iter()
194 .cloned()
195 .map(|member| {
196 SemanticFact::AngularTemplateMemberAccess(AngularTemplateMemberAccessFact { member })
197 })
198 .collect();
199 let member_accesses = template_member_accesses;
200
201 let angular_used_selectors = angular::collect_angular_used_selectors(source);
206 let has_dynamic_component_render = source.contains("ngComponentOutlet");
207
208 let complexity = if need_complexity {
209 crate::template_complexity::compute_angular_template_complexity(source)
210 .into_iter()
211 .collect()
212 } else {
213 Vec::new()
214 };
215
216 HtmlModuleParts {
217 imports,
218 member_accesses,
219 semantic_facts,
220 security_sinks,
221 angular_used_selectors,
222 has_dynamic_component_render,
223 complexity,
224 }
225}
226
227pub(crate) fn parse_html_to_module_with_complexity(
229 file_id: FileId,
230 source: &str,
231 content_hash: u64,
232 need_complexity: bool,
233) -> ModuleInfo {
234 let parsed_suppressions = crate::suppress::parse_suppressions_from_source(source);
235 let parts = collect_html_module_parts(source, need_complexity);
236 html_module_info(file_id, content_hash, source, parsed_suppressions, parts)
237}
238
239fn html_module_info(
243 file_id: FileId,
244 content_hash: u64,
245 source: &str,
246 parsed_suppressions: crate::suppress::ParsedSuppressions,
247 parts: HtmlModuleParts,
248) -> ModuleInfo {
249 let HtmlModuleParts {
250 imports,
251 member_accesses,
252 semantic_facts,
253 security_sinks,
254 angular_used_selectors,
255 has_dynamic_component_render,
256 complexity,
257 } = parts;
258
259 ModuleInfo {
260 file_id,
261 exports: Vec::new(),
262 imports,
263 re_exports: Vec::new(),
264 dynamic_imports: Vec::new(),
265 dynamic_import_patterns: Vec::new(),
266 require_calls: Vec::new(),
267 package_path_references: Box::default(),
268 member_accesses,
269 semantic_facts: semantic_facts.into(),
270 whole_object_uses: Box::default(),
271 has_cjs_exports: false,
272 has_angular_component_template_url: false,
273 content_hash,
274 suppressions: parsed_suppressions.suppressions,
275 unknown_suppression_kinds: parsed_suppressions.unknown_kinds,
276 unused_import_bindings: Vec::new(),
277 type_referenced_import_bindings: Vec::new(),
278 value_referenced_import_bindings: Vec::new(),
279 line_offsets: fallow_types::extract::compute_line_offsets(source),
280 complexity,
281 flag_uses: Vec::new(),
282 class_heritage: vec![],
283 exported_factory_returns: Box::default(),
284 type_member_types: Box::default(),
285 injection_tokens: vec![],
286 local_type_declarations: Vec::new(),
287 public_signature_type_references: Vec::new(),
288 namespace_object_aliases: Vec::new(),
289 iconify_prefixes: Vec::new(),
290 iconify_icon_names: Vec::new(),
291 auto_import_candidates: Vec::new(),
292 directives: Vec::new(),
293 client_only_dynamic_import_spans: Vec::new(),
294 security_sinks,
295 security_sinks_skipped: 0,
296 security_unresolved_callee_sites: Vec::new(),
297 tainted_bindings: Vec::new(),
298 sanitized_sink_args: Vec::new(),
299 security_control_sites: Vec::new(),
300 callee_uses: Vec::new(),
301 misplaced_directives: Vec::new(),
302 inline_server_action_exports: Vec::new(),
303 di_key_sites: Vec::new(),
304 has_dynamic_provide: false,
305 referenced_import_bindings: Vec::new(),
306 component_props: Vec::new(),
307 has_props_attrs_fallthrough: false,
308 has_define_expose: false,
309 has_define_model: false,
310 has_unharvestable_props: false,
311 component_emits: Vec::new(),
312 angular_inputs: Vec::new(),
313 angular_outputs: Vec::new(),
314 angular_component_selectors: Vec::new(),
315 registered_custom_elements: Vec::new(),
316 used_custom_element_tags: collect_custom_element_tags(source),
321 angular_used_selectors,
322 angular_entry_component_refs: Vec::new(),
323 has_dynamic_component_render,
324 has_unharvestable_emits: false,
325 has_dynamic_emit: false,
326 has_emit_whole_object_use: false,
327 load_return_keys: Vec::new(),
328 has_unharvestable_load: false,
329 has_load_data_whole_use: false,
330 has_page_data_store_whole_use: false,
331 component_functions: Vec::new(),
332 react_props: Vec::new(),
333 hook_uses: Vec::new(),
334 render_edges: Vec::new(),
335 svelte_dispatched_events: Vec::new(),
336 svelte_listened_events: Vec::new(),
337 has_dynamic_dispatch: false,
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn is_html_file_html() {
347 assert!(is_html_file(Path::new("index.html")));
348 }
349
350 #[test]
351 fn is_html_file_nested() {
352 assert!(is_html_file(Path::new("pages/about.html")));
353 }
354
355 #[test]
356 fn is_html_file_rejects_htm() {
357 assert!(!is_html_file(Path::new("index.htm")));
358 }
359
360 #[test]
361 fn is_html_file_rejects_js() {
362 assert!(!is_html_file(Path::new("app.js")));
363 }
364
365 #[test]
366 fn is_html_file_rejects_ts() {
367 assert!(!is_html_file(Path::new("app.ts")));
368 }
369
370 #[test]
371 fn is_html_file_rejects_vue() {
372 assert!(!is_html_file(Path::new("App.vue")));
373 }
374
375 #[test]
376 fn remote_url_http() {
377 assert!(is_remote_url("http://example.com/script.js"));
378 }
379
380 #[test]
381 fn remote_url_https() {
382 assert!(is_remote_url("https://cdn.example.com/style.css"));
383 }
384
385 #[test]
386 fn remote_url_protocol_relative() {
387 assert!(is_remote_url("//cdn.example.com/lib.js"));
388 }
389
390 #[test]
391 fn remote_url_data() {
392 assert!(is_remote_url("data:text/javascript;base64,abc"));
393 }
394
395 #[test]
396 fn local_relative_not_remote() {
397 assert!(!is_remote_url("./src/entry.js"));
398 }
399
400 #[test]
401 fn local_root_relative_not_remote() {
402 assert!(!is_remote_url("/src/entry.js"));
403 }
404
405 #[test]
406 fn extracts_module_script_src() {
407 let info = parse_html_to_module(
408 FileId(0),
409 r#"<script type="module" src="./src/entry.js"></script>"#,
410 0,
411 );
412 assert_eq!(info.imports.len(), 1);
413 assert_eq!(info.imports[0].source, "./src/entry.js");
414 }
415
416 #[test]
417 fn extracts_plain_script_src() {
418 let info = parse_html_to_module(
419 FileId(0),
420 r#"<script src="./src/polyfills.js"></script>"#,
421 0,
422 );
423 assert_eq!(info.imports.len(), 1);
424 assert_eq!(info.imports[0].source, "./src/polyfills.js");
425 }
426
427 #[test]
428 fn extracts_multiple_scripts() {
429 let info = parse_html_to_module(
430 FileId(0),
431 r#"
432 <script type="module" src="./src/entry.js"></script>
433 <script src="./src/polyfills.js"></script>
434 "#,
435 0,
436 );
437 assert_eq!(info.imports.len(), 2);
438 }
439
440 #[test]
441 fn skips_inline_script() {
442 let info = parse_html_to_module(FileId(0), r#"<script>console.log("hello");</script>"#, 0);
443 assert!(info.imports.is_empty());
444 }
445
446 #[test]
447 fn skips_handlebars_placeholder_in_script_src() {
448 let info = parse_html_to_module(
449 FileId(0),
450 r#"<script src="{{rootURL}}assets/app.js"></script>
451 <script src="{{config.assetsPath}}vendor.js"></script>"#,
452 0,
453 );
454 assert!(
455 info.imports.is_empty(),
456 "Handlebars-placeholder script srcs should not enter the import graph; got {:?}",
457 info.imports
458 );
459 }
460
461 #[test]
462 fn skips_handlebars_placeholder_in_link_href() {
463 let info = parse_html_to_module(
464 FileId(0),
465 r#"<link rel="stylesheet" href="{{rootURL}}assets/app.css">"#,
466 0,
467 );
468 assert!(info.imports.is_empty());
469 }
470
471 #[test]
472 fn skips_ember_cli_blueprint_placeholder() {
473 let info = parse_html_to_module(
474 FileId(0),
475 r####"<script src="###APPNAME###/app.js"></script>"####,
476 0,
477 );
478 assert!(info.imports.is_empty());
479 }
480
481 #[test]
482 fn extracts_normal_specifier_alongside_placeholders() {
483 let info = parse_html_to_module(
484 FileId(0),
485 r#"<script src="{{rootURL}}assets/app.js"></script>
486 <script src="./src/main.ts"></script>"#,
487 0,
488 );
489 assert_eq!(info.imports.len(), 1);
490 assert_eq!(info.imports[0].source, "./src/main.ts");
491 }
492
493 #[test]
494 fn skips_remote_script() {
495 let info = parse_html_to_module(
496 FileId(0),
497 r#"<script src="https://cdn.example.com/lib.js"></script>"#,
498 0,
499 );
500 assert!(info.imports.is_empty());
501 }
502
503 #[test]
504 fn skips_protocol_relative_script() {
505 let info = parse_html_to_module(
506 FileId(0),
507 r#"<script src="//cdn.example.com/lib.js"></script>"#,
508 0,
509 );
510 assert!(info.imports.is_empty());
511 }
512
513 #[test]
514 fn extracts_stylesheet_link() {
515 let info = parse_html_to_module(
516 FileId(0),
517 r#"<link rel="stylesheet" href="./src/global.css" />"#,
518 0,
519 );
520 assert_eq!(info.imports.len(), 1);
521 assert_eq!(info.imports[0].source, "./src/global.css");
522 }
523
524 #[test]
525 fn extracts_modulepreload_link() {
526 let info = parse_html_to_module(
527 FileId(0),
528 r#"<link rel="modulepreload" href="./src/vendor.js" />"#,
529 0,
530 );
531 assert_eq!(info.imports.len(), 1);
532 assert_eq!(info.imports[0].source, "./src/vendor.js");
533 }
534
535 #[test]
536 fn extracts_link_with_reversed_attrs() {
537 let info = parse_html_to_module(
538 FileId(0),
539 r#"<link href="./src/global.css" rel="stylesheet" />"#,
540 0,
541 );
542 assert_eq!(info.imports.len(), 1);
543 assert_eq!(info.imports[0].source, "./src/global.css");
544 }
545
546 #[test]
547 fn bare_script_src_normalized_to_relative() {
548 let info = parse_html_to_module(FileId(0), r#"<script src="app.js"></script>"#, 0);
549 assert_eq!(info.imports.len(), 1);
550 assert_eq!(info.imports[0].source, "./app.js");
551 }
552
553 #[test]
554 fn bare_module_script_src_normalized_to_relative() {
555 let info = parse_html_to_module(
556 FileId(0),
557 r#"<script type="module" src="main.ts"></script>"#,
558 0,
559 );
560 assert_eq!(info.imports.len(), 1);
561 assert_eq!(info.imports[0].source, "./main.ts");
562 }
563
564 #[test]
565 fn bare_stylesheet_link_href_normalized_to_relative() {
566 let info = parse_html_to_module(
567 FileId(0),
568 r#"<link rel="stylesheet" href="styles.css" />"#,
569 0,
570 );
571 assert_eq!(info.imports.len(), 1);
572 assert_eq!(info.imports[0].source, "./styles.css");
573 }
574
575 #[test]
576 fn bare_link_href_reversed_attrs_normalized_to_relative() {
577 let info = parse_html_to_module(
578 FileId(0),
579 r#"<link href="styles.css" rel="stylesheet" />"#,
580 0,
581 );
582 assert_eq!(info.imports.len(), 1);
583 assert_eq!(info.imports[0].source, "./styles.css");
584 }
585
586 #[test]
587 fn bare_modulepreload_link_href_normalized_to_relative() {
588 let info = parse_html_to_module(
589 FileId(0),
590 r#"<link rel="modulepreload" href="vendor.js" />"#,
591 0,
592 );
593 assert_eq!(info.imports.len(), 1);
594 assert_eq!(info.imports[0].source, "./vendor.js");
595 }
596
597 #[test]
598 fn bare_asset_with_subdir_normalized_to_relative() {
599 let info = parse_html_to_module(FileId(0), r#"<script src="assets/app.js"></script>"#, 0);
600 assert_eq!(info.imports.len(), 1);
601 assert_eq!(info.imports[0].source, "./assets/app.js");
602 }
603
604 #[test]
605 fn root_absolute_script_src_unchanged() {
606 let info = parse_html_to_module(FileId(0), r#"<script src="/src/main.ts"></script>"#, 0);
607 assert_eq!(info.imports.len(), 1);
608 assert_eq!(info.imports[0].source, "/src/main.ts");
609 }
610
611 #[test]
612 fn parent_relative_script_src_unchanged() {
613 let info = parse_html_to_module(
614 FileId(0),
615 r#"<script src="../shared/vendor.js"></script>"#,
616 0,
617 );
618 assert_eq!(info.imports.len(), 1);
619 assert_eq!(info.imports[0].source, "../shared/vendor.js");
620 }
621
622 #[test]
623 fn skips_preload_link() {
624 let info = parse_html_to_module(
625 FileId(0),
626 r#"<link rel="preload" href="./src/font.woff2" as="font" />"#,
627 0,
628 );
629 assert!(info.imports.is_empty());
630 }
631
632 #[test]
633 fn skips_icon_link() {
634 let info =
635 parse_html_to_module(FileId(0), r#"<link rel="icon" href="./favicon.ico" />"#, 0);
636 assert!(info.imports.is_empty());
637 }
638
639 #[test]
640 fn skips_remote_stylesheet() {
641 let info = parse_html_to_module(
642 FileId(0),
643 r#"<link rel="stylesheet" href="https://fonts.googleapis.com/css" />"#,
644 0,
645 );
646 assert!(info.imports.is_empty());
647 }
648
649 #[test]
650 fn skips_commented_out_script() {
651 let info = parse_html_to_module(
652 FileId(0),
653 r#"<!-- <script src="./old.js"></script> -->
654 <script src="./new.js"></script>"#,
655 0,
656 );
657 assert_eq!(info.imports.len(), 1);
658 assert_eq!(info.imports[0].source, "./new.js");
659 }
660
661 #[test]
662 fn skips_commented_out_link() {
663 let info = parse_html_to_module(
664 FileId(0),
665 r#"<!-- <link rel="stylesheet" href="./old.css" /> -->
666 <link rel="stylesheet" href="./new.css" />"#,
667 0,
668 );
669 assert_eq!(info.imports.len(), 1);
670 assert_eq!(info.imports[0].source, "./new.css");
671 }
672
673 #[test]
674 fn handles_multiline_script_tag() {
675 let info = parse_html_to_module(
676 FileId(0),
677 "<script\n type=\"module\"\n src=\"./src/entry.js\"\n></script>",
678 0,
679 );
680 assert_eq!(info.imports.len(), 1);
681 assert_eq!(info.imports[0].source, "./src/entry.js");
682 }
683
684 #[test]
685 fn handles_multiline_link_tag() {
686 let info = parse_html_to_module(
687 FileId(0),
688 "<link\n rel=\"stylesheet\"\n href=\"./src/global.css\"\n/>",
689 0,
690 );
691 assert_eq!(info.imports.len(), 1);
692 assert_eq!(info.imports[0].source, "./src/global.css");
693 }
694
695 #[test]
696 fn full_vite_html() {
697 let info = parse_html_to_module(
698 FileId(0),
699 r#"<!doctype html>
700<html>
701 <head>
702 <link rel="stylesheet" href="./src/global.css" />
703 <link rel="icon" href="/favicon.ico" />
704 </head>
705 <body>
706 <div id="app"></div>
707 <script type="module" src="./src/entry.js"></script>
708 </body>
709</html>"#,
710 0,
711 );
712 assert_eq!(info.imports.len(), 2);
713 let sources: Vec<&str> = info.imports.iter().map(|i| i.source.as_str()).collect();
714 assert!(sources.contains(&"./src/global.css"));
715 assert!(sources.contains(&"./src/entry.js"));
716 }
717
718 #[test]
719 fn empty_html() {
720 let info = parse_html_to_module(FileId(0), "", 0);
721 assert!(info.imports.is_empty());
722 }
723
724 #[test]
725 fn html_with_no_assets() {
726 let info = parse_html_to_module(
727 FileId(0),
728 r"<!doctype html><html><body><h1>Hello</h1></body></html>",
729 0,
730 );
731 assert!(info.imports.is_empty());
732 }
733
734 #[test]
735 fn single_quoted_attributes() {
736 let info = parse_html_to_module(FileId(0), r"<script src='./src/entry.js'></script>", 0);
737 assert_eq!(info.imports.len(), 1);
738 assert_eq!(info.imports[0].source, "./src/entry.js");
739 }
740
741 #[test]
742 fn all_imports_are_side_effect() {
743 let info = parse_html_to_module(
744 FileId(0),
745 r#"<script src="./entry.js"></script>
746 <link rel="stylesheet" href="./style.css" />"#,
747 0,
748 );
749 for imp in &info.imports {
750 assert!(matches!(imp.imported_name, ImportedName::SideEffect));
751 assert!(imp.local_name.is_empty());
752 assert!(!imp.is_type_only);
753 }
754 }
755
756 #[test]
757 fn suppression_comments_extracted() {
758 let info = parse_html_to_module(
759 FileId(0),
760 "<!-- fallow-ignore-file -->\n<script src=\"./entry.js\"></script>",
761 0,
762 );
763 assert_eq!(info.imports.len(), 1);
764 }
765
766 #[test]
767 fn angular_template_extracts_member_refs() {
768 let info = parse_html_to_module(
769 FileId(0),
770 "<h1>{{ title() }}</h1>\n\
771 <p [class.highlighted]=\"isHighlighted\">{{ greeting() }}</p>\n\
772 <button (click)=\"onButtonClick()\">Toggle</button>",
773 0,
774 );
775 let fact_names: rustc_hash::FxHashSet<&str> = info
776 .semantic_facts
777 .iter()
778 .filter_map(|fact| {
779 if let SemanticFact::AngularTemplateMemberAccess(access) = fact {
780 Some(access.member.as_str())
781 } else {
782 None
783 }
784 })
785 .collect();
786 assert!(fact_names.contains("title"), "should contain 'title'");
787 assert!(
788 fact_names.contains("isHighlighted"),
789 "should contain 'isHighlighted'"
790 );
791 assert!(fact_names.contains("greeting"), "should contain 'greeting'");
792 assert!(
793 fact_names.contains("onButtonClick"),
794 "should contain 'onButtonClick'"
795 );
796 assert!(
797 info.member_accesses.is_empty(),
798 "Angular template refs should emit typed facts instead of member accesses: {:?}",
799 info.member_accesses
800 );
801 }
802
803 #[test]
804 fn plain_html_no_angular_refs() {
805 let info = parse_html_to_module(
806 FileId(0),
807 "<!doctype html><html><body><h1>Hello</h1></body></html>",
808 0,
809 );
810 assert!(info.member_accesses.is_empty());
811 }
812}