1use std::collections::{HashMap, HashSet};
2
3use fallow_config::{PackageJson, ResolvedConfig};
4
5use crate::extract::MemberKind;
6use crate::graph::ModuleGraph;
7use crate::resolve::ResolvedModule;
8use crate::results::*;
9
10fn byte_offset_to_line_col(source: &str, byte_offset: u32) -> (u32, u32) {
13 let byte_offset = byte_offset as usize;
14 let prefix = &source[..byte_offset.min(source.len())];
15 let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32 + 1;
16 let col = prefix
17 .rfind('\n')
18 .map(|pos| byte_offset - pos - 1)
19 .unwrap_or(byte_offset) as u32;
20 (line, col)
21}
22
23fn read_source(path: &std::path::Path) -> String {
25 std::fs::read_to_string(path).unwrap_or_default()
26}
27
28pub fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
30 find_dead_code_with_resolved(graph, config, &[], None)
31}
32
33pub fn find_dead_code_with_resolved(
35 graph: &ModuleGraph,
36 config: &ResolvedConfig,
37 resolved_modules: &[ResolvedModule],
38 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
39) -> AnalysisResults {
40 find_dead_code_full(graph, config, resolved_modules, plugin_result, &[])
41}
42
43pub fn find_dead_code_full(
45 graph: &ModuleGraph,
46 config: &ResolvedConfig,
47 resolved_modules: &[ResolvedModule],
48 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
49 workspaces: &[fallow_config::WorkspaceInfo],
50) -> AnalysisResults {
51 let _span = tracing::info_span!("find_dead_code").entered();
52
53 let mut results = AnalysisResults::default();
54
55 if config.detect.unused_files {
56 results.unused_files = find_unused_files(graph);
57 }
58
59 if config.detect.unused_exports || config.detect.unused_types {
60 let (exports, types) = find_unused_exports(graph, config, plugin_result);
61 if config.detect.unused_exports {
62 results.unused_exports = exports;
63 }
64 if config.detect.unused_types {
65 results.unused_types = types;
66 }
67 }
68
69 if config.detect.unused_enum_members || config.detect.unused_class_members {
70 let (enum_members, class_members) = find_unused_members(graph, config, resolved_modules);
71 if config.detect.unused_enum_members {
72 results.unused_enum_members = enum_members;
73 }
74 if config.detect.unused_class_members {
75 results.unused_class_members = class_members;
76 }
77 }
78
79 let pkg_path = config.root.join("package.json");
81 if let Ok(pkg) = PackageJson::load(&pkg_path) {
82 if config.detect.unused_dependencies || config.detect.unused_dev_dependencies {
83 let (deps, dev_deps) =
84 find_unused_dependencies(graph, &pkg, config, plugin_result, workspaces);
85 if config.detect.unused_dependencies {
86 results.unused_dependencies = deps;
87 }
88 if config.detect.unused_dev_dependencies {
89 results.unused_dev_dependencies = dev_deps;
90 }
91 }
92
93 if config.detect.unlisted_dependencies {
94 results.unlisted_dependencies =
95 find_unlisted_dependencies(graph, &pkg, config, workspaces);
96 }
97 }
98
99 if config.detect.unresolved_imports && !resolved_modules.is_empty() {
100 results.unresolved_imports = find_unresolved_imports(resolved_modules, config);
101 }
102
103 if config.detect.duplicate_exports {
104 results.duplicate_exports = find_duplicate_exports(graph, config);
105 }
106
107 results
108}
109
110fn find_unused_files(graph: &ModuleGraph) -> Vec<UnusedFile> {
123 graph
124 .modules
125 .iter()
126 .filter(|m| !m.is_reachable && !m.is_entry_point)
127 .filter(|m| !is_declaration_file(&m.path))
128 .filter(|m| !is_config_file(&m.path))
129 .filter(|m| !is_barrel_with_reachable_sources(m, graph))
130 .map(|m| UnusedFile {
131 path: m.path.clone(),
132 })
133 .collect()
134}
135
136fn is_declaration_file(path: &std::path::Path) -> bool {
138 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
139 name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
140}
141
142fn is_barrel_with_reachable_sources(
149 module: &crate::graph::ModuleNode,
150 graph: &ModuleGraph,
151) -> bool {
152 if module.re_exports.is_empty() {
154 return false;
155 }
156
157 let has_local_exports = module
160 .exports
161 .iter()
162 .any(|e| e.span.start != 0 || e.span.end != 0);
163 if has_local_exports || module.has_cjs_exports {
164 return false;
165 }
166
167 module.re_exports.iter().any(|re| {
169 let source_idx = re.source_file.0 as usize;
170 graph
171 .modules
172 .get(source_idx)
173 .is_some_and(|m| m.is_reachable)
174 })
175}
176
177fn is_config_file(path: &std::path::Path) -> bool {
183 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
184
185 if name.starts_with('.') && !name.starts_with("..") {
188 let lower = name.to_ascii_lowercase();
189 if lower.contains("rc.") {
191 return true;
192 }
193 }
194
195 let config_patterns = [
198 "babel.config.",
200 "rollup.config.",
201 "webpack.config.",
202 "postcss.config.",
203 "stencil.config.",
204 "remotion.config.",
205 "metro.config.",
206 "tsup.config.",
207 "unbuild.config.",
208 "esbuild.config.",
209 "swc.config.",
210 "turbo.",
211 "jest.config.",
213 "jest.setup.",
214 "vitest.config.",
215 "vitest.ci.config.",
216 "vitest.setup.",
217 "vitest.workspace.",
218 "playwright.config.",
219 "cypress.config.",
220 "eslint.config.",
222 "prettier.config.",
223 "stylelint.config.",
224 "lint-staged.config.",
225 "commitlint.config.",
226 "next.config.",
228 "next-sitemap.config.",
229 "nuxt.config.",
230 "astro.config.",
231 "sanity.config.",
232 "vite.config.",
233 "tailwind.config.",
234 "drizzle.config.",
235 "knexfile.",
236 "sentry.client.config.",
237 "sentry.server.config.",
238 "sentry.edge.config.",
239 "react-router.config.",
240 "typedoc.",
242 "knip.config.",
244 "fallow.config.",
245 "i18next-parser.config.",
246 "codegen.config.",
247 "graphql.config.",
248 "npmpackagejsonlint.config.",
249 "release-it.",
250 "release.config.",
251 "contentlayer.config.",
252 "next-env.d.",
254 "env.d.",
255 "vite-env.d.",
256 ];
257
258 config_patterns.iter().any(|p| name.starts_with(p))
259}
260
261fn find_unused_exports(
263 graph: &ModuleGraph,
264 config: &ResolvedConfig,
265 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
266) -> (Vec<UnusedExport>, Vec<UnusedExport>) {
267 let mut unused_exports = Vec::new();
268 let mut unused_types = Vec::new();
269
270 let ignore_matchers: Vec<(globset::GlobMatcher, &[String])> = config
272 .ignore_export_rules
273 .iter()
274 .filter_map(|rule| {
275 globset::Glob::new(&rule.file)
276 .ok()
277 .map(|g| (g.compile_matcher(), rule.exports.as_slice()))
278 })
279 .collect();
280
281 let framework_matchers: Vec<(globset::GlobMatcher, &[String])> = config
282 .framework_rules
283 .iter()
284 .flat_map(|rule| &rule.used_exports)
285 .filter_map(|used| {
286 globset::Glob::new(&used.file_pattern)
287 .ok()
288 .map(|g| (g.compile_matcher(), used.exports.as_slice()))
289 })
290 .collect();
291
292 let plugin_matchers: Vec<(globset::GlobMatcher, Vec<&str>)> = plugin_result
294 .map(|pr| {
295 pr.used_exports
296 .iter()
297 .filter_map(|(file_pat, exports)| {
298 globset::Glob::new(file_pat).ok().map(|g| {
299 (
300 g.compile_matcher(),
301 exports.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
302 )
303 })
304 })
305 .collect()
306 })
307 .unwrap_or_default();
308
309 for module in &graph.modules {
310 if !module.is_reachable {
312 continue;
313 }
314
315 if module.is_entry_point {
317 continue;
318 }
319
320 if module.has_cjs_exports && module.exports.is_empty() {
322 continue;
323 }
324
325 if module.path.extension().is_some_and(|ext| ext == "svelte") {
334 continue;
335 }
336
337 let relative_path = module
339 .path
340 .strip_prefix(&config.root)
341 .unwrap_or(&module.path);
342 let file_str = relative_path.to_string_lossy();
343
344 let matching_ignore: Vec<&[String]> = ignore_matchers
346 .iter()
347 .filter(|(m, _)| m.is_match(file_str.as_ref()))
348 .map(|(_, exports)| *exports)
349 .collect();
350
351 let matching_framework: Vec<&[String]> = framework_matchers
352 .iter()
353 .filter(|(m, _)| m.is_match(file_str.as_ref()))
354 .map(|(_, exports)| *exports)
355 .collect();
356
357 let matching_plugin: Vec<&Vec<&str>> = plugin_matchers
359 .iter()
360 .filter(|(m, _)| m.is_match(file_str.as_ref()))
361 .map(|(_, exports)| exports)
362 .collect();
363
364 let mut source_content: Option<String> = None;
366
367 for export in &module.exports {
368 if export.references.is_empty() {
369 let export_str = export.name.to_string();
370
371 if matching_ignore
373 .iter()
374 .any(|exports| exports.iter().any(|e| e == "*" || e == &export_str))
375 {
376 continue;
377 }
378
379 if matching_framework
381 .iter()
382 .any(|exports| exports.iter().any(|e| e == &export_str))
383 {
384 continue;
385 }
386
387 if matching_plugin
389 .iter()
390 .any(|exports| exports.iter().any(|e| *e == export_str))
391 {
392 continue;
393 }
394
395 let source = source_content.get_or_insert_with(|| read_source(&module.path));
396 let (line, col) = byte_offset_to_line_col(source, export.span.start);
397
398 let unused = UnusedExport {
399 path: module.path.clone(),
400 export_name: export_str,
401 is_type_only: export.is_type_only,
402 line,
403 col,
404 span_start: export.span.start,
405 };
406
407 if export.is_type_only {
408 unused_types.push(unused);
409 } else {
410 unused_exports.push(unused);
411 }
412 }
413 }
414 }
415
416 (unused_exports, unused_types)
417}
418
419fn find_unused_dependencies(
421 graph: &ModuleGraph,
422 pkg: &PackageJson,
423 config: &ResolvedConfig,
424 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
425 workspaces: &[fallow_config::WorkspaceInfo],
426) -> (Vec<UnusedDependency>, Vec<UnusedDependency>) {
427 let used_packages: HashSet<&str> = graph.package_usage.keys().map(|s| s.as_str()).collect();
428
429 let plugin_referenced: HashSet<&str> = plugin_result
431 .map(|pr| {
432 pr.referenced_dependencies
433 .iter()
434 .map(|s| s.as_str())
435 .collect()
436 })
437 .unwrap_or_default();
438
439 let plugin_tooling: HashSet<&str> = plugin_result
441 .map(|pr| pr.tooling_dependencies.iter().map(|s| s.as_str()).collect())
442 .unwrap_or_default();
443
444 let workspace_names: HashSet<&str> = workspaces.iter().map(|ws| ws.name.as_str()).collect();
446
447 let unused_deps: Vec<UnusedDependency> = pkg
448 .production_dependency_names()
449 .into_iter()
450 .filter(|dep| !used_packages.contains(dep.as_str()))
451 .filter(|dep| !is_implicit_dependency(dep))
452 .filter(|dep| !plugin_referenced.contains(dep.as_str()))
453 .filter(|dep| !config.ignore_dependencies.iter().any(|d| d == dep))
454 .filter(|dep| !workspace_names.contains(dep.as_str()))
456 .map(|dep| UnusedDependency {
457 package_name: dep,
458 location: DependencyLocation::Dependencies,
459 })
460 .collect();
461
462 let unused_dev_deps: Vec<UnusedDependency> = pkg
463 .dev_dependency_names()
464 .into_iter()
465 .filter(|dep| !used_packages.contains(dep.as_str()))
466 .filter(|dep| !is_tooling_dependency(dep))
467 .filter(|dep| !plugin_tooling.contains(dep.as_str()))
468 .filter(|dep| !plugin_referenced.contains(dep.as_str()))
469 .filter(|dep| !config.ignore_dependencies.iter().any(|d| d == dep))
470 .filter(|dep| !workspace_names.contains(dep.as_str()))
471 .map(|dep| UnusedDependency {
472 package_name: dep,
473 location: DependencyLocation::DevDependencies,
474 })
475 .collect();
476
477 (unused_deps, unused_dev_deps)
478}
479
480fn find_unused_members(
485 graph: &ModuleGraph,
486 _config: &ResolvedConfig,
487 resolved_modules: &[ResolvedModule],
488) -> (Vec<UnusedMember>, Vec<UnusedMember>) {
489 let mut unused_enum_members = Vec::new();
490 let mut unused_class_members = Vec::new();
491
492 let mut accessed_members: HashSet<(String, String)> = HashSet::new();
495
496 let mut self_accessed_members: HashMap<crate::discover::FileId, HashSet<String>> =
500 HashMap::new();
501
502 let mut whole_object_used_exports: HashSet<String> = HashSet::new();
505
506 for resolved in resolved_modules {
507 let local_to_imported: HashMap<&str, &str> = resolved
509 .resolved_imports
510 .iter()
511 .filter_map(|imp| match &imp.info.imported_name {
512 crate::extract::ImportedName::Named(name) => {
513 Some((imp.info.local_name.as_str(), name.as_str()))
514 }
515 crate::extract::ImportedName::Default => {
516 Some((imp.info.local_name.as_str(), "default"))
517 }
518 _ => None,
519 })
520 .collect();
521
522 for access in &resolved.member_accesses {
523 if access.object == "this" {
525 self_accessed_members
526 .entry(resolved.file_id)
527 .or_default()
528 .insert(access.member.clone());
529 continue;
530 }
531 let export_name = local_to_imported
533 .get(access.object.as_str())
534 .copied()
535 .unwrap_or(access.object.as_str());
536 accessed_members.insert((export_name.to_string(), access.member.clone()));
537 }
538
539 for local_name in &resolved.whole_object_uses {
541 let export_name = local_to_imported
542 .get(local_name.as_str())
543 .copied()
544 .unwrap_or(local_name.as_str());
545 whole_object_used_exports.insert(export_name.to_string());
546 }
547 }
548
549 for module in &graph.modules {
550 if !module.is_reachable || module.is_entry_point {
551 continue;
552 }
553
554 let mut source_content: Option<String> = None;
556
557 for export in &module.exports {
558 if export.members.is_empty() {
559 continue;
560 }
561
562 if export.references.is_empty() && !graph.has_namespace_import(module.file_id) {
564 continue;
565 }
566
567 let export_name = export.name.to_string();
568
569 if whole_object_used_exports.contains(&export_name) {
572 continue;
573 }
574
575 let file_self_accesses = self_accessed_members.get(&module.file_id);
577
578 for member in &export.members {
579 if accessed_members.contains(&(export_name.clone(), member.name.clone())) {
581 continue;
582 }
583
584 if matches!(
587 member.kind,
588 MemberKind::ClassMethod | MemberKind::ClassProperty
589 ) && file_self_accesses.is_some_and(|accesses| accesses.contains(&member.name))
590 {
591 continue;
592 }
593
594 if matches!(
598 member.kind,
599 MemberKind::ClassMethod | MemberKind::ClassProperty
600 ) && (is_react_lifecycle_method(&member.name)
601 || is_angular_lifecycle_method(&member.name))
602 {
603 continue;
604 }
605
606 let source = source_content.get_or_insert_with(|| read_source(&module.path));
607 let (line, col) = byte_offset_to_line_col(source, member.span.start);
608
609 let unused = UnusedMember {
610 path: module.path.clone(),
611 parent_name: export_name.clone(),
612 member_name: member.name.clone(),
613 kind: member.kind.clone(),
614 line,
615 col,
616 };
617
618 match member.kind {
619 MemberKind::EnumMember => unused_enum_members.push(unused),
620 MemberKind::ClassMethod | MemberKind::ClassProperty => {
621 unused_class_members.push(unused);
622 }
623 }
624 }
625 }
626 }
627
628 (unused_enum_members, unused_class_members)
629}
630
631fn find_unlisted_dependencies(
633 graph: &ModuleGraph,
634 pkg: &PackageJson,
635 config: &ResolvedConfig,
636 workspaces: &[fallow_config::WorkspaceInfo],
637) -> Vec<UnlistedDependency> {
638 let all_deps: HashSet<String> = pkg.all_dependency_names().into_iter().collect();
639
640 let mut all_workspace_deps: HashSet<String> = all_deps.clone();
643 let mut workspace_names: HashSet<String> = HashSet::new();
645 let mut ws_dep_map: Vec<(std::path::PathBuf, HashSet<String>)> = Vec::new();
647
648 for ws in workspaces {
649 workspace_names.insert(ws.name.clone());
650 let ws_pkg_path = ws.root.join("package.json");
651 if let Ok(ws_pkg) = PackageJson::load(&ws_pkg_path) {
652 let ws_deps: HashSet<String> = ws_pkg.all_dependency_names().into_iter().collect();
653 all_workspace_deps.extend(ws_deps.iter().cloned());
654 let canonical_ws = ws.root.canonicalize().unwrap_or_else(|_| ws.root.clone());
655 ws_dep_map.push((canonical_ws, ws_deps));
656 }
657 }
658
659 let mut unlisted: HashMap<String, Vec<std::path::PathBuf>> = HashMap::new();
660
661 for (package_name, file_ids) in &graph.package_usage {
662 if is_builtin_module(package_name) || is_path_alias(package_name) {
663 continue;
664 }
665 if workspace_names.contains(package_name) {
667 continue;
668 }
669 if all_workspace_deps.contains(package_name) {
671 continue;
672 }
673
674 let mut unlisted_paths: Vec<std::path::PathBuf> = Vec::new();
676 for id in file_ids {
677 if let Some(module) = graph.modules.get(id.0 as usize) {
678 let file_canonical = module.path.canonicalize().unwrap_or(module.path.clone());
679 let listed_in_ws = ws_dep_map.iter().any(|(ws_root, ws_deps)| {
680 file_canonical.starts_with(ws_root) && ws_deps.contains(package_name)
681 });
682 let listed_in_root = all_deps.contains(package_name);
684 if !listed_in_ws && !listed_in_root {
685 unlisted_paths.push(module.path.clone());
686 }
687 }
688 }
689
690 if !unlisted_paths.is_empty() {
691 unlisted_paths.sort();
692 unlisted_paths.dedup();
693 unlisted.insert(package_name.clone(), unlisted_paths);
694 }
695 }
696
697 let _ = config; unlisted
699 .into_iter()
700 .map(|(name, paths)| UnlistedDependency {
701 package_name: name,
702 imported_from: paths,
703 })
704 .collect()
705}
706
707fn is_path_alias(name: &str) -> bool {
713 if name.starts_with('#') {
715 return true;
716 }
717 if name.starts_with("~/") {
719 return true;
720 }
721 if name.starts_with("@/") {
723 return true;
724 }
725 if name.starts_with('@') {
729 let scope = name.split('/').next().unwrap_or(name);
730 if scope.len() > 1 && scope.chars().nth(1).is_some_and(|c| c.is_ascii_uppercase()) {
731 return true;
732 }
733 }
734
735 false
736}
737
738fn find_unresolved_imports(
740 resolved_modules: &[ResolvedModule],
741 _config: &ResolvedConfig,
742) -> Vec<UnresolvedImport> {
743 let mut unresolved = Vec::new();
744
745 for module in resolved_modules {
746 let mut source_content: Option<String> = None;
748
749 for import in &module.resolved_imports {
750 if let crate::resolve::ResolveResult::Unresolvable(spec) = &import.target {
751 let source = source_content.get_or_insert_with(|| read_source(&module.path));
752 let (line, col) = byte_offset_to_line_col(source, import.info.span.start);
753
754 unresolved.push(UnresolvedImport {
755 path: module.path.clone(),
756 specifier: spec.clone(),
757 line,
758 col,
759 });
760 }
761 }
762 }
763
764 unresolved
765}
766
767fn find_duplicate_exports(graph: &ModuleGraph, config: &ResolvedConfig) -> Vec<DuplicateExport> {
773 let mut re_export_sources: HashMap<usize, HashSet<usize>> = HashMap::new();
775 for (idx, module) in graph.modules.iter().enumerate() {
776 for re in &module.re_exports {
777 re_export_sources
778 .entry(idx)
779 .or_default()
780 .insert(re.source_file.0 as usize);
781 }
782 }
783
784 let mut export_locations: HashMap<String, Vec<(usize, std::path::PathBuf)>> = HashMap::new();
785
786 for (idx, module) in graph.modules.iter().enumerate() {
787 if !module.is_reachable || module.is_entry_point {
788 continue;
789 }
790
791 for export in &module.exports {
792 if matches!(export.name, crate::extract::ExportName::Default) {
793 continue; }
795 if export.span.start == 0 && export.span.end == 0 {
798 continue;
799 }
800 let name = export.name.to_string();
801 export_locations
802 .entry(name)
803 .or_default()
804 .push((idx, module.path.clone()));
805 }
806 }
807
808 let _ = config; export_locations
811 .into_iter()
812 .filter_map(|(name, locations)| {
813 if locations.len() <= 1 {
814 return None;
815 }
816 let module_indices: HashSet<usize> = locations.iter().map(|(idx, _)| *idx).collect();
820 let independent: Vec<std::path::PathBuf> = locations
821 .into_iter()
822 .filter(|(idx, _)| {
823 let sources = re_export_sources.get(idx);
826 let has_source_in_set = sources
827 .map(|s| s.iter().any(|src| module_indices.contains(src)))
828 .unwrap_or(false);
829 !has_source_in_set
830 })
831 .map(|(_, path)| path)
832 .collect();
833
834 if independent.len() > 1 {
835 Some(DuplicateExport {
836 export_name: name,
837 locations: independent,
838 })
839 } else {
840 None
841 }
842 })
843 .collect()
844}
845
846fn is_builtin_module(name: &str) -> bool {
848 let builtins = [
849 "assert",
850 "assert/strict",
851 "async_hooks",
852 "buffer",
853 "child_process",
854 "cluster",
855 "console",
856 "constants",
857 "crypto",
858 "dgram",
859 "diagnostics_channel",
860 "dns",
861 "dns/promises",
862 "domain",
863 "events",
864 "fs",
865 "fs/promises",
866 "http",
867 "http2",
868 "https",
869 "inspector",
870 "inspector/promises",
871 "module",
872 "net",
873 "os",
874 "path",
875 "path/posix",
876 "path/win32",
877 "perf_hooks",
878 "process",
879 "punycode",
880 "querystring",
881 "readline",
882 "readline/promises",
883 "repl",
884 "stream",
885 "stream/consumers",
886 "stream/promises",
887 "stream/web",
888 "string_decoder",
889 "sys",
890 "test",
891 "test/reporters",
892 "timers",
893 "timers/promises",
894 "tls",
895 "trace_events",
896 "tty",
897 "url",
898 "util",
899 "util/types",
900 "v8",
901 "vm",
902 "wasi",
903 "worker_threads",
904 "zlib",
905 ];
906 let stripped = name.strip_prefix("node:").unwrap_or(name);
907 builtins.contains(&stripped) || {
910 stripped
912 .split('/')
913 .next()
914 .is_some_and(|root| builtins.contains(&root))
915 }
916}
917
918fn is_implicit_dependency(name: &str) -> bool {
920 if name.starts_with("@types/") {
921 return true;
922 }
923
924 let implicit_deps = [
927 "react-dom",
928 "react-dom/client",
929 "react-native",
930 "@next/font",
931 "@next/mdx",
932 "@next/bundle-analyzer",
933 "@next/env",
934 "utf-8-validate",
936 "bufferutil",
937 ];
938 implicit_deps.contains(&name)
939}
940
941fn is_tooling_dependency(name: &str) -> bool {
943 let tooling_prefixes = [
944 "@types/",
945 "eslint",
946 "@typescript-eslint",
947 "husky",
948 "lint-staged",
949 "commitlint",
950 "@commitlint",
951 "stylelint",
952 "postcss",
953 "autoprefixer",
954 "tailwindcss",
955 "@tailwindcss",
956 "@vitest/",
957 "@jest/",
958 "@testing-library/",
959 "@playwright/",
960 "@storybook/",
961 "storybook",
962 "@babel/",
963 "babel-",
964 "@react-native-community/cli",
965 "@react-native/",
966 "secretlint",
967 "@secretlint/",
968 "oxlint",
969 "@semantic-release/",
971 "semantic-release",
972 "@release-it/",
973 "@lerna-lite/",
974 "@changesets/",
975 "@graphql-codegen/",
977 "@rollup/",
978 "@biomejs/",
980 ];
981
982 let exact_matches = [
983 "typescript",
984 "prettier",
985 "turbo",
986 "concurrently",
987 "cross-env",
988 "rimraf",
989 "npm-run-all",
990 "npm-run-all2",
991 "nodemon",
992 "ts-node",
993 "tsx",
994 "knip",
995 "fallow",
996 "jest",
997 "vitest",
998 "happy-dom",
999 "jsdom",
1000 "vite",
1001 "sass",
1002 "sass-embedded",
1003 "webpack",
1004 "webpack-cli",
1005 "webpack-dev-server",
1006 "esbuild",
1007 "rollup",
1008 "swc",
1009 "@swc/core",
1010 "@swc/jest",
1011 "terser",
1012 "cssnano",
1013 "sharp",
1014 "release-it",
1016 "lerna",
1017 "dotenv-cli",
1019 "dotenv-flow",
1020 "oxfmt",
1022 "jscpd",
1023 "npm-check-updates",
1024 "markdownlint-cli",
1025 "npm-package-json-lint",
1026 "synp",
1027 "flow-bin",
1028 "i18next-parser",
1030 "i18next-conv",
1031 "webpack-bundle-analyzer",
1033 "vite-plugin-svgr",
1035 "vite-plugin-eslint",
1036 "@vitejs/plugin-vue",
1037 "@vitejs/plugin-react",
1038 "next-sitemap",
1040 "tsup",
1042 "unbuild",
1043 "typedoc",
1044 "nx",
1046 "@manypkg/cli",
1047 "vue-tsc",
1049 "@vue/tsconfig",
1050 "@tsconfig/node20",
1051 "@tsconfig/react-native",
1052 "@typescript/native-preview",
1054 "tw-animate-css",
1056 "@ianvs/prettier-plugin-sort-imports",
1058 "prettier-plugin-tailwindcss",
1059 "prettier-plugin-organize-imports",
1060 "@vitejs/plugin-react-swc",
1062 "@vitejs/plugin-legacy",
1063 ];
1064
1065 tooling_prefixes.iter().any(|p| name.starts_with(p)) || exact_matches.contains(&name)
1066}
1067
1068fn is_angular_lifecycle_method(name: &str) -> bool {
1073 matches!(
1074 name,
1075 "ngOnInit"
1076 | "ngOnDestroy"
1077 | "ngOnChanges"
1078 | "ngDoCheck"
1079 | "ngAfterContentInit"
1080 | "ngAfterContentChecked"
1081 | "ngAfterViewInit"
1082 | "ngAfterViewChecked"
1083 | "ngAcceptInputType"
1084 | "canActivate"
1086 | "canDeactivate"
1087 | "canActivateChild"
1088 | "canMatch"
1089 | "resolve"
1090 | "intercept"
1091 | "transform"
1092 | "validate"
1094 | "registerOnChange"
1095 | "registerOnTouched"
1096 | "writeValue"
1097 | "setDisabledState"
1098 )
1099}
1100
1101fn is_react_lifecycle_method(name: &str) -> bool {
1102 matches!(
1103 name,
1104 "render"
1105 | "componentDidMount"
1106 | "componentDidUpdate"
1107 | "componentWillUnmount"
1108 | "shouldComponentUpdate"
1109 | "getSnapshotBeforeUpdate"
1110 | "getDerivedStateFromProps"
1111 | "getDerivedStateFromError"
1112 | "componentDidCatch"
1113 | "componentWillMount"
1114 | "componentWillReceiveProps"
1115 | "componentWillUpdate"
1116 | "UNSAFE_componentWillMount"
1117 | "UNSAFE_componentWillReceiveProps"
1118 | "UNSAFE_componentWillUpdate"
1119 | "getChildContext"
1120 | "contextType"
1121 )
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use super::*;
1127
1128 #[test]
1130 fn builtin_module_fs() {
1131 assert!(is_builtin_module("fs"));
1132 }
1133
1134 #[test]
1135 fn builtin_module_path() {
1136 assert!(is_builtin_module("path"));
1137 }
1138
1139 #[test]
1140 fn builtin_module_with_node_prefix() {
1141 assert!(is_builtin_module("node:fs"));
1142 assert!(is_builtin_module("node:path"));
1143 assert!(is_builtin_module("node:crypto"));
1144 }
1145
1146 #[test]
1147 fn builtin_module_all_known() {
1148 let known = [
1149 "assert",
1150 "buffer",
1151 "child_process",
1152 "cluster",
1153 "console",
1154 "constants",
1155 "crypto",
1156 "dgram",
1157 "dns",
1158 "domain",
1159 "events",
1160 "fs",
1161 "http",
1162 "http2",
1163 "https",
1164 "module",
1165 "net",
1166 "os",
1167 "path",
1168 "perf_hooks",
1169 "process",
1170 "punycode",
1171 "querystring",
1172 "readline",
1173 "repl",
1174 "stream",
1175 "string_decoder",
1176 "sys",
1177 "timers",
1178 "tls",
1179 "tty",
1180 "url",
1181 "util",
1182 "v8",
1183 "vm",
1184 "wasi",
1185 "worker_threads",
1186 "zlib",
1187 ];
1188 for name in &known {
1189 assert!(is_builtin_module(name), "{name} should be a builtin module");
1190 }
1191 }
1192
1193 #[test]
1194 fn not_builtin_module() {
1195 assert!(!is_builtin_module("react"));
1196 assert!(!is_builtin_module("lodash"));
1197 assert!(!is_builtin_module("express"));
1198 assert!(!is_builtin_module("@scope/pkg"));
1199 }
1200
1201 #[test]
1202 fn not_builtin_similar_names() {
1203 assert!(!is_builtin_module("filesystem"));
1204 assert!(!is_builtin_module("pathlib"));
1205 assert!(!is_builtin_module("node:react"));
1206 }
1207
1208 #[test]
1210 fn implicit_dep_types_packages() {
1211 assert!(is_implicit_dependency("@types/node"));
1212 assert!(is_implicit_dependency("@types/react"));
1213 assert!(is_implicit_dependency("@types/jest"));
1214 }
1215
1216 #[test]
1217 fn not_implicit_dep() {
1218 assert!(!is_implicit_dependency("react"));
1219 assert!(!is_implicit_dependency("@scope/types"));
1220 assert!(!is_implicit_dependency("types"));
1221 assert!(!is_implicit_dependency("typescript"));
1222 assert!(!is_implicit_dependency("prettier"));
1223 assert!(!is_implicit_dependency("eslint"));
1224 }
1225
1226 #[test]
1228 fn tooling_dep_prefixes() {
1229 assert!(is_tooling_dependency("@types/node"));
1230 assert!(is_tooling_dependency("eslint"));
1231 assert!(is_tooling_dependency("eslint-plugin-react"));
1232 assert!(is_tooling_dependency("prettier"));
1233 assert!(is_tooling_dependency("@typescript-eslint/parser"));
1234 assert!(is_tooling_dependency("husky"));
1235 assert!(is_tooling_dependency("lint-staged"));
1236 assert!(is_tooling_dependency("commitlint"));
1237 assert!(is_tooling_dependency("@commitlint/config-conventional"));
1238 assert!(is_tooling_dependency("stylelint"));
1239 assert!(is_tooling_dependency("postcss"));
1240 assert!(is_tooling_dependency("autoprefixer"));
1241 assert!(is_tooling_dependency("tailwindcss"));
1242 assert!(is_tooling_dependency("@tailwindcss/forms"));
1243 }
1244
1245 #[test]
1246 fn tooling_dep_exact_matches() {
1247 assert!(is_tooling_dependency("typescript"));
1248 assert!(is_tooling_dependency("prettier"));
1249 assert!(is_tooling_dependency("turbo"));
1250 assert!(is_tooling_dependency("concurrently"));
1251 assert!(is_tooling_dependency("cross-env"));
1252 assert!(is_tooling_dependency("rimraf"));
1253 assert!(is_tooling_dependency("npm-run-all"));
1254 assert!(is_tooling_dependency("nodemon"));
1255 assert!(is_tooling_dependency("ts-node"));
1256 assert!(is_tooling_dependency("tsx"));
1257 }
1258
1259 #[test]
1260 fn not_tooling_dep() {
1261 assert!(!is_tooling_dependency("react"));
1262 assert!(!is_tooling_dependency("next"));
1263 assert!(!is_tooling_dependency("lodash"));
1264 assert!(!is_tooling_dependency("express"));
1265 assert!(!is_tooling_dependency("@emotion/react"));
1266 }
1267
1268 #[test]
1270 fn tooling_dep_testing_frameworks() {
1271 assert!(is_tooling_dependency("jest"));
1272 assert!(is_tooling_dependency("vitest"));
1273 assert!(is_tooling_dependency("@jest/globals"));
1274 assert!(is_tooling_dependency("@vitest/coverage-v8"));
1275 assert!(is_tooling_dependency("@testing-library/react"));
1276 assert!(is_tooling_dependency("@testing-library/jest-dom"));
1277 assert!(is_tooling_dependency("@playwright/test"));
1278 }
1279
1280 #[test]
1281 fn tooling_dep_environments_and_cli() {
1282 assert!(is_tooling_dependency("happy-dom"));
1283 assert!(is_tooling_dependency("jsdom"));
1284 assert!(is_tooling_dependency("knip"));
1285 }
1286
1287 #[test]
1289 fn react_lifecycle_standard_methods() {
1290 assert!(is_react_lifecycle_method("render"));
1291 assert!(is_react_lifecycle_method("componentDidMount"));
1292 assert!(is_react_lifecycle_method("componentDidUpdate"));
1293 assert!(is_react_lifecycle_method("componentWillUnmount"));
1294 assert!(is_react_lifecycle_method("shouldComponentUpdate"));
1295 assert!(is_react_lifecycle_method("getSnapshotBeforeUpdate"));
1296 }
1297
1298 #[test]
1299 fn react_lifecycle_static_methods() {
1300 assert!(is_react_lifecycle_method("getDerivedStateFromProps"));
1301 assert!(is_react_lifecycle_method("getDerivedStateFromError"));
1302 }
1303
1304 #[test]
1305 fn react_lifecycle_error_boundary() {
1306 assert!(is_react_lifecycle_method("componentDidCatch"));
1307 }
1308
1309 #[test]
1310 fn react_lifecycle_deprecated_and_unsafe() {
1311 assert!(is_react_lifecycle_method("componentWillMount"));
1312 assert!(is_react_lifecycle_method("componentWillReceiveProps"));
1313 assert!(is_react_lifecycle_method("componentWillUpdate"));
1314 assert!(is_react_lifecycle_method("UNSAFE_componentWillMount"));
1315 assert!(is_react_lifecycle_method(
1316 "UNSAFE_componentWillReceiveProps"
1317 ));
1318 assert!(is_react_lifecycle_method("UNSAFE_componentWillUpdate"));
1319 }
1320
1321 #[test]
1322 fn react_lifecycle_context_methods() {
1323 assert!(is_react_lifecycle_method("getChildContext"));
1324 assert!(is_react_lifecycle_method("contextType"));
1325 }
1326
1327 #[test]
1328 fn not_react_lifecycle_method() {
1329 assert!(!is_react_lifecycle_method("handleClick"));
1330 assert!(!is_react_lifecycle_method("fetchData"));
1331 assert!(!is_react_lifecycle_method("constructor"));
1332 assert!(!is_react_lifecycle_method("setState"));
1333 assert!(!is_react_lifecycle_method("forceUpdate"));
1334 assert!(!is_react_lifecycle_method("customMethod"));
1335 }
1336
1337 #[test]
1339 fn declaration_file_dts() {
1340 assert!(is_declaration_file(std::path::Path::new("styled.d.ts")));
1341 assert!(is_declaration_file(std::path::Path::new(
1342 "src/types/styled.d.ts"
1343 )));
1344 assert!(is_declaration_file(std::path::Path::new("env.d.ts")));
1345 }
1346
1347 #[test]
1348 fn declaration_file_dmts_dcts() {
1349 assert!(is_declaration_file(std::path::Path::new("module.d.mts")));
1350 assert!(is_declaration_file(std::path::Path::new("module.d.cts")));
1351 }
1352
1353 #[test]
1354 fn not_declaration_file() {
1355 assert!(!is_declaration_file(std::path::Path::new("index.ts")));
1356 assert!(!is_declaration_file(std::path::Path::new("component.tsx")));
1357 assert!(!is_declaration_file(std::path::Path::new("utils.js")));
1358 assert!(!is_declaration_file(std::path::Path::new("styles.d.css")));
1359 }
1360
1361 #[test]
1363 fn byte_offset_empty_source() {
1364 assert_eq!(byte_offset_to_line_col("", 0), (1, 0));
1365 }
1366
1367 #[test]
1368 fn byte_offset_single_line_start() {
1369 assert_eq!(byte_offset_to_line_col("hello", 0), (1, 0));
1370 }
1371
1372 #[test]
1373 fn byte_offset_single_line_middle() {
1374 assert_eq!(byte_offset_to_line_col("hello", 4), (1, 4));
1375 }
1376
1377 #[test]
1378 fn byte_offset_multiline_start_of_line2() {
1379 let source = "line1\nline2\nline3";
1383 assert_eq!(byte_offset_to_line_col(source, 6), (2, 0));
1384 }
1385
1386 #[test]
1387 fn byte_offset_multiline_middle_of_line3() {
1388 let source = "line1\nline2\nline3";
1393 assert_eq!(byte_offset_to_line_col(source, 14), (3, 2));
1394 }
1395
1396 #[test]
1397 fn byte_offset_at_newline_boundary() {
1398 let source = "line1\nline2";
1401 assert_eq!(byte_offset_to_line_col(source, 5), (1, 5));
1402 }
1403
1404 #[test]
1405 fn byte_offset_beyond_source_length() {
1406 let source = "hello";
1410 assert_eq!(byte_offset_to_line_col(source, 100), (1, 100));
1411 }
1412
1413 #[test]
1414 fn byte_offset_multibyte_utf8() {
1415 let source = "hi\n\u{1F600}x";
1417 assert_eq!(byte_offset_to_line_col(source, 3), (2, 0));
1419 assert_eq!(byte_offset_to_line_col(source, 7), (2, 4));
1421 }
1422
1423 #[test]
1424 fn byte_offset_multibyte_accented_chars() {
1425 let source = "caf\u{00E9}\nbar";
1427 assert_eq!(byte_offset_to_line_col(source, 6), (2, 0));
1430 assert_eq!(byte_offset_to_line_col(source, 3), (1, 3));
1432 }
1433
1434 #[test]
1436 fn path_alias_at_slash() {
1437 assert!(is_path_alias("@/components"));
1438 }
1439
1440 #[test]
1441 fn path_alias_tilde() {
1442 assert!(is_path_alias("~/lib"));
1443 }
1444
1445 #[test]
1446 fn path_alias_hash_imports_map() {
1447 assert!(is_path_alias("#internal/module"));
1448 }
1449
1450 #[test]
1451 fn path_alias_pascal_case_scope() {
1452 assert!(is_path_alias("@Components/Button"));
1453 }
1454
1455 #[test]
1456 fn not_path_alias_regular_package() {
1457 assert!(!is_path_alias("react"));
1458 }
1459
1460 #[test]
1461 fn not_path_alias_scoped_npm_package() {
1462 assert!(!is_path_alias("@scope/pkg"));
1463 }
1464
1465 #[test]
1466 fn not_path_alias_emotion_react() {
1467 assert!(!is_path_alias("@emotion/react"));
1468 }
1469
1470 #[test]
1471 fn not_path_alias_lodash() {
1472 assert!(!is_path_alias("lodash"));
1473 }
1474
1475 #[test]
1476 fn not_path_alias_lowercase_short_scope() {
1477 assert!(!is_path_alias("@s/lowercase"));
1478 }
1479
1480 #[test]
1482 fn angular_lifecycle_core_hooks() {
1483 assert!(is_angular_lifecycle_method("ngOnInit"));
1484 assert!(is_angular_lifecycle_method("ngOnDestroy"));
1485 assert!(is_angular_lifecycle_method("ngOnChanges"));
1486 assert!(is_angular_lifecycle_method("ngAfterViewInit"));
1487 }
1488
1489 #[test]
1490 fn angular_lifecycle_check_hooks() {
1491 assert!(is_angular_lifecycle_method("ngDoCheck"));
1492 assert!(is_angular_lifecycle_method("ngAfterContentChecked"));
1493 assert!(is_angular_lifecycle_method("ngAfterViewChecked"));
1494 }
1495
1496 #[test]
1497 fn angular_lifecycle_content_hooks() {
1498 assert!(is_angular_lifecycle_method("ngAfterContentInit"));
1499 assert!(is_angular_lifecycle_method("ngAcceptInputType"));
1500 }
1501
1502 #[test]
1503 fn angular_lifecycle_guard_resolver_methods() {
1504 assert!(is_angular_lifecycle_method("canActivate"));
1505 assert!(is_angular_lifecycle_method("canDeactivate"));
1506 assert!(is_angular_lifecycle_method("canActivateChild"));
1507 assert!(is_angular_lifecycle_method("canMatch"));
1508 assert!(is_angular_lifecycle_method("resolve"));
1509 assert!(is_angular_lifecycle_method("intercept"));
1510 assert!(is_angular_lifecycle_method("transform"));
1511 }
1512
1513 #[test]
1514 fn angular_lifecycle_form_methods() {
1515 assert!(is_angular_lifecycle_method("validate"));
1516 assert!(is_angular_lifecycle_method("registerOnChange"));
1517 assert!(is_angular_lifecycle_method("registerOnTouched"));
1518 assert!(is_angular_lifecycle_method("writeValue"));
1519 assert!(is_angular_lifecycle_method("setDisabledState"));
1520 }
1521
1522 #[test]
1523 fn not_angular_lifecycle_method() {
1524 assert!(!is_angular_lifecycle_method("onClick"));
1525 assert!(!is_angular_lifecycle_method("handleSubmit"));
1526 assert!(!is_angular_lifecycle_method("render"));
1527 }
1528}