1use std::collections::{HashMap, HashSet};
2
3use fallow_config::{PackageJson, ResolvedConfig};
4
5use crate::discover::FileId;
6use crate::extract::{MemberKind, ModuleInfo};
7use crate::graph::ModuleGraph;
8use crate::resolve::ResolvedModule;
9use crate::results::*;
10use crate::suppress::{self, IssueKind, Suppression};
11
12fn byte_offset_to_line_col(source: &str, byte_offset: u32) -> (u32, u32) {
15 let byte_offset = byte_offset as usize;
16 let prefix = &source[..byte_offset.min(source.len())];
17 let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32 + 1;
18 let col = prefix
19 .rfind('\n')
20 .map(|pos| byte_offset - pos - 1)
21 .unwrap_or(byte_offset) as u32;
22 (line, col)
23}
24
25fn read_source(path: &std::path::Path) -> String {
27 std::fs::read_to_string(path).unwrap_or_default()
28}
29
30pub fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
32 find_dead_code_with_resolved(graph, config, &[], None)
33}
34
35pub fn find_dead_code_with_resolved(
37 graph: &ModuleGraph,
38 config: &ResolvedConfig,
39 resolved_modules: &[ResolvedModule],
40 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
41) -> AnalysisResults {
42 find_dead_code_full(graph, config, resolved_modules, plugin_result, &[], &[])
43}
44
45pub fn find_dead_code_full(
47 graph: &ModuleGraph,
48 config: &ResolvedConfig,
49 resolved_modules: &[ResolvedModule],
50 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
51 workspaces: &[fallow_config::WorkspaceInfo],
52 modules: &[ModuleInfo],
53) -> AnalysisResults {
54 let _span = tracing::info_span!("find_dead_code").entered();
55
56 let suppressions_by_file: HashMap<FileId, &[Suppression]> = modules
58 .iter()
59 .filter(|m| !m.suppressions.is_empty())
60 .map(|m| (m.file_id, m.suppressions.as_slice()))
61 .collect();
62
63 let mut results = AnalysisResults::default();
64
65 if config.detect.unused_files {
66 results.unused_files = find_unused_files(graph, &suppressions_by_file);
67 }
68
69 if config.detect.unused_exports || config.detect.unused_types {
70 let (exports, types) =
71 find_unused_exports(graph, config, plugin_result, &suppressions_by_file);
72 if config.detect.unused_exports {
73 results.unused_exports = exports;
74 }
75 if config.detect.unused_types {
76 results.unused_types = types;
77 }
78 }
79
80 if config.detect.unused_enum_members || config.detect.unused_class_members {
81 let (enum_members, class_members) =
82 find_unused_members(graph, config, resolved_modules, &suppressions_by_file);
83 if config.detect.unused_enum_members {
84 results.unused_enum_members = enum_members;
85 }
86 if config.detect.unused_class_members {
87 results.unused_class_members = class_members;
88 }
89 }
90
91 let pkg_path = config.root.join("package.json");
93 if let Ok(pkg) = PackageJson::load(&pkg_path) {
94 if config.detect.unused_dependencies || config.detect.unused_dev_dependencies {
95 let (deps, dev_deps) =
96 find_unused_dependencies(graph, &pkg, config, plugin_result, workspaces);
97 if config.detect.unused_dependencies {
98 results.unused_dependencies = deps;
99 }
100 if config.detect.unused_dev_dependencies {
101 results.unused_dev_dependencies = dev_deps;
102 }
103 }
104
105 if config.detect.unlisted_dependencies {
106 results.unlisted_dependencies =
107 find_unlisted_dependencies(graph, &pkg, config, workspaces);
108 }
109 }
110
111 if config.detect.unresolved_imports && !resolved_modules.is_empty() {
112 results.unresolved_imports =
113 find_unresolved_imports(resolved_modules, config, &suppressions_by_file);
114 }
115
116 if config.detect.duplicate_exports {
117 results.duplicate_exports = find_duplicate_exports(graph, config, &suppressions_by_file);
118 }
119
120 if config.production {
122 let pkg_path = config.root.join("package.json");
123 if let Ok(pkg) = PackageJson::load(&pkg_path) {
124 results.type_only_dependencies =
125 find_type_only_dependencies(graph, &pkg, config, workspaces);
126 }
127 }
128
129 results
130}
131
132fn find_unused_files(
145 graph: &ModuleGraph,
146 suppressions_by_file: &HashMap<FileId, &[Suppression]>,
147) -> Vec<UnusedFile> {
148 graph
149 .modules
150 .iter()
151 .filter(|m| !m.is_reachable && !m.is_entry_point)
152 .filter(|m| !is_declaration_file(&m.path))
153 .filter(|m| !is_config_file(&m.path))
154 .filter(|m| !is_barrel_with_reachable_sources(m, graph))
155 .filter(|m| {
156 !suppressions_by_file
157 .get(&m.file_id)
158 .is_some_and(|supps| suppress::is_file_suppressed(supps, IssueKind::UnusedFile))
159 })
160 .map(|m| UnusedFile {
161 path: m.path.clone(),
162 })
163 .collect()
164}
165
166fn is_declaration_file(path: &std::path::Path) -> bool {
168 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
169 name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
170}
171
172fn is_barrel_with_reachable_sources(
179 module: &crate::graph::ModuleNode,
180 graph: &ModuleGraph,
181) -> bool {
182 if module.re_exports.is_empty() {
184 return false;
185 }
186
187 let has_local_exports = module
190 .exports
191 .iter()
192 .any(|e| e.span.start != 0 || e.span.end != 0);
193 if has_local_exports || module.has_cjs_exports {
194 return false;
195 }
196
197 module.re_exports.iter().any(|re| {
199 let source_idx = re.source_file.0 as usize;
200 graph
201 .modules
202 .get(source_idx)
203 .is_some_and(|m| m.is_reachable)
204 })
205}
206
207fn is_config_file(path: &std::path::Path) -> bool {
213 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
214
215 if name.starts_with('.') && !name.starts_with("..") {
218 let lower = name.to_ascii_lowercase();
219 if lower.contains("rc.") {
221 return true;
222 }
223 }
224
225 let config_patterns = [
228 "babel.config.",
230 "rollup.config.",
231 "webpack.config.",
232 "postcss.config.",
233 "stencil.config.",
234 "remotion.config.",
235 "metro.config.",
236 "tsup.config.",
237 "unbuild.config.",
238 "esbuild.config.",
239 "swc.config.",
240 "turbo.",
241 "jest.config.",
243 "jest.setup.",
244 "vitest.config.",
245 "vitest.ci.config.",
246 "vitest.setup.",
247 "vitest.workspace.",
248 "playwright.config.",
249 "cypress.config.",
250 "eslint.config.",
252 "prettier.config.",
253 "stylelint.config.",
254 "lint-staged.config.",
255 "commitlint.config.",
256 "next.config.",
258 "next-sitemap.config.",
259 "nuxt.config.",
260 "astro.config.",
261 "sanity.config.",
262 "vite.config.",
263 "tailwind.config.",
264 "drizzle.config.",
265 "knexfile.",
266 "sentry.client.config.",
267 "sentry.server.config.",
268 "sentry.edge.config.",
269 "react-router.config.",
270 "typedoc.",
272 "knip.config.",
274 "fallow.config.",
275 "i18next-parser.config.",
276 "codegen.config.",
277 "graphql.config.",
278 "npmpackagejsonlint.config.",
279 "release-it.",
280 "release.config.",
281 "contentlayer.config.",
282 "next-env.d.",
284 "env.d.",
285 "vite-env.d.",
286 ];
287
288 config_patterns.iter().any(|p| name.starts_with(p))
289}
290
291fn find_unused_exports(
293 graph: &ModuleGraph,
294 config: &ResolvedConfig,
295 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
296 suppressions_by_file: &HashMap<FileId, &[Suppression]>,
297) -> (Vec<UnusedExport>, Vec<UnusedExport>) {
298 let mut unused_exports = Vec::new();
299 let mut unused_types = Vec::new();
300
301 let ignore_matchers: Vec<(globset::GlobMatcher, &[String])> = config
303 .ignore_export_rules
304 .iter()
305 .filter_map(|rule| {
306 globset::Glob::new(&rule.file)
307 .ok()
308 .map(|g| (g.compile_matcher(), rule.exports.as_slice()))
309 })
310 .collect();
311
312 let framework_matchers: Vec<(globset::GlobMatcher, &[String])> = config
313 .framework_rules
314 .iter()
315 .flat_map(|rule| &rule.used_exports)
316 .filter_map(|used| {
317 globset::Glob::new(&used.file_pattern)
318 .ok()
319 .map(|g| (g.compile_matcher(), used.exports.as_slice()))
320 })
321 .collect();
322
323 let plugin_matchers: Vec<(globset::GlobMatcher, Vec<&str>)> = plugin_result
325 .map(|pr| {
326 pr.used_exports
327 .iter()
328 .filter_map(|(file_pat, exports)| {
329 globset::Glob::new(file_pat).ok().map(|g| {
330 (
331 g.compile_matcher(),
332 exports.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
333 )
334 })
335 })
336 .collect()
337 })
338 .unwrap_or_default();
339
340 for module in &graph.modules {
341 if !module.is_reachable {
343 continue;
344 }
345
346 if module.is_entry_point {
348 continue;
349 }
350
351 if module.has_cjs_exports && module.exports.is_empty() {
353 continue;
354 }
355
356 if module.path.extension().is_some_and(|ext| ext == "svelte") {
365 continue;
366 }
367
368 let relative_path = module
370 .path
371 .strip_prefix(&config.root)
372 .unwrap_or(&module.path);
373 let file_str = relative_path.to_string_lossy();
374
375 let matching_ignore: Vec<&[String]> = ignore_matchers
377 .iter()
378 .filter(|(m, _)| m.is_match(file_str.as_ref()))
379 .map(|(_, exports)| *exports)
380 .collect();
381
382 let matching_framework: Vec<&[String]> = framework_matchers
383 .iter()
384 .filter(|(m, _)| m.is_match(file_str.as_ref()))
385 .map(|(_, exports)| *exports)
386 .collect();
387
388 let matching_plugin: Vec<&Vec<&str>> = plugin_matchers
390 .iter()
391 .filter(|(m, _)| m.is_match(file_str.as_ref()))
392 .map(|(_, exports)| exports)
393 .collect();
394
395 let mut source_content: Option<String> = None;
397
398 for export in &module.exports {
399 if export.references.is_empty() {
400 let export_str = export.name.to_string();
401
402 if matching_ignore
404 .iter()
405 .any(|exports| exports.iter().any(|e| e == "*" || e == &export_str))
406 {
407 continue;
408 }
409
410 if matching_framework
412 .iter()
413 .any(|exports| exports.iter().any(|e| e == &export_str))
414 {
415 continue;
416 }
417
418 if matching_plugin
420 .iter()
421 .any(|exports| exports.iter().any(|e| *e == export_str))
422 {
423 continue;
424 }
425
426 let source = source_content.get_or_insert_with(|| read_source(&module.path));
427 let (line, col) = byte_offset_to_line_col(source, export.span.start);
428
429 let is_re_export = export.span.start == 0 && export.span.end == 0;
431
432 let issue_kind = if export.is_type_only {
434 IssueKind::UnusedType
435 } else {
436 IssueKind::UnusedExport
437 };
438 if let Some(supps) = suppressions_by_file.get(&module.file_id)
439 && suppress::is_suppressed(supps, line, issue_kind)
440 {
441 continue;
442 }
443
444 let unused = UnusedExport {
445 path: module.path.clone(),
446 export_name: export_str,
447 is_type_only: export.is_type_only,
448 line,
449 col,
450 span_start: export.span.start,
451 is_re_export,
452 };
453
454 if export.is_type_only {
455 unused_types.push(unused);
456 } else {
457 unused_exports.push(unused);
458 }
459 }
460 }
461 }
462
463 (unused_exports, unused_types)
464}
465
466fn find_unused_dependencies(
472 graph: &ModuleGraph,
473 pkg: &PackageJson,
474 config: &ResolvedConfig,
475 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
476 workspaces: &[fallow_config::WorkspaceInfo],
477) -> (Vec<UnusedDependency>, Vec<UnusedDependency>) {
478 let plugin_referenced: HashSet<&str> = plugin_result
480 .map(|pr| {
481 pr.referenced_dependencies
482 .iter()
483 .map(|s| s.as_str())
484 .collect()
485 })
486 .unwrap_or_default();
487
488 let plugin_tooling: HashSet<&str> = plugin_result
490 .map(|pr| pr.tooling_dependencies.iter().map(|s| s.as_str()).collect())
491 .unwrap_or_default();
492
493 let script_used: HashSet<&str> = plugin_result
495 .map(|pr| pr.script_used_packages.iter().map(|s| s.as_str()).collect())
496 .unwrap_or_default();
497
498 let workspace_names: HashSet<&str> = workspaces.iter().map(|ws| ws.name.as_str()).collect();
500
501 let used_packages: HashSet<&str> = graph.package_usage.keys().map(|s| s.as_str()).collect();
503
504 let root_pkg_path = config.root.join("package.json");
505
506 let mut unused_deps: Vec<UnusedDependency> = pkg
508 .production_dependency_names()
509 .into_iter()
510 .filter(|dep| !used_packages.contains(dep.as_str()))
511 .filter(|dep| !script_used.contains(dep.as_str()))
512 .filter(|dep| !is_implicit_dependency(dep))
513 .filter(|dep| !plugin_referenced.contains(dep.as_str()))
514 .filter(|dep| !config.ignore_dependencies.iter().any(|d| d == dep))
515 .filter(|dep| !workspace_names.contains(dep.as_str()))
516 .map(|dep| UnusedDependency {
517 package_name: dep,
518 location: DependencyLocation::Dependencies,
519 path: root_pkg_path.clone(),
520 })
521 .collect();
522
523 let mut unused_dev_deps: Vec<UnusedDependency> = pkg
524 .dev_dependency_names()
525 .into_iter()
526 .filter(|dep| !used_packages.contains(dep.as_str()))
527 .filter(|dep| !script_used.contains(dep.as_str()))
528 .filter(|dep| !is_tooling_dependency(dep))
529 .filter(|dep| !plugin_tooling.contains(dep.as_str()))
530 .filter(|dep| !plugin_referenced.contains(dep.as_str()))
531 .filter(|dep| !config.ignore_dependencies.iter().any(|d| d == dep))
532 .filter(|dep| !workspace_names.contains(dep.as_str()))
533 .map(|dep| UnusedDependency {
534 package_name: dep,
535 location: DependencyLocation::DevDependencies,
536 path: root_pkg_path.clone(),
537 })
538 .collect();
539
540 let root_flagged: HashSet<String> = unused_deps
543 .iter()
544 .chain(unused_dev_deps.iter())
545 .map(|d| d.package_name.clone())
546 .collect();
547
548 for ws in workspaces {
549 let ws_pkg_path = ws.root.join("package.json");
550 let ws_pkg = match PackageJson::load(&ws_pkg_path) {
551 Ok(p) => p,
552 Err(_) => continue,
553 };
554
555 let canonical_ws = ws.root.canonicalize().unwrap_or_else(|_| ws.root.clone());
556
557 let is_used_in_workspace = |dep: &str| -> bool {
559 if let Some(file_ids) = graph.package_usage.get(dep) {
560 file_ids.iter().any(|id| {
561 if let Some(module) = graph.modules.get(id.0 as usize) {
562 let file_canonical = module
563 .path
564 .canonicalize()
565 .unwrap_or_else(|_| module.path.clone());
566 file_canonical.starts_with(&canonical_ws)
567 } else {
568 false
569 }
570 })
571 } else {
572 false
573 }
574 };
575
576 for dep in ws_pkg.production_dependency_names() {
578 if root_flagged.contains(&dep) {
579 continue; }
581 if is_implicit_dependency(&dep) {
582 continue;
583 }
584 if script_used.contains(dep.as_str()) {
585 continue;
586 }
587 if plugin_referenced.contains(dep.as_str()) {
588 continue;
589 }
590 if config.ignore_dependencies.iter().any(|d| d == &dep) {
591 continue;
592 }
593 if workspace_names.contains(dep.as_str()) {
594 continue;
595 }
596 if is_used_in_workspace(&dep) {
597 continue;
598 }
599 unused_deps.push(UnusedDependency {
600 package_name: dep,
601 location: DependencyLocation::Dependencies,
602 path: ws_pkg_path.clone(),
603 });
604 }
605
606 for dep in ws_pkg.dev_dependency_names() {
608 if root_flagged.contains(&dep) {
609 continue;
610 }
611 if is_tooling_dependency(&dep) {
612 continue;
613 }
614 if script_used.contains(dep.as_str()) {
615 continue;
616 }
617 if plugin_tooling.contains(dep.as_str()) {
618 continue;
619 }
620 if plugin_referenced.contains(dep.as_str()) {
621 continue;
622 }
623 if config.ignore_dependencies.iter().any(|d| d == &dep) {
624 continue;
625 }
626 if workspace_names.contains(dep.as_str()) {
627 continue;
628 }
629 if is_used_in_workspace(&dep) {
630 continue;
631 }
632 unused_dev_deps.push(UnusedDependency {
633 package_name: dep,
634 location: DependencyLocation::DevDependencies,
635 path: ws_pkg_path.clone(),
636 });
637 }
638 }
639
640 (unused_deps, unused_dev_deps)
641}
642
643fn find_unused_members(
648 graph: &ModuleGraph,
649 _config: &ResolvedConfig,
650 resolved_modules: &[ResolvedModule],
651 suppressions_by_file: &HashMap<FileId, &[Suppression]>,
652) -> (Vec<UnusedMember>, Vec<UnusedMember>) {
653 let mut unused_enum_members = Vec::new();
654 let mut unused_class_members = Vec::new();
655
656 let mut accessed_members: HashSet<(String, String)> = HashSet::new();
659
660 let mut self_accessed_members: HashMap<crate::discover::FileId, HashSet<String>> =
664 HashMap::new();
665
666 let mut whole_object_used_exports: HashSet<String> = HashSet::new();
669
670 for resolved in resolved_modules {
671 let local_to_imported: HashMap<&str, &str> = resolved
673 .resolved_imports
674 .iter()
675 .filter_map(|imp| match &imp.info.imported_name {
676 crate::extract::ImportedName::Named(name) => {
677 Some((imp.info.local_name.as_str(), name.as_str()))
678 }
679 crate::extract::ImportedName::Default => {
680 Some((imp.info.local_name.as_str(), "default"))
681 }
682 _ => None,
683 })
684 .collect();
685
686 for access in &resolved.member_accesses {
687 if access.object == "this" {
689 self_accessed_members
690 .entry(resolved.file_id)
691 .or_default()
692 .insert(access.member.clone());
693 continue;
694 }
695 let export_name = local_to_imported
697 .get(access.object.as_str())
698 .copied()
699 .unwrap_or(access.object.as_str());
700 accessed_members.insert((export_name.to_string(), access.member.clone()));
701 }
702
703 for local_name in &resolved.whole_object_uses {
705 let export_name = local_to_imported
706 .get(local_name.as_str())
707 .copied()
708 .unwrap_or(local_name.as_str());
709 whole_object_used_exports.insert(export_name.to_string());
710 }
711 }
712
713 for module in &graph.modules {
714 if !module.is_reachable || module.is_entry_point {
715 continue;
716 }
717
718 let mut source_content: Option<String> = None;
720
721 for export in &module.exports {
722 if export.members.is_empty() {
723 continue;
724 }
725
726 if export.references.is_empty() && !graph.has_namespace_import(module.file_id) {
728 continue;
729 }
730
731 let export_name = export.name.to_string();
732
733 if whole_object_used_exports.contains(&export_name) {
736 continue;
737 }
738
739 let file_self_accesses = self_accessed_members.get(&module.file_id);
741
742 for member in &export.members {
743 if accessed_members.contains(&(export_name.clone(), member.name.clone())) {
745 continue;
746 }
747
748 if matches!(
751 member.kind,
752 MemberKind::ClassMethod | MemberKind::ClassProperty
753 ) && file_self_accesses.is_some_and(|accesses| accesses.contains(&member.name))
754 {
755 continue;
756 }
757
758 if matches!(
762 member.kind,
763 MemberKind::ClassMethod | MemberKind::ClassProperty
764 ) && (is_react_lifecycle_method(&member.name)
765 || is_angular_lifecycle_method(&member.name))
766 {
767 continue;
768 }
769
770 let source = source_content.get_or_insert_with(|| read_source(&module.path));
771 let (line, col) = byte_offset_to_line_col(source, member.span.start);
772
773 let issue_kind = match member.kind {
775 MemberKind::EnumMember => IssueKind::UnusedEnumMember,
776 MemberKind::ClassMethod | MemberKind::ClassProperty => {
777 IssueKind::UnusedClassMember
778 }
779 };
780 if let Some(supps) = suppressions_by_file.get(&module.file_id)
781 && suppress::is_suppressed(supps, line, issue_kind)
782 {
783 continue;
784 }
785
786 let unused = UnusedMember {
787 path: module.path.clone(),
788 parent_name: export_name.clone(),
789 member_name: member.name.clone(),
790 kind: member.kind.clone(),
791 line,
792 col,
793 };
794
795 match member.kind {
796 MemberKind::EnumMember => unused_enum_members.push(unused),
797 MemberKind::ClassMethod | MemberKind::ClassProperty => {
798 unused_class_members.push(unused);
799 }
800 }
801 }
802 }
803 }
804
805 (unused_enum_members, unused_class_members)
806}
807
808fn find_type_only_dependencies(
814 graph: &ModuleGraph,
815 pkg: &PackageJson,
816 config: &ResolvedConfig,
817 workspaces: &[fallow_config::WorkspaceInfo],
818) -> Vec<TypeOnlyDependency> {
819 let root_pkg_path = config.root.join("package.json");
820 let workspace_names: HashSet<&str> = workspaces.iter().map(|ws| ws.name.as_str()).collect();
821
822 let mut type_only_deps = Vec::new();
823
824 for dep in pkg.production_dependency_names() {
826 if workspace_names.contains(dep.as_str()) {
828 continue;
829 }
830 if config.ignore_dependencies.iter().any(|d| d == &dep) {
832 continue;
833 }
834
835 let has_any_usage = graph.package_usage.contains_key(dep.as_str());
836 let has_type_only_usage = graph.type_only_package_usage.contains_key(dep.as_str());
837
838 if !has_any_usage {
839 continue;
841 }
842
843 let total_count = graph
846 .package_usage
847 .get(dep.as_str())
848 .map(|v| v.len())
849 .unwrap_or(0);
850 let type_only_count = graph
851 .type_only_package_usage
852 .get(dep.as_str())
853 .map(|v| v.len())
854 .unwrap_or(0);
855
856 if has_type_only_usage && type_only_count == total_count {
857 type_only_deps.push(TypeOnlyDependency {
858 package_name: dep,
859 path: root_pkg_path.clone(),
860 });
861 }
862 }
863
864 type_only_deps
865}
866
867fn find_unlisted_dependencies(
869 graph: &ModuleGraph,
870 pkg: &PackageJson,
871 config: &ResolvedConfig,
872 workspaces: &[fallow_config::WorkspaceInfo],
873) -> Vec<UnlistedDependency> {
874 let all_deps: HashSet<String> = pkg.all_dependency_names().into_iter().collect();
875
876 let mut all_workspace_deps: HashSet<String> = all_deps.clone();
879 let mut workspace_names: HashSet<String> = HashSet::new();
881 let mut ws_dep_map: Vec<(std::path::PathBuf, HashSet<String>)> = Vec::new();
883
884 for ws in workspaces {
885 workspace_names.insert(ws.name.clone());
886 let ws_pkg_path = ws.root.join("package.json");
887 if let Ok(ws_pkg) = PackageJson::load(&ws_pkg_path) {
888 let ws_deps: HashSet<String> = ws_pkg.all_dependency_names().into_iter().collect();
889 all_workspace_deps.extend(ws_deps.iter().cloned());
890 let canonical_ws = ws.root.canonicalize().unwrap_or_else(|_| ws.root.clone());
891 ws_dep_map.push((canonical_ws, ws_deps));
892 }
893 }
894
895 let mut unlisted: HashMap<String, Vec<std::path::PathBuf>> = HashMap::new();
896
897 for (package_name, file_ids) in &graph.package_usage {
898 if is_builtin_module(package_name) || is_path_alias(package_name) {
899 continue;
900 }
901 if workspace_names.contains(package_name) {
903 continue;
904 }
905 if all_workspace_deps.contains(package_name) {
907 continue;
908 }
909
910 let mut unlisted_paths: Vec<std::path::PathBuf> = Vec::new();
912 for id in file_ids {
913 if let Some(module) = graph.modules.get(id.0 as usize) {
914 let file_canonical = module.path.canonicalize().unwrap_or(module.path.clone());
915 let listed_in_ws = ws_dep_map.iter().any(|(ws_root, ws_deps)| {
916 file_canonical.starts_with(ws_root) && ws_deps.contains(package_name)
917 });
918 let listed_in_root = all_deps.contains(package_name);
920 if !listed_in_ws && !listed_in_root {
921 unlisted_paths.push(module.path.clone());
922 }
923 }
924 }
925
926 if !unlisted_paths.is_empty() {
927 unlisted_paths.sort();
928 unlisted_paths.dedup();
929 unlisted.insert(package_name.clone(), unlisted_paths);
930 }
931 }
932
933 let _ = config; unlisted
935 .into_iter()
936 .map(|(name, paths)| UnlistedDependency {
937 package_name: name,
938 imported_from: paths,
939 })
940 .collect()
941}
942
943fn is_path_alias(name: &str) -> bool {
949 if name.starts_with('#') {
951 return true;
952 }
953 if name.starts_with("~/") {
955 return true;
956 }
957 if name.starts_with("@/") {
959 return true;
960 }
961 if name.starts_with('@') {
965 let scope = name.split('/').next().unwrap_or(name);
966 if scope.len() > 1 && scope.chars().nth(1).is_some_and(|c| c.is_ascii_uppercase()) {
967 return true;
968 }
969 }
970
971 false
972}
973
974fn find_unresolved_imports(
976 resolved_modules: &[ResolvedModule],
977 _config: &ResolvedConfig,
978 suppressions_by_file: &HashMap<FileId, &[Suppression]>,
979) -> Vec<UnresolvedImport> {
980 let mut unresolved = Vec::new();
981
982 for module in resolved_modules {
983 let mut source_content: Option<String> = None;
985
986 for import in &module.resolved_imports {
987 if let crate::resolve::ResolveResult::Unresolvable(spec) = &import.target {
988 let source = source_content.get_or_insert_with(|| read_source(&module.path));
989 let (line, col) = byte_offset_to_line_col(source, import.info.span.start);
990
991 if let Some(supps) = suppressions_by_file.get(&module.file_id)
993 && suppress::is_suppressed(supps, line, IssueKind::UnresolvedImport)
994 {
995 continue;
996 }
997
998 unresolved.push(UnresolvedImport {
999 path: module.path.clone(),
1000 specifier: spec.clone(),
1001 line,
1002 col,
1003 });
1004 }
1005 }
1006 }
1007
1008 unresolved
1009}
1010
1011fn find_duplicate_exports(
1017 graph: &ModuleGraph,
1018 config: &ResolvedConfig,
1019 suppressions_by_file: &HashMap<FileId, &[Suppression]>,
1020) -> Vec<DuplicateExport> {
1021 let mut re_export_sources: HashMap<usize, HashSet<usize>> = HashMap::new();
1023 for (idx, module) in graph.modules.iter().enumerate() {
1024 for re in &module.re_exports {
1025 re_export_sources
1026 .entry(idx)
1027 .or_default()
1028 .insert(re.source_file.0 as usize);
1029 }
1030 }
1031
1032 let mut export_locations: HashMap<String, Vec<(usize, std::path::PathBuf)>> = HashMap::new();
1033
1034 for (idx, module) in graph.modules.iter().enumerate() {
1035 if !module.is_reachable || module.is_entry_point {
1036 continue;
1037 }
1038
1039 if suppressions_by_file
1041 .get(&module.file_id)
1042 .is_some_and(|supps| suppress::is_file_suppressed(supps, IssueKind::DuplicateExport))
1043 {
1044 continue;
1045 }
1046
1047 for export in &module.exports {
1048 if matches!(export.name, crate::extract::ExportName::Default) {
1049 continue; }
1051 if export.span.start == 0 && export.span.end == 0 {
1054 continue;
1055 }
1056 let name = export.name.to_string();
1057 export_locations
1058 .entry(name)
1059 .or_default()
1060 .push((idx, module.path.clone()));
1061 }
1062 }
1063
1064 let _ = config; export_locations
1067 .into_iter()
1068 .filter_map(|(name, locations)| {
1069 if locations.len() <= 1 {
1070 return None;
1071 }
1072 let module_indices: HashSet<usize> = locations.iter().map(|(idx, _)| *idx).collect();
1076 let independent: Vec<std::path::PathBuf> = locations
1077 .into_iter()
1078 .filter(|(idx, _)| {
1079 let sources = re_export_sources.get(idx);
1082 let has_source_in_set = sources
1083 .map(|s| s.iter().any(|src| module_indices.contains(src)))
1084 .unwrap_or(false);
1085 !has_source_in_set
1086 })
1087 .map(|(_, path)| path)
1088 .collect();
1089
1090 if independent.len() > 1 {
1091 Some(DuplicateExport {
1092 export_name: name,
1093 locations: independent,
1094 })
1095 } else {
1096 None
1097 }
1098 })
1099 .collect()
1100}
1101
1102fn is_builtin_module(name: &str) -> bool {
1104 let builtins = [
1105 "assert",
1106 "assert/strict",
1107 "async_hooks",
1108 "buffer",
1109 "child_process",
1110 "cluster",
1111 "console",
1112 "constants",
1113 "crypto",
1114 "dgram",
1115 "diagnostics_channel",
1116 "dns",
1117 "dns/promises",
1118 "domain",
1119 "events",
1120 "fs",
1121 "fs/promises",
1122 "http",
1123 "http2",
1124 "https",
1125 "inspector",
1126 "inspector/promises",
1127 "module",
1128 "net",
1129 "os",
1130 "path",
1131 "path/posix",
1132 "path/win32",
1133 "perf_hooks",
1134 "process",
1135 "punycode",
1136 "querystring",
1137 "readline",
1138 "readline/promises",
1139 "repl",
1140 "stream",
1141 "stream/consumers",
1142 "stream/promises",
1143 "stream/web",
1144 "string_decoder",
1145 "sys",
1146 "test",
1147 "test/reporters",
1148 "timers",
1149 "timers/promises",
1150 "tls",
1151 "trace_events",
1152 "tty",
1153 "url",
1154 "util",
1155 "util/types",
1156 "v8",
1157 "vm",
1158 "wasi",
1159 "worker_threads",
1160 "zlib",
1161 ];
1162 let stripped = name.strip_prefix("node:").unwrap_or(name);
1163 builtins.contains(&stripped) || {
1166 stripped
1168 .split('/')
1169 .next()
1170 .is_some_and(|root| builtins.contains(&root))
1171 }
1172}
1173
1174fn is_implicit_dependency(name: &str) -> bool {
1176 if name.starts_with("@types/") {
1177 return true;
1178 }
1179
1180 let implicit_deps = [
1183 "react-dom",
1184 "react-dom/client",
1185 "react-native",
1186 "@next/font",
1187 "@next/mdx",
1188 "@next/bundle-analyzer",
1189 "@next/env",
1190 "utf-8-validate",
1192 "bufferutil",
1193 ];
1194 implicit_deps.contains(&name)
1195}
1196
1197fn is_tooling_dependency(name: &str) -> bool {
1199 let tooling_prefixes = [
1200 "@types/",
1201 "eslint",
1202 "@typescript-eslint",
1203 "husky",
1204 "lint-staged",
1205 "commitlint",
1206 "@commitlint",
1207 "stylelint",
1208 "postcss",
1209 "autoprefixer",
1210 "tailwindcss",
1211 "@tailwindcss",
1212 "@vitest/",
1213 "@jest/",
1214 "@testing-library/",
1215 "@playwright/",
1216 "@storybook/",
1217 "storybook",
1218 "@babel/",
1219 "babel-",
1220 "@react-native-community/cli",
1221 "@react-native/",
1222 "secretlint",
1223 "@secretlint/",
1224 "oxlint",
1225 "@semantic-release/",
1227 "semantic-release",
1228 "@release-it/",
1229 "@lerna-lite/",
1230 "@changesets/",
1231 "@graphql-codegen/",
1233 "@rollup/",
1234 "@biomejs/",
1236 ];
1237
1238 let exact_matches = [
1239 "typescript",
1240 "prettier",
1241 "turbo",
1242 "concurrently",
1243 "cross-env",
1244 "rimraf",
1245 "npm-run-all",
1246 "npm-run-all2",
1247 "nodemon",
1248 "ts-node",
1249 "tsx",
1250 "knip",
1251 "fallow",
1252 "jest",
1253 "vitest",
1254 "happy-dom",
1255 "jsdom",
1256 "vite",
1257 "sass",
1258 "sass-embedded",
1259 "webpack",
1260 "webpack-cli",
1261 "webpack-dev-server",
1262 "esbuild",
1263 "rollup",
1264 "swc",
1265 "@swc/core",
1266 "@swc/jest",
1267 "terser",
1268 "cssnano",
1269 "sharp",
1270 "release-it",
1272 "lerna",
1273 "dotenv-cli",
1275 "dotenv-flow",
1276 "oxfmt",
1278 "jscpd",
1279 "npm-check-updates",
1280 "markdownlint-cli",
1281 "npm-package-json-lint",
1282 "synp",
1283 "flow-bin",
1284 "i18next-parser",
1286 "i18next-conv",
1287 "webpack-bundle-analyzer",
1289 "vite-plugin-svgr",
1291 "vite-plugin-eslint",
1292 "@vitejs/plugin-vue",
1293 "@vitejs/plugin-react",
1294 "next-sitemap",
1296 "tsup",
1298 "unbuild",
1299 "typedoc",
1300 "nx",
1302 "@manypkg/cli",
1303 "vue-tsc",
1305 "@vue/tsconfig",
1306 "@tsconfig/node20",
1307 "@tsconfig/react-native",
1308 "@typescript/native-preview",
1310 "tw-animate-css",
1312 "@ianvs/prettier-plugin-sort-imports",
1314 "prettier-plugin-tailwindcss",
1315 "prettier-plugin-organize-imports",
1316 "@vitejs/plugin-react-swc",
1318 "@vitejs/plugin-legacy",
1319 ];
1320
1321 tooling_prefixes.iter().any(|p| name.starts_with(p)) || exact_matches.contains(&name)
1322}
1323
1324fn is_angular_lifecycle_method(name: &str) -> bool {
1329 matches!(
1330 name,
1331 "ngOnInit"
1332 | "ngOnDestroy"
1333 | "ngOnChanges"
1334 | "ngDoCheck"
1335 | "ngAfterContentInit"
1336 | "ngAfterContentChecked"
1337 | "ngAfterViewInit"
1338 | "ngAfterViewChecked"
1339 | "ngAcceptInputType"
1340 | "canActivate"
1342 | "canDeactivate"
1343 | "canActivateChild"
1344 | "canMatch"
1345 | "resolve"
1346 | "intercept"
1347 | "transform"
1348 | "validate"
1350 | "registerOnChange"
1351 | "registerOnTouched"
1352 | "writeValue"
1353 | "setDisabledState"
1354 )
1355}
1356
1357fn is_react_lifecycle_method(name: &str) -> bool {
1358 matches!(
1359 name,
1360 "render"
1361 | "componentDidMount"
1362 | "componentDidUpdate"
1363 | "componentWillUnmount"
1364 | "shouldComponentUpdate"
1365 | "getSnapshotBeforeUpdate"
1366 | "getDerivedStateFromProps"
1367 | "getDerivedStateFromError"
1368 | "componentDidCatch"
1369 | "componentWillMount"
1370 | "componentWillReceiveProps"
1371 | "componentWillUpdate"
1372 | "UNSAFE_componentWillMount"
1373 | "UNSAFE_componentWillReceiveProps"
1374 | "UNSAFE_componentWillUpdate"
1375 | "getChildContext"
1376 | "contextType"
1377 )
1378}
1379
1380#[cfg(test)]
1381mod tests {
1382 use super::*;
1383
1384 #[test]
1386 fn builtin_module_fs() {
1387 assert!(is_builtin_module("fs"));
1388 }
1389
1390 #[test]
1391 fn builtin_module_path() {
1392 assert!(is_builtin_module("path"));
1393 }
1394
1395 #[test]
1396 fn builtin_module_with_node_prefix() {
1397 assert!(is_builtin_module("node:fs"));
1398 assert!(is_builtin_module("node:path"));
1399 assert!(is_builtin_module("node:crypto"));
1400 }
1401
1402 #[test]
1403 fn builtin_module_all_known() {
1404 let known = [
1405 "assert",
1406 "buffer",
1407 "child_process",
1408 "cluster",
1409 "console",
1410 "constants",
1411 "crypto",
1412 "dgram",
1413 "dns",
1414 "domain",
1415 "events",
1416 "fs",
1417 "http",
1418 "http2",
1419 "https",
1420 "module",
1421 "net",
1422 "os",
1423 "path",
1424 "perf_hooks",
1425 "process",
1426 "punycode",
1427 "querystring",
1428 "readline",
1429 "repl",
1430 "stream",
1431 "string_decoder",
1432 "sys",
1433 "timers",
1434 "tls",
1435 "tty",
1436 "url",
1437 "util",
1438 "v8",
1439 "vm",
1440 "wasi",
1441 "worker_threads",
1442 "zlib",
1443 ];
1444 for name in &known {
1445 assert!(is_builtin_module(name), "{name} should be a builtin module");
1446 }
1447 }
1448
1449 #[test]
1450 fn not_builtin_module() {
1451 assert!(!is_builtin_module("react"));
1452 assert!(!is_builtin_module("lodash"));
1453 assert!(!is_builtin_module("express"));
1454 assert!(!is_builtin_module("@scope/pkg"));
1455 }
1456
1457 #[test]
1458 fn not_builtin_similar_names() {
1459 assert!(!is_builtin_module("filesystem"));
1460 assert!(!is_builtin_module("pathlib"));
1461 assert!(!is_builtin_module("node:react"));
1462 }
1463
1464 #[test]
1466 fn implicit_dep_types_packages() {
1467 assert!(is_implicit_dependency("@types/node"));
1468 assert!(is_implicit_dependency("@types/react"));
1469 assert!(is_implicit_dependency("@types/jest"));
1470 }
1471
1472 #[test]
1473 fn not_implicit_dep() {
1474 assert!(!is_implicit_dependency("react"));
1475 assert!(!is_implicit_dependency("@scope/types"));
1476 assert!(!is_implicit_dependency("types"));
1477 assert!(!is_implicit_dependency("typescript"));
1478 assert!(!is_implicit_dependency("prettier"));
1479 assert!(!is_implicit_dependency("eslint"));
1480 }
1481
1482 #[test]
1484 fn tooling_dep_prefixes() {
1485 assert!(is_tooling_dependency("@types/node"));
1486 assert!(is_tooling_dependency("eslint"));
1487 assert!(is_tooling_dependency("eslint-plugin-react"));
1488 assert!(is_tooling_dependency("prettier"));
1489 assert!(is_tooling_dependency("@typescript-eslint/parser"));
1490 assert!(is_tooling_dependency("husky"));
1491 assert!(is_tooling_dependency("lint-staged"));
1492 assert!(is_tooling_dependency("commitlint"));
1493 assert!(is_tooling_dependency("@commitlint/config-conventional"));
1494 assert!(is_tooling_dependency("stylelint"));
1495 assert!(is_tooling_dependency("postcss"));
1496 assert!(is_tooling_dependency("autoprefixer"));
1497 assert!(is_tooling_dependency("tailwindcss"));
1498 assert!(is_tooling_dependency("@tailwindcss/forms"));
1499 }
1500
1501 #[test]
1502 fn tooling_dep_exact_matches() {
1503 assert!(is_tooling_dependency("typescript"));
1504 assert!(is_tooling_dependency("prettier"));
1505 assert!(is_tooling_dependency("turbo"));
1506 assert!(is_tooling_dependency("concurrently"));
1507 assert!(is_tooling_dependency("cross-env"));
1508 assert!(is_tooling_dependency("rimraf"));
1509 assert!(is_tooling_dependency("npm-run-all"));
1510 assert!(is_tooling_dependency("nodemon"));
1511 assert!(is_tooling_dependency("ts-node"));
1512 assert!(is_tooling_dependency("tsx"));
1513 }
1514
1515 #[test]
1516 fn not_tooling_dep() {
1517 assert!(!is_tooling_dependency("react"));
1518 assert!(!is_tooling_dependency("next"));
1519 assert!(!is_tooling_dependency("lodash"));
1520 assert!(!is_tooling_dependency("express"));
1521 assert!(!is_tooling_dependency("@emotion/react"));
1522 }
1523
1524 #[test]
1526 fn tooling_dep_testing_frameworks() {
1527 assert!(is_tooling_dependency("jest"));
1528 assert!(is_tooling_dependency("vitest"));
1529 assert!(is_tooling_dependency("@jest/globals"));
1530 assert!(is_tooling_dependency("@vitest/coverage-v8"));
1531 assert!(is_tooling_dependency("@testing-library/react"));
1532 assert!(is_tooling_dependency("@testing-library/jest-dom"));
1533 assert!(is_tooling_dependency("@playwright/test"));
1534 }
1535
1536 #[test]
1537 fn tooling_dep_environments_and_cli() {
1538 assert!(is_tooling_dependency("happy-dom"));
1539 assert!(is_tooling_dependency("jsdom"));
1540 assert!(is_tooling_dependency("knip"));
1541 }
1542
1543 #[test]
1545 fn react_lifecycle_standard_methods() {
1546 assert!(is_react_lifecycle_method("render"));
1547 assert!(is_react_lifecycle_method("componentDidMount"));
1548 assert!(is_react_lifecycle_method("componentDidUpdate"));
1549 assert!(is_react_lifecycle_method("componentWillUnmount"));
1550 assert!(is_react_lifecycle_method("shouldComponentUpdate"));
1551 assert!(is_react_lifecycle_method("getSnapshotBeforeUpdate"));
1552 }
1553
1554 #[test]
1555 fn react_lifecycle_static_methods() {
1556 assert!(is_react_lifecycle_method("getDerivedStateFromProps"));
1557 assert!(is_react_lifecycle_method("getDerivedStateFromError"));
1558 }
1559
1560 #[test]
1561 fn react_lifecycle_error_boundary() {
1562 assert!(is_react_lifecycle_method("componentDidCatch"));
1563 }
1564
1565 #[test]
1566 fn react_lifecycle_deprecated_and_unsafe() {
1567 assert!(is_react_lifecycle_method("componentWillMount"));
1568 assert!(is_react_lifecycle_method("componentWillReceiveProps"));
1569 assert!(is_react_lifecycle_method("componentWillUpdate"));
1570 assert!(is_react_lifecycle_method("UNSAFE_componentWillMount"));
1571 assert!(is_react_lifecycle_method(
1572 "UNSAFE_componentWillReceiveProps"
1573 ));
1574 assert!(is_react_lifecycle_method("UNSAFE_componentWillUpdate"));
1575 }
1576
1577 #[test]
1578 fn react_lifecycle_context_methods() {
1579 assert!(is_react_lifecycle_method("getChildContext"));
1580 assert!(is_react_lifecycle_method("contextType"));
1581 }
1582
1583 #[test]
1584 fn not_react_lifecycle_method() {
1585 assert!(!is_react_lifecycle_method("handleClick"));
1586 assert!(!is_react_lifecycle_method("fetchData"));
1587 assert!(!is_react_lifecycle_method("constructor"));
1588 assert!(!is_react_lifecycle_method("setState"));
1589 assert!(!is_react_lifecycle_method("forceUpdate"));
1590 assert!(!is_react_lifecycle_method("customMethod"));
1591 }
1592
1593 #[test]
1595 fn declaration_file_dts() {
1596 assert!(is_declaration_file(std::path::Path::new("styled.d.ts")));
1597 assert!(is_declaration_file(std::path::Path::new(
1598 "src/types/styled.d.ts"
1599 )));
1600 assert!(is_declaration_file(std::path::Path::new("env.d.ts")));
1601 }
1602
1603 #[test]
1604 fn declaration_file_dmts_dcts() {
1605 assert!(is_declaration_file(std::path::Path::new("module.d.mts")));
1606 assert!(is_declaration_file(std::path::Path::new("module.d.cts")));
1607 }
1608
1609 #[test]
1610 fn not_declaration_file() {
1611 assert!(!is_declaration_file(std::path::Path::new("index.ts")));
1612 assert!(!is_declaration_file(std::path::Path::new("component.tsx")));
1613 assert!(!is_declaration_file(std::path::Path::new("utils.js")));
1614 assert!(!is_declaration_file(std::path::Path::new("styles.d.css")));
1615 }
1616
1617 #[test]
1619 fn byte_offset_empty_source() {
1620 assert_eq!(byte_offset_to_line_col("", 0), (1, 0));
1621 }
1622
1623 #[test]
1624 fn byte_offset_single_line_start() {
1625 assert_eq!(byte_offset_to_line_col("hello", 0), (1, 0));
1626 }
1627
1628 #[test]
1629 fn byte_offset_single_line_middle() {
1630 assert_eq!(byte_offset_to_line_col("hello", 4), (1, 4));
1631 }
1632
1633 #[test]
1634 fn byte_offset_multiline_start_of_line2() {
1635 let source = "line1\nline2\nline3";
1639 assert_eq!(byte_offset_to_line_col(source, 6), (2, 0));
1640 }
1641
1642 #[test]
1643 fn byte_offset_multiline_middle_of_line3() {
1644 let source = "line1\nline2\nline3";
1649 assert_eq!(byte_offset_to_line_col(source, 14), (3, 2));
1650 }
1651
1652 #[test]
1653 fn byte_offset_at_newline_boundary() {
1654 let source = "line1\nline2";
1657 assert_eq!(byte_offset_to_line_col(source, 5), (1, 5));
1658 }
1659
1660 #[test]
1661 fn byte_offset_beyond_source_length() {
1662 let source = "hello";
1666 assert_eq!(byte_offset_to_line_col(source, 100), (1, 100));
1667 }
1668
1669 #[test]
1670 fn byte_offset_multibyte_utf8() {
1671 let source = "hi\n\u{1F600}x";
1673 assert_eq!(byte_offset_to_line_col(source, 3), (2, 0));
1675 assert_eq!(byte_offset_to_line_col(source, 7), (2, 4));
1677 }
1678
1679 #[test]
1680 fn byte_offset_multibyte_accented_chars() {
1681 let source = "caf\u{00E9}\nbar";
1683 assert_eq!(byte_offset_to_line_col(source, 6), (2, 0));
1686 assert_eq!(byte_offset_to_line_col(source, 3), (1, 3));
1688 }
1689
1690 #[test]
1692 fn path_alias_at_slash() {
1693 assert!(is_path_alias("@/components"));
1694 }
1695
1696 #[test]
1697 fn path_alias_tilde() {
1698 assert!(is_path_alias("~/lib"));
1699 }
1700
1701 #[test]
1702 fn path_alias_hash_imports_map() {
1703 assert!(is_path_alias("#internal/module"));
1704 }
1705
1706 #[test]
1707 fn path_alias_pascal_case_scope() {
1708 assert!(is_path_alias("@Components/Button"));
1709 }
1710
1711 #[test]
1712 fn not_path_alias_regular_package() {
1713 assert!(!is_path_alias("react"));
1714 }
1715
1716 #[test]
1717 fn not_path_alias_scoped_npm_package() {
1718 assert!(!is_path_alias("@scope/pkg"));
1719 }
1720
1721 #[test]
1722 fn not_path_alias_emotion_react() {
1723 assert!(!is_path_alias("@emotion/react"));
1724 }
1725
1726 #[test]
1727 fn not_path_alias_lodash() {
1728 assert!(!is_path_alias("lodash"));
1729 }
1730
1731 #[test]
1732 fn not_path_alias_lowercase_short_scope() {
1733 assert!(!is_path_alias("@s/lowercase"));
1734 }
1735
1736 #[test]
1738 fn angular_lifecycle_core_hooks() {
1739 assert!(is_angular_lifecycle_method("ngOnInit"));
1740 assert!(is_angular_lifecycle_method("ngOnDestroy"));
1741 assert!(is_angular_lifecycle_method("ngOnChanges"));
1742 assert!(is_angular_lifecycle_method("ngAfterViewInit"));
1743 }
1744
1745 #[test]
1746 fn angular_lifecycle_check_hooks() {
1747 assert!(is_angular_lifecycle_method("ngDoCheck"));
1748 assert!(is_angular_lifecycle_method("ngAfterContentChecked"));
1749 assert!(is_angular_lifecycle_method("ngAfterViewChecked"));
1750 }
1751
1752 #[test]
1753 fn angular_lifecycle_content_hooks() {
1754 assert!(is_angular_lifecycle_method("ngAfterContentInit"));
1755 assert!(is_angular_lifecycle_method("ngAcceptInputType"));
1756 }
1757
1758 #[test]
1759 fn angular_lifecycle_guard_resolver_methods() {
1760 assert!(is_angular_lifecycle_method("canActivate"));
1761 assert!(is_angular_lifecycle_method("canDeactivate"));
1762 assert!(is_angular_lifecycle_method("canActivateChild"));
1763 assert!(is_angular_lifecycle_method("canMatch"));
1764 assert!(is_angular_lifecycle_method("resolve"));
1765 assert!(is_angular_lifecycle_method("intercept"));
1766 assert!(is_angular_lifecycle_method("transform"));
1767 }
1768
1769 #[test]
1770 fn angular_lifecycle_form_methods() {
1771 assert!(is_angular_lifecycle_method("validate"));
1772 assert!(is_angular_lifecycle_method("registerOnChange"));
1773 assert!(is_angular_lifecycle_method("registerOnTouched"));
1774 assert!(is_angular_lifecycle_method("writeValue"));
1775 assert!(is_angular_lifecycle_method("setDisabledState"));
1776 }
1777
1778 #[test]
1779 fn not_angular_lifecycle_method() {
1780 assert!(!is_angular_lifecycle_method("onClick"));
1781 assert!(!is_angular_lifecycle_method("handleSubmit"));
1782 assert!(!is_angular_lifecycle_method("render"));
1783 }
1784}