1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::path::{Path, PathBuf};
4use std::sync::{Mutex, OnceLock};
5
6use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
7use rustc_hash::FxHashSet;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use super::boundaries::ResolvedBoundaryConfig;
12use super::duplicates_config::DuplicatesConfig;
13use super::flags::FlagsConfig;
14use super::format::OutputFormat;
15use super::health::HealthConfig;
16use super::resolve::ResolveConfig;
17use super::rules::{PartialRulesConfig, RulesConfig, Severity};
18use super::similar_code::SimilarCodeConfig;
19use super::used_class_members::UsedClassMemberRule;
20use crate::external_plugin::{ExternalPluginDef, discover_external_plugins};
21
22use super::{
23 BoundaryConfig, FallowConfig, FindingIgnoreMatcher, IgnoreExportsUsedInFileConfig,
24 ProductionConfig, SecurityConfig, TypeAwareConfig,
25};
26
27static INTER_FILE_WARN_SEEN: OnceLock<Mutex<FxHashSet<u64>>> = OnceLock::new();
29
30fn inter_file_warn_key(rule_name: &str, files: &[String]) -> u64 {
32 let mut sorted: Vec<&str> = files.iter().map(String::as_str).collect();
33 sorted.sort_unstable();
34 let mut hasher = DefaultHasher::new();
35 rule_name.hash(&mut hasher);
36 for s in &sorted {
37 s.hash(&mut hasher);
38 }
39 hasher.finish()
40}
41
42fn record_inter_file_warn_seen(rule_name: &str, files: &[String]) -> bool {
44 let seen = INTER_FILE_WARN_SEEN.get_or_init(|| Mutex::new(FxHashSet::default()));
45 let key = inter_file_warn_key(rule_name, files);
46 seen.lock().map_or(true, |mut set| set.insert(key))
47}
48
49#[cfg(test)]
50fn reset_inter_file_warn_dedup_for_test() {
51 if let Some(seen) = INTER_FILE_WARN_SEEN.get()
52 && let Ok(mut set) = seen.lock()
53 {
54 set.clear();
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
60#[serde(deny_unknown_fields)]
61pub struct IgnoreExportRule {
62 pub file: String,
64 pub exports: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
70pub struct CompiledIgnoreExportRule {
71 pub matcher: globset::GlobMatcher,
73 pub exports: Vec<String>,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
79#[serde(deny_unknown_fields)]
80pub struct IgnoreCatalogReferenceRule {
81 pub package: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub catalog: Option<String>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub consumer: Option<String>,
89}
90
91#[derive(Debug, Clone)]
93pub struct CompiledIgnoreCatalogReferenceRule {
94 pub package: String,
96 pub catalog: Option<String>,
98 pub consumer_matcher: Option<globset::GlobMatcher>,
101}
102
103impl CompiledIgnoreCatalogReferenceRule {
104 #[must_use]
106 pub fn matches(&self, package: &str, catalog: &str, consumer_path: &str) -> bool {
107 if self.package != package {
108 return false;
109 }
110 if let Some(catalog_filter) = &self.catalog
111 && catalog_filter != catalog
112 {
113 return false;
114 }
115 if let Some(matcher) = &self.consumer_matcher
116 && !matcher.is_match(consumer_path)
117 {
118 return false;
119 }
120 true
121 }
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
126#[serde(deny_unknown_fields)]
127pub struct IgnoreDependencyOverrideRule {
128 pub package: String,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub source: Option<String>,
133}
134
135#[derive(Debug, Clone)]
137pub struct CompiledIgnoreDependencyOverrideRule {
138 pub package: String,
140 pub source: Option<String>,
143}
144
145impl CompiledIgnoreDependencyOverrideRule {
146 #[must_use]
148 pub fn matches(&self, package: &str, source_label: &str) -> bool {
149 if self.package != package {
150 return false;
151 }
152 if let Some(source_filter) = &self.source
153 && source_filter != source_label
154 {
155 return false;
156 }
157 true
158 }
159}
160
161#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
163#[serde(rename_all = "camelCase", deny_unknown_fields)]
164pub struct ConfigOverride {
165 pub files: Vec<String>,
167 #[serde(default)]
169 pub rules: PartialRulesConfig,
170}
171
172#[derive(Debug, Clone)]
174pub struct ResolvedOverride {
175 pub matchers: Vec<globset::GlobMatcher>,
178 pub rules: PartialRulesConfig,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
189pub enum AnalysisSnapshot {
190 #[default]
192 Current,
193 Base,
195}
196
197impl AnalysisSnapshot {
198 #[must_use]
200 pub const fn is_base(self) -> bool {
201 matches!(self, Self::Base)
202 }
203}
204
205#[derive(Debug, Clone)]
207pub struct ResolvedConfig {
208 pub root: PathBuf,
210 pub entry_patterns: Vec<String>,
213 pub ignore_patterns: GlobSet,
217 pub user_ignore_pattern_count: usize,
226 pub ignore_findings: FindingIgnoreMatcher,
229 pub output: OutputFormat,
231 pub cache_dir: PathBuf,
234 pub threads: usize,
236 pub no_cache: bool,
238 pub cache_max_size_mb: Option<u32>,
241 pub cache_config_hash: u64,
245 pub ignore_dependencies: Vec<String>,
248 pub ignore_unresolved_imports: Vec<GlobMatcher>,
251 pub ignore_export_rules: Vec<IgnoreExportRule>,
255 pub compiled_ignore_exports: Vec<CompiledIgnoreExportRule>,
257 pub compiled_ignore_catalog_references: Vec<CompiledIgnoreCatalogReferenceRule>,
259 pub compiled_ignore_dependency_overrides: Vec<CompiledIgnoreDependencyOverrideRule>,
261 pub ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig,
263 pub used_class_members: Vec<UsedClassMemberRule>,
266 pub ignore_decorators: Vec<String>,
269 pub unused_component_props_ignore: Option<regex::Regex>,
274 pub duplicates: DuplicatesConfig,
276 pub similar_code: SimilarCodeConfig,
278 pub health: HealthConfig,
280 pub type_aware: TypeAwareConfig,
282 pub rules: RulesConfig,
286 pub boundaries: ResolvedBoundaryConfig,
289 pub rule_packs: Vec<crate::rule_pack::RulePackDef>,
294 pub rule_pack_sources: Vec<PathBuf>,
297 pub production: bool,
302 pub quiet: bool,
305 pub external_plugins: Vec<ExternalPluginDef>,
309 pub dynamically_loaded: Vec<String>,
312 pub overrides: Vec<ResolvedOverride>,
314 pub regression: Option<super::RegressionConfig>,
316 pub audit: super::AuditConfig,
318 pub codeowners: Option<String>,
321 pub public_packages: Vec<String>,
325 pub flags: FlagsConfig,
327 pub security: SecurityConfig,
330 pub fix: super::FixConfig,
332 pub resolve: ResolveConfig,
335 pub include_entry_exports: bool,
338 pub auto_imports: bool,
342 pub fail_on_parse_error: bool,
346 pub max_file_size_bytes: Option<u64>,
355 pub analysis_snapshot: AnalysisSnapshot,
360}
361
362pub const DEFAULT_MAX_FILE_SIZE_MB: u32 = 5;
366
367pub const DEFAULT_MAX_FILE_SIZE_BYTES: u64 = DEFAULT_MAX_FILE_SIZE_MB as u64 * 1024 * 1024;
369
370#[must_use]
375pub fn resolve_max_file_size_bytes(max_file_size_mb: Option<u32>) -> Option<u64> {
376 match max_file_size_mb {
377 None => Some(DEFAULT_MAX_FILE_SIZE_BYTES),
378 Some(0) => None,
379 Some(mb) => Some(u64::from(mb) * 1024 * 1024),
380 }
381}
382
383#[must_use]
393pub fn cache_config_hash(external_plugins: &[ExternalPluginDef], flags: &FlagsConfig) -> u64 {
394 let mut names: Vec<&str> = external_plugins.iter().map(|p| p.name.as_str()).collect();
395 names.sort_unstable();
396 let mut hasher = xxhash_rust::xxh3::Xxh3::new();
397 for name in names {
398 hash_str(&mut hasher, name);
399 }
400 let patterns = flags.patterns();
401 if !patterns.is_builtin_only() {
402 hasher.update(b"\0flags");
403 hasher.update(&(patterns.sdk_patterns.len() as u64).to_le_bytes());
404 for (function, name_arg, provider) in &patterns.sdk_patterns {
405 hash_str(&mut hasher, function);
406 hasher.update(&(*name_arg as u64).to_le_bytes());
407 hash_str(&mut hasher, provider);
408 }
409 hasher.update(&(patterns.env_prefixes.len() as u64).to_le_bytes());
410 for prefix in &patterns.env_prefixes {
411 hash_str(&mut hasher, prefix);
412 }
413 hasher.update(&[u8::from(patterns.config_object_heuristics)]);
414 }
415 hasher.digest()
416}
417
418fn hash_str(hasher: &mut xxhash_rust::xxh3::Xxh3, value: &str) {
419 hasher.update(&(value.len() as u32).to_le_bytes());
420 hasher.update(value.as_bytes());
421}
422
423fn resolve_cache_dir(root: &Path, configured: Option<PathBuf>) -> PathBuf {
424 let Some(dir) = configured else {
425 return root.join(".fallow");
426 };
427 if dir.is_absolute() {
428 dir
429 } else {
430 root.join(dir)
431 }
432}
433
434fn normalize_user_glob_pattern(pattern: &str) -> &str {
435 pattern.strip_prefix("./").unwrap_or(pattern)
436}
437
438pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &[
452 "**/node_modules/**",
453 "**/dist/**",
454 "**/build/**",
455 "**/.git/**",
456 "**/coverage/**",
457 "**/*.min.js",
458 "**/*.min.mjs",
459 "**/*.min.cjs",
460 "**/*.bundle.js",
461];
462
463#[expect(
464 clippy::expect_used,
465 reason = "user glob patterns are validated before config resolution"
466)]
467fn compile_ignore_patterns(ignore_patterns: &[String]) -> GlobSet {
468 let mut ignore_builder = GlobSetBuilder::new();
469 for pattern in ignore_patterns {
470 let normalized = normalize_user_glob_pattern(pattern);
471 ignore_builder.add(
472 Glob::new(normalized).expect("ignorePatterns entry was validated at config load time"),
473 );
474 }
475
476 for pattern in DEFAULT_IGNORE_PATTERNS {
477 ignore_builder.add(Glob::new(pattern).expect("default ignore pattern is valid"));
478 }
479
480 ignore_builder.build().unwrap_or_default()
481}
482
483#[expect(
484 clippy::expect_used,
485 reason = "user glob patterns are validated before config resolution"
486)]
487fn compile_ignore_unresolved_imports(patterns: &[String]) -> Vec<GlobMatcher> {
488 patterns
489 .iter()
490 .map(|pattern| {
491 let normalized = normalize_user_glob_pattern(pattern);
492 Glob::new(normalized)
493 .expect("ignoreUnresolvedImports entry was validated at config load time")
494 .compile_matcher()
495 })
496 .collect()
497}
498
499fn resolve_rules_for_production(mut rules: RulesConfig, production: bool) -> RulesConfig {
500 if production {
501 rules.unused_dev_dependencies = Severity::Off;
502 rules.unused_optional_dependencies = Severity::Off;
503 }
504 rules
505}
506
507fn resolve_boundaries(
508 mut boundaries: super::boundaries::BoundaryConfig,
509 root: &Path,
510) -> ResolvedBoundaryConfig {
511 expand_boundary_preset(&mut boundaries, root);
512 let logical_groups = boundaries.expand_auto_discover(root);
513 let mut resolved = boundaries.resolve();
514 resolved.logical_groups = logical_groups;
515 resolved
516}
517
518pub(super) fn expand_boundary_preset(
521 boundaries: &mut super::boundaries::BoundaryConfig,
522 root: &Path,
523) {
524 if boundaries.preset.is_some() {
525 let source_root = crate::workspace::parse_tsconfig_root_dir(root)
526 .filter(|r| r != "." && !r.starts_with("..") && !std::path::Path::new(r).is_absolute())
527 .unwrap_or_else(|| "src".to_owned());
528 if source_root != "src" {
529 tracing::info!("boundary preset: using rootDir '{source_root}' from tsconfig.json");
530 }
531 boundaries.expand(&source_root);
532 }
533}
534
535fn ineffective_inter_file_override_rules(rules: &PartialRulesConfig) -> Vec<&'static str> {
541 let mut names = Vec::new();
542 if rules.duplicate_exports.is_some() {
543 names.push("duplicate-exports");
544 }
545 if rules.re_export_cycle.is_some() {
546 names.push("re-export-cycle");
547 }
548 names
549}
550
551fn warn_inter_file_overrides(rules: &PartialRulesConfig, files: &[String]) {
552 let ineffective = ineffective_inter_file_override_rules(rules);
553 if ineffective.contains(&"duplicate-exports")
554 && record_inter_file_warn_seen("duplicate-exports", files)
555 {
556 let files = files.join(", ");
557 tracing::warn!(
558 "overrides.rules.duplicate-exports has no effect for files matching [{files}]: duplicate-exports is an inter-file rule. Use top-level `ignoreExports` to exclude these files from duplicate-export grouping."
559 );
560 }
561 if ineffective.contains(&"re-export-cycle")
562 && record_inter_file_warn_seen("re-export-cycle", files)
563 {
564 let files = files.join(", ");
565 tracing::warn!(
566 "overrides.rules.re-export-cycle has no effect for files matching [{files}]: re-export-cycle is an inter-file rule (the cycle spans multiple barrels). Use a file-level `// fallow-ignore-file re-export-cycle` comment in one participating file instead, or set `rules.re-export-cycle: off` at the top level."
567 );
568 }
569}
570
571#[expect(
572 clippy::expect_used,
573 reason = "override glob patterns are validated before config resolution"
574)]
575fn compile_overrides(overrides: Vec<ConfigOverride>) -> Vec<ResolvedOverride> {
576 overrides
577 .into_iter()
578 .filter_map(|override_entry| {
579 warn_inter_file_overrides(&override_entry.rules, &override_entry.files);
580 let matchers: Vec<globset::GlobMatcher> = override_entry
581 .files
582 .iter()
583 .map(|pattern| {
584 Glob::new(pattern)
585 .expect("overrides[].files pattern was validated at config load time")
586 .compile_matcher()
587 })
588 .collect();
589 if matchers.is_empty() {
590 None
591 } else {
592 Some(ResolvedOverride {
593 matchers,
594 rules: override_entry.rules,
595 })
596 }
597 })
598 .collect()
599}
600
601#[expect(
603 clippy::expect_used,
604 reason = "user glob patterns are validated before config resolution"
605)]
606fn compile_ignore_export_rules(rules: &[IgnoreExportRule]) -> Vec<CompiledIgnoreExportRule> {
607 rules
608 .iter()
609 .map(|rule| CompiledIgnoreExportRule {
610 matcher: Glob::new(&rule.file)
611 .expect("ignoreExports[].file was validated at config load time")
612 .compile_matcher(),
613 exports: rule.exports.clone(),
614 })
615 .collect()
616}
617
618#[expect(
620 clippy::expect_used,
621 reason = "user glob patterns are validated before config resolution"
622)]
623fn compile_ignore_catalog_reference_rules(
624 rules: &[IgnoreCatalogReferenceRule],
625) -> Vec<CompiledIgnoreCatalogReferenceRule> {
626 rules
627 .iter()
628 .map(|rule| CompiledIgnoreCatalogReferenceRule {
629 package: rule.package.clone(),
630 catalog: rule.catalog.clone(),
631 consumer_matcher: rule.consumer.as_ref().map(|pattern| {
632 Glob::new(pattern)
633 .expect("ignoreCatalogReferences[].consumer was validated at config load time")
634 .compile_matcher()
635 }),
636 })
637 .collect()
638}
639
640fn compile_ignore_dependency_override_rules(
642 rules: &[IgnoreDependencyOverrideRule],
643) -> Vec<CompiledIgnoreDependencyOverrideRule> {
644 rules
645 .iter()
646 .map(|rule| CompiledIgnoreDependencyOverrideRule {
647 package: rule.package.clone(),
648 source: rule.source.clone(),
649 })
650 .collect()
651}
652
653struct CompiledIgnoreSettings {
654 patterns: GlobSet,
655 user_pattern_count: usize,
656 findings: FindingIgnoreMatcher,
657 unresolved_imports: Vec<GlobMatcher>,
658 exports: Vec<CompiledIgnoreExportRule>,
659 catalog_references: Vec<CompiledIgnoreCatalogReferenceRule>,
660 dependency_overrides: Vec<CompiledIgnoreDependencyOverrideRule>,
661}
662
663fn compile_ignore_settings(config: &FallowConfig) -> CompiledIgnoreSettings {
664 CompiledIgnoreSettings {
665 patterns: compile_ignore_patterns(&config.ignore_patterns),
666 user_pattern_count: config.ignore_patterns.len(),
667 findings: FindingIgnoreMatcher::compile(&config.ignore_findings),
668 unresolved_imports: compile_ignore_unresolved_imports(&config.ignore_unresolved_imports),
669 exports: compile_ignore_export_rules(&config.ignore_exports),
670 catalog_references: compile_ignore_catalog_reference_rules(
671 &config.ignore_catalog_references,
672 ),
673 dependency_overrides: compile_ignore_dependency_override_rules(
674 &config.ignore_dependency_overrides,
675 ),
676 }
677}
678
679struct ResolvedPluginSettings {
680 external_plugins: Vec<ExternalPluginDef>,
681 rule_packs: Vec<crate::rule_pack::RulePackDef>,
682 rule_pack_sources: Vec<PathBuf>,
683}
684
685fn resolve_plugin_settings(
686 root: &Path,
687 configured_plugins: &[String],
688 framework: Vec<ExternalPluginDef>,
689 rule_packs: &[String],
690) -> ResolvedPluginSettings {
691 let mut external_plugins = discover_external_plugins(root, configured_plugins);
692 external_plugins.extend(framework);
693
694 let configured_rule_packs = rule_packs;
695 let rule_packs =
696 crate::rule_pack::load_rule_packs(root, configured_rule_packs).unwrap_or_else(|errors| {
697 for error in &errors {
698 tracing::error!("invalid rule pack: {error}");
699 }
700 Vec::new()
701 });
702 let rule_pack_sources = if rule_packs.len() == configured_rule_packs.len() {
703 configured_rule_packs.iter().map(PathBuf::from).collect()
704 } else {
705 Vec::new()
706 };
707
708 ResolvedPluginSettings {
709 external_plugins,
710 rule_packs,
711 rule_pack_sources,
712 }
713}
714
715struct ResolvedCacheSettings {
716 dir: PathBuf,
717 max_size_mb: Option<u32>,
718 config_hash: u64,
719}
720
721struct ResolvedProductionRules {
722 production: bool,
723 rules: RulesConfig,
724}
725
726fn resolve_production_rules(
727 production_config: ProductionConfig,
728 rules: RulesConfig,
729) -> ResolvedProductionRules {
730 let production = production_config.global();
731 ResolvedProductionRules {
732 production,
733 rules: resolve_rules_for_production(rules, production),
734 }
735}
736
737fn resolve_cache_settings(
738 root: &Path,
739 configured_dir: Option<PathBuf>,
740 configured_max_size_mb: Option<u32>,
741 override_max_size_mb: Option<u32>,
742 no_cache: bool,
743 config_hash: impl FnOnce() -> u64,
744) -> ResolvedCacheSettings {
745 ResolvedCacheSettings {
746 dir: resolve_cache_dir(root, configured_dir),
747 max_size_mb: override_max_size_mb.or(configured_max_size_mb),
748 config_hash: if no_cache { 0 } else { config_hash() },
749 }
750}
751
752fn normalize_security_config(security: SecurityConfig) -> SecurityConfig {
753 SecurityConfig {
754 request_receivers: security.normalized_request_receivers(),
755 ..security
756 }
757}
758
759struct ResolvedPathPolicySettings {
760 boundaries: ResolvedBoundaryConfig,
761 overrides: Vec<ResolvedOverride>,
762}
763
764fn resolve_path_policy_settings(
765 boundaries: BoundaryConfig,
766 overrides: Vec<ConfigOverride>,
767 root: &Path,
768) -> ResolvedPathPolicySettings {
769 ResolvedPathPolicySettings {
770 boundaries: resolve_boundaries(boundaries, root),
771 overrides: compile_overrides(overrides),
772 }
773}
774
775fn compile_unused_component_props_ignore(pattern: Option<&str>) -> Option<regex::Regex> {
776 pattern.and_then(|pattern| match regex::Regex::new(pattern) {
777 Ok(re) => Some(re),
778 Err(error) => {
779 tracing::warn!(
780 %error,
781 "ignoring invalid unusedComponentProps.ignorePattern; this config was \
782 not validated through FallowConfig::load"
783 );
784 None
785 }
786 })
787}
788
789impl FallowConfig {
790 #[expect(
792 clippy::too_many_arguments,
793 reason = "public cross-crate API: ResolvedConfig builder whose runtime-override parameters (root, output, threads, no_cache, quiet, cache_max_size_mb) are an established stable signature; bundling them would break callers"
794 )]
795 pub fn resolve(
796 self,
797 root: PathBuf,
798 output: OutputFormat,
799 threads: usize,
800 no_cache: bool,
801 quiet: bool,
802 cache_max_size_mb: Option<u32>,
803 ) -> ResolvedConfig {
804 let compiled_ignores = compile_ignore_settings(&self);
805
806 let production_rules = resolve_production_rules(self.production, self.rules);
807
808 let plugins =
809 resolve_plugin_settings(&root, &self.plugins, self.framework, &self.rule_packs);
810
811 let cache = resolve_cache_settings(
812 &root,
813 self.cache.dir,
814 self.cache.max_size_mb,
815 cache_max_size_mb,
816 no_cache,
817 || cache_config_hash(&plugins.external_plugins, &self.flags),
818 );
819
820 let path_policy = resolve_path_policy_settings(self.boundaries, self.overrides, &root);
821
822 let unused_component_props_ignore = compile_unused_component_props_ignore(
823 self.unused_component_props.ignore_pattern.as_deref(),
824 );
825
826 ResolvedConfig {
827 root,
828 entry_patterns: self.entry,
829 ignore_patterns: compiled_ignores.patterns,
830 user_ignore_pattern_count: compiled_ignores.user_pattern_count,
831 ignore_findings: compiled_ignores.findings,
832 output,
833 cache_dir: cache.dir,
834 threads,
835 no_cache,
836 cache_max_size_mb: cache.max_size_mb,
837 cache_config_hash: cache.config_hash,
838 ignore_dependencies: self.ignore_dependencies,
839 ignore_unresolved_imports: compiled_ignores.unresolved_imports,
840 ignore_export_rules: self.ignore_exports,
841 compiled_ignore_exports: compiled_ignores.exports,
842 compiled_ignore_catalog_references: compiled_ignores.catalog_references,
843 compiled_ignore_dependency_overrides: compiled_ignores.dependency_overrides,
844 ignore_exports_used_in_file: self.ignore_exports_used_in_file,
845 used_class_members: self.used_class_members,
846 ignore_decorators: self.ignore_decorators,
847 unused_component_props_ignore,
848 duplicates: self.duplicates,
849 similar_code: self.similar_code,
850 health: self.health,
851 type_aware: self.type_aware,
852 rules: production_rules.rules,
853 boundaries: path_policy.boundaries,
854 rule_packs: plugins.rule_packs,
855 rule_pack_sources: plugins.rule_pack_sources,
856 production: production_rules.production,
857 quiet,
858 external_plugins: plugins.external_plugins,
859 dynamically_loaded: self.dynamically_loaded,
860 overrides: path_policy.overrides,
861 regression: self.regression,
862 audit: self.audit,
863 codeowners: self.codeowners,
864 public_packages: self.public_packages,
865 flags: self.flags,
866 security: normalize_security_config(self.security),
867 fix: self.fix,
868 resolve: self.resolve,
869 include_entry_exports: self.include_entry_exports,
870 auto_imports: self.auto_imports,
871 fail_on_parse_error: self.fail_on_parse_error,
872 max_file_size_bytes: Some(DEFAULT_MAX_FILE_SIZE_BYTES),
873 analysis_snapshot: AnalysisSnapshot::Current,
874 }
875 }
876}
877
878impl ResolvedConfig {
879 #[must_use]
882 pub fn resolve_rules_for_path(&self, path: &Path) -> RulesConfig {
883 if self.overrides.is_empty() {
884 return self.rules.clone();
885 }
886
887 let relative = path.strip_prefix(&self.root).unwrap_or(path);
888 let relative_str = relative.to_string_lossy();
889
890 let mut rules = self.rules.clone();
891 for override_entry in &self.overrides {
892 let matches = override_entry
893 .matchers
894 .iter()
895 .any(|m| m.is_match(relative_str.as_ref()));
896 if matches {
897 rules.apply_partial(&override_entry.rules);
898 }
899 }
900 rules
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907 use crate::CacheConfig;
908 use crate::config::boundaries::BoundaryConfig;
909 use crate::config::health::HealthConfig;
910
911 #[test]
912 fn cache_config_hash_keys_on_user_flag_patterns() {
913 let builtin = cache_config_hash(&[], &FlagsConfig::default());
914 let with_pattern = |function: &str| FlagsConfig {
915 sdk_patterns: vec![super::super::flags::SdkPattern {
916 function: function.to_string(),
917 name_arg: 0,
918 provider: None,
919 }],
920 ..FlagsConfig::default()
921 };
922
923 assert_eq!(
924 builtin,
925 xxhash_rust::xxh3::Xxh3::new().digest(),
926 "a config without flag patterns keeps the plugin-only hash"
927 );
928 assert_ne!(builtin, cache_config_hash(&[], &with_pattern("isOn")));
929 assert_ne!(
930 cache_config_hash(&[], &with_pattern("isOn")),
931 cache_config_hash(&[], &with_pattern("isOff"))
932 );
933 assert_ne!(
934 builtin,
935 cache_config_hash(
936 &[],
937 &FlagsConfig {
938 config_object_heuristics: true,
939 ..FlagsConfig::default()
940 }
941 )
942 );
943 assert_ne!(
944 builtin,
945 cache_config_hash(
946 &[],
947 &FlagsConfig {
948 env_prefixes: vec!["MYAPP_".to_string()],
949 ..FlagsConfig::default()
950 }
951 )
952 );
953 }
954
955 #[test]
956 fn overrides_deserialize() {
957 let json_str = r#"{
958 "overrides": [{
959 "files": ["*.test.ts"],
960 "rules": {
961 "unused-exports": "off"
962 }
963 }]
964 }"#;
965 let config: FallowConfig = serde_json::from_str(json_str).unwrap();
966 assert_eq!(config.overrides.len(), 1);
967 assert_eq!(config.overrides[0].files, vec!["*.test.ts"]);
968 assert_eq!(
969 config.overrides[0].rules.unused_exports,
970 Some(Severity::Off)
971 );
972 assert_eq!(config.overrides[0].rules.unused_files, None);
973 }
974
975 #[test]
976 fn resolve_rules_for_path_no_overrides() {
977 let config = FallowConfig {
978 schema: None,
979 minimum_version: None,
980 extends: vec![],
981 entry: vec![],
982 ignore_patterns: vec![],
983 ignore_findings: vec![],
984 framework: vec![],
985 workspaces: None,
986 ignore_dependencies: vec![],
987 ignore_unresolved_imports: vec![],
988 ignore_exports: vec![],
989 ignore_catalog_references: vec![],
990 ignore_dependency_overrides: vec![],
991 ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig::default(),
992 used_class_members: vec![],
993 ignore_decorators: vec![],
994 unused_component_props: crate::UnusedComponentPropsConfig::default(),
995 duplicates: DuplicatesConfig::default(),
996 similar_code: SimilarCodeConfig::default(),
997 health: HealthConfig::default(),
998 rules: RulesConfig::default(),
999 boundaries: BoundaryConfig::default(),
1000 production: false.into(),
1001 plugins: vec![],
1002 rule_packs: vec![],
1003 dynamically_loaded: vec![],
1004 overrides: vec![],
1005 regression: None,
1006 type_aware: crate::TypeAwareConfig::default(),
1007 audit: crate::config::AuditConfig::default(),
1008 codeowners: None,
1009 public_packages: vec![],
1010 flags: FlagsConfig::default(),
1011 security: SecurityConfig::default(),
1012 fix: crate::config::FixConfig::default(),
1013 resolve: ResolveConfig::default(),
1014 sealed: false,
1015 include_entry_exports: false,
1016 auto_imports: false,
1017 fail_on_parse_error: false,
1018 cache: CacheConfig::default(),
1019 };
1020 let resolved = config.resolve(
1021 PathBuf::from("/project"),
1022 OutputFormat::Human,
1023 1,
1024 true,
1025 true,
1026 None,
1027 );
1028 let rules = resolved.resolve_rules_for_path(Path::new("/project/src/foo.ts"));
1029 assert_eq!(rules.unused_files, Severity::Error);
1030 }
1031
1032 #[test]
1033 fn resolve_rules_for_path_with_matching_override() {
1034 let config = FallowConfig {
1035 schema: None,
1036 minimum_version: None,
1037 extends: vec![],
1038 entry: vec![],
1039 ignore_patterns: vec![],
1040 ignore_findings: vec![],
1041 framework: vec![],
1042 workspaces: None,
1043 ignore_dependencies: vec![],
1044 ignore_unresolved_imports: vec![],
1045 ignore_exports: vec![],
1046 ignore_catalog_references: vec![],
1047 ignore_dependency_overrides: vec![],
1048 ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig::default(),
1049 used_class_members: vec![],
1050 ignore_decorators: vec![],
1051 unused_component_props: crate::UnusedComponentPropsConfig::default(),
1052 duplicates: DuplicatesConfig::default(),
1053 similar_code: SimilarCodeConfig::default(),
1054 health: HealthConfig::default(),
1055 rules: RulesConfig::default(),
1056 boundaries: BoundaryConfig::default(),
1057 production: false.into(),
1058 plugins: vec![],
1059 rule_packs: vec![],
1060 dynamically_loaded: vec![],
1061 overrides: vec![ConfigOverride {
1062 files: vec!["*.test.ts".to_string()],
1063 rules: PartialRulesConfig {
1064 unused_exports: Some(Severity::Off),
1065 ..Default::default()
1066 },
1067 }],
1068 regression: None,
1069 type_aware: crate::TypeAwareConfig::default(),
1070 audit: crate::config::AuditConfig::default(),
1071 codeowners: None,
1072 public_packages: vec![],
1073 flags: FlagsConfig::default(),
1074 security: SecurityConfig::default(),
1075 fix: crate::config::FixConfig::default(),
1076 resolve: ResolveConfig::default(),
1077 sealed: false,
1078 include_entry_exports: false,
1079 auto_imports: false,
1080 fail_on_parse_error: false,
1081 cache: CacheConfig::default(),
1082 };
1083 let resolved = config.resolve(
1084 PathBuf::from("/project"),
1085 OutputFormat::Human,
1086 1,
1087 true,
1088 true,
1089 None,
1090 );
1091
1092 let test_rules = resolved.resolve_rules_for_path(Path::new("/project/src/utils.test.ts"));
1093 assert_eq!(test_rules.unused_exports, Severity::Off);
1094 assert_eq!(test_rules.unused_files, Severity::Error); let src_rules = resolved.resolve_rules_for_path(Path::new("/project/src/utils.ts"));
1097 assert_eq!(src_rules.unused_exports, Severity::Error);
1098 }
1099
1100 #[test]
1101 fn resolve_rules_for_path_later_override_wins() {
1102 let config = FallowConfig {
1103 schema: None,
1104 minimum_version: None,
1105 extends: vec![],
1106 entry: vec![],
1107 ignore_patterns: vec![],
1108 ignore_findings: vec![],
1109 framework: vec![],
1110 workspaces: None,
1111 ignore_dependencies: vec![],
1112 ignore_unresolved_imports: vec![],
1113 ignore_exports: vec![],
1114 ignore_catalog_references: vec![],
1115 ignore_dependency_overrides: vec![],
1116 ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig::default(),
1117 used_class_members: vec![],
1118 ignore_decorators: vec![],
1119 unused_component_props: crate::UnusedComponentPropsConfig::default(),
1120 duplicates: DuplicatesConfig::default(),
1121 similar_code: SimilarCodeConfig::default(),
1122 health: HealthConfig::default(),
1123 rules: RulesConfig::default(),
1124 boundaries: BoundaryConfig::default(),
1125 production: false.into(),
1126 plugins: vec![],
1127 rule_packs: vec![],
1128 dynamically_loaded: vec![],
1129 overrides: vec![
1130 ConfigOverride {
1131 files: vec!["*.ts".to_string()],
1132 rules: PartialRulesConfig {
1133 unused_files: Some(Severity::Warn),
1134 ..Default::default()
1135 },
1136 },
1137 ConfigOverride {
1138 files: vec!["*.test.ts".to_string()],
1139 rules: PartialRulesConfig {
1140 unused_files: Some(Severity::Off),
1141 ..Default::default()
1142 },
1143 },
1144 ],
1145 regression: None,
1146 type_aware: crate::TypeAwareConfig::default(),
1147 audit: crate::config::AuditConfig::default(),
1148 codeowners: None,
1149 public_packages: vec![],
1150 flags: FlagsConfig::default(),
1151 security: SecurityConfig::default(),
1152 fix: crate::config::FixConfig::default(),
1153 resolve: ResolveConfig::default(),
1154 sealed: false,
1155 include_entry_exports: false,
1156 auto_imports: false,
1157 fail_on_parse_error: false,
1158 cache: CacheConfig::default(),
1159 };
1160 let resolved = config.resolve(
1161 PathBuf::from("/project"),
1162 OutputFormat::Human,
1163 1,
1164 true,
1165 true,
1166 None,
1167 );
1168
1169 let rules = resolved.resolve_rules_for_path(Path::new("/project/foo.test.ts"));
1170 assert_eq!(rules.unused_files, Severity::Off);
1171
1172 let rules2 = resolved.resolve_rules_for_path(Path::new("/project/foo.ts"));
1173 assert_eq!(rules2.unused_files, Severity::Warn);
1174 }
1175
1176 #[test]
1177 fn resolve_keeps_inter_file_rule_override_after_warning() {
1178 let config = FallowConfig {
1179 schema: None,
1180 minimum_version: None,
1181 extends: vec![],
1182 entry: vec![],
1183 ignore_patterns: vec![],
1184 ignore_findings: vec![],
1185 framework: vec![],
1186 workspaces: None,
1187 ignore_dependencies: vec![],
1188 ignore_unresolved_imports: vec![],
1189 ignore_exports: vec![],
1190 ignore_catalog_references: vec![],
1191 ignore_dependency_overrides: vec![],
1192 ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig::default(),
1193 used_class_members: vec![],
1194 ignore_decorators: vec![],
1195 unused_component_props: crate::UnusedComponentPropsConfig::default(),
1196 duplicates: DuplicatesConfig::default(),
1197 similar_code: SimilarCodeConfig::default(),
1198 health: HealthConfig::default(),
1199 rules: RulesConfig::default(),
1200 boundaries: BoundaryConfig::default(),
1201 production: false.into(),
1202 plugins: vec![],
1203 rule_packs: vec![],
1204 dynamically_loaded: vec![],
1205 overrides: vec![ConfigOverride {
1206 files: vec!["**/ui/**".to_string()],
1207 rules: PartialRulesConfig {
1208 duplicate_exports: Some(Severity::Off),
1209 unused_files: Some(Severity::Warn),
1210 ..Default::default()
1211 },
1212 }],
1213 regression: None,
1214 type_aware: crate::TypeAwareConfig::default(),
1215 audit: crate::config::AuditConfig::default(),
1216 codeowners: None,
1217 public_packages: vec![],
1218 flags: FlagsConfig::default(),
1219 security: SecurityConfig::default(),
1220 fix: crate::config::FixConfig::default(),
1221 resolve: ResolveConfig::default(),
1222 sealed: false,
1223 include_entry_exports: false,
1224 auto_imports: false,
1225 fail_on_parse_error: false,
1226 cache: CacheConfig::default(),
1227 };
1228 let resolved = config.resolve(
1229 PathBuf::from("/project"),
1230 OutputFormat::Human,
1231 1,
1232 true,
1233 true,
1234 None,
1235 );
1236 assert_eq!(
1237 resolved.overrides.len(),
1238 1,
1239 "inter-file rule warning must not drop the override; co-located non-inter-file rules still apply"
1240 );
1241 let rules = resolved.resolve_rules_for_path(Path::new("/project/ui/dialog.ts"));
1242 assert_eq!(rules.unused_files, Severity::Warn);
1243 }
1244
1245 #[test]
1246 fn circular_dependency_override_is_not_reported_as_ineffective() {
1247 let rules = PartialRulesConfig {
1248 circular_dependencies: Some(Severity::Off),
1249 duplicate_exports: Some(Severity::Off),
1250 re_export_cycle: Some(Severity::Off),
1251 ..PartialRulesConfig::default()
1252 };
1253 assert_eq!(
1254 ineffective_inter_file_override_rules(&rules),
1255 vec!["duplicate-exports", "re-export-cycle"]
1256 );
1257
1258 let only_circular = PartialRulesConfig {
1259 circular_dependencies: Some(Severity::Off),
1260 ..PartialRulesConfig::default()
1261 };
1262 assert!(ineffective_inter_file_override_rules(&only_circular).is_empty());
1263 }
1264
1265 #[test]
1266 fn inter_file_warn_dedup_returns_true_only_on_first_key_match() {
1267 reset_inter_file_warn_dedup_for_test();
1268 let files_a = vec!["__test_dedup_a/*".to_string()];
1269 let files_b = vec!["__test_dedup_b/*".to_string()];
1270
1271 assert!(record_inter_file_warn_seen("duplicate-exports", &files_a));
1272 assert!(!record_inter_file_warn_seen("duplicate-exports", &files_a));
1273 assert!(!record_inter_file_warn_seen("duplicate-exports", &files_a));
1274
1275 assert!(record_inter_file_warn_seen("circular-dependency", &files_a));
1276 assert!(!record_inter_file_warn_seen(
1277 "circular-dependency",
1278 &files_a
1279 ));
1280
1281 assert!(record_inter_file_warn_seen("duplicate-exports", &files_b));
1282
1283 let files_reordered = vec![
1284 "__test_dedup_b/*".to_string(),
1285 "__test_dedup_a/*".to_string(),
1286 ];
1287 let files_natural = vec![
1288 "__test_dedup_a/*".to_string(),
1289 "__test_dedup_b/*".to_string(),
1290 ];
1291 reset_inter_file_warn_dedup_for_test();
1292 assert!(record_inter_file_warn_seen(
1293 "duplicate-exports",
1294 &files_natural
1295 ));
1296 assert!(!record_inter_file_warn_seen(
1297 "duplicate-exports",
1298 &files_reordered
1299 ));
1300 }
1301
1302 #[test]
1303 fn resolve_called_n_times_dedupes_inter_file_warning_to_one() {
1304 reset_inter_file_warn_dedup_for_test();
1305 let files = vec!["__test_resolve_dedup/**".to_string()];
1306 let build_config = || FallowConfig {
1307 schema: None,
1308 minimum_version: None,
1309 extends: vec![],
1310 entry: vec![],
1311 ignore_patterns: vec![],
1312 ignore_findings: vec![],
1313 framework: vec![],
1314 workspaces: None,
1315 ignore_dependencies: vec![],
1316 ignore_unresolved_imports: vec![],
1317 ignore_exports: vec![],
1318 ignore_catalog_references: vec![],
1319 ignore_dependency_overrides: vec![],
1320 ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig::default(),
1321 used_class_members: vec![],
1322 ignore_decorators: vec![],
1323 unused_component_props: crate::UnusedComponentPropsConfig::default(),
1324 duplicates: DuplicatesConfig::default(),
1325 similar_code: SimilarCodeConfig::default(),
1326 health: HealthConfig::default(),
1327 rules: RulesConfig::default(),
1328 boundaries: BoundaryConfig::default(),
1329 production: false.into(),
1330 plugins: vec![],
1331 rule_packs: vec![],
1332 dynamically_loaded: vec![],
1333 overrides: vec![ConfigOverride {
1334 files: files.clone(),
1335 rules: PartialRulesConfig {
1336 duplicate_exports: Some(Severity::Off),
1337 ..Default::default()
1338 },
1339 }],
1340 regression: None,
1341 type_aware: crate::TypeAwareConfig::default(),
1342 audit: crate::config::AuditConfig::default(),
1343 codeowners: None,
1344 public_packages: vec![],
1345 flags: FlagsConfig::default(),
1346 security: SecurityConfig::default(),
1347 fix: crate::config::FixConfig::default(),
1348 resolve: ResolveConfig::default(),
1349 sealed: false,
1350 include_entry_exports: false,
1351 auto_imports: false,
1352 fail_on_parse_error: false,
1353 cache: CacheConfig::default(),
1354 };
1355 for _ in 0..10 {
1356 let _ = build_config().resolve(
1357 PathBuf::from("/project"),
1358 OutputFormat::Human,
1359 1,
1360 true,
1361 true,
1362 None,
1363 );
1364 }
1365 assert!(
1366 !record_inter_file_warn_seen("duplicate-exports", &files),
1367 "warn key for duplicate-exports + __test_resolve_dedup/** should be marked after the first resolve"
1368 );
1369 }
1370
1371 fn make_config(production: bool) -> FallowConfig {
1373 FallowConfig {
1374 schema: None,
1375 minimum_version: None,
1376 extends: vec![],
1377 entry: vec![],
1378 ignore_patterns: vec![],
1379 ignore_findings: vec![],
1380 framework: vec![],
1381 workspaces: None,
1382 ignore_dependencies: vec![],
1383 ignore_unresolved_imports: vec![],
1384 ignore_exports: vec![],
1385 ignore_catalog_references: vec![],
1386 ignore_dependency_overrides: vec![],
1387 ignore_exports_used_in_file: IgnoreExportsUsedInFileConfig::default(),
1388 used_class_members: vec![],
1389 ignore_decorators: vec![],
1390 unused_component_props: crate::UnusedComponentPropsConfig::default(),
1391 duplicates: DuplicatesConfig::default(),
1392 similar_code: SimilarCodeConfig::default(),
1393 health: HealthConfig::default(),
1394 rules: RulesConfig::default(),
1395 boundaries: BoundaryConfig::default(),
1396 production: production.into(),
1397 plugins: vec![],
1398 rule_packs: vec![],
1399 dynamically_loaded: vec![],
1400 overrides: vec![],
1401 regression: None,
1402 type_aware: crate::TypeAwareConfig::default(),
1403 audit: crate::config::AuditConfig::default(),
1404 codeowners: None,
1405 public_packages: vec![],
1406 flags: FlagsConfig::default(),
1407 security: SecurityConfig::default(),
1408 fix: crate::config::FixConfig::default(),
1409 resolve: ResolveConfig::default(),
1410 sealed: false,
1411 include_entry_exports: false,
1412 auto_imports: false,
1413 fail_on_parse_error: false,
1414 cache: CacheConfig::default(),
1415 }
1416 }
1417
1418 #[test]
1419 fn resolve_tracks_rule_pack_sources_in_config_order() {
1420 let dir = tempfile::tempdir().unwrap();
1421 std::fs::create_dir_all(dir.path().join("rule-packs")).unwrap();
1422 std::fs::write(
1423 dir.path().join("rule-packs/team-policy.jsonc"),
1424 r#"{
1425 "version": 1,
1426 "name": "team-policy",
1427 "rules": [
1428 {
1429 "id": "no-moment",
1430 "kind": "banned-import",
1431 "specifiers": ["moment"]
1432 }
1433 ]
1434}
1435"#,
1436 )
1437 .unwrap();
1438
1439 let mut config = make_config(false);
1440 config.rule_packs = vec!["rule-packs/team-policy.jsonc".to_string()];
1441
1442 let resolved = config.resolve(
1443 dir.path().to_path_buf(),
1444 OutputFormat::Human,
1445 1,
1446 true,
1447 true,
1448 None,
1449 );
1450
1451 assert_eq!(resolved.rule_packs.len(), 1);
1452 assert_eq!(resolved.rule_packs[0].name, "team-policy");
1453 assert_eq!(
1454 resolved.rule_pack_sources,
1455 vec![PathBuf::from("rule-packs/team-policy.jsonc")]
1456 );
1457 }
1458
1459 #[test]
1460 fn resolve_production_forces_dev_deps_off() {
1461 let resolved = make_config(true).resolve(
1462 PathBuf::from("/project"),
1463 OutputFormat::Human,
1464 1,
1465 true,
1466 true,
1467 None,
1468 );
1469 assert_eq!(
1470 resolved.rules.unused_dev_dependencies,
1471 Severity::Off,
1472 "production mode should force unused_dev_dependencies to off"
1473 );
1474 }
1475
1476 #[test]
1477 fn resolve_production_forces_optional_deps_off() {
1478 let resolved = make_config(true).resolve(
1479 PathBuf::from("/project"),
1480 OutputFormat::Human,
1481 1,
1482 true,
1483 true,
1484 None,
1485 );
1486 assert_eq!(
1487 resolved.rules.unused_optional_dependencies,
1488 Severity::Off,
1489 "production mode should force unused_optional_dependencies to off"
1490 );
1491 }
1492
1493 #[test]
1494 fn resolve_production_preserves_other_rules() {
1495 let resolved = make_config(true).resolve(
1496 PathBuf::from("/project"),
1497 OutputFormat::Human,
1498 1,
1499 true,
1500 true,
1501 None,
1502 );
1503 assert_eq!(resolved.rules.unused_files, Severity::Error);
1504 assert_eq!(resolved.rules.unused_exports, Severity::Error);
1505 assert_eq!(resolved.rules.unused_dependencies, Severity::Error);
1506 }
1507
1508 #[test]
1509 fn resolve_non_production_keeps_dev_deps_default() {
1510 let resolved = make_config(false).resolve(
1511 PathBuf::from("/project"),
1512 OutputFormat::Human,
1513 1,
1514 true,
1515 true,
1516 None,
1517 );
1518 assert_eq!(
1519 resolved.rules.unused_dev_dependencies,
1520 Severity::Warn,
1521 "non-production should keep default severity"
1522 );
1523 assert_eq!(resolved.rules.unused_optional_dependencies, Severity::Warn);
1524 }
1525
1526 #[test]
1527 fn resolve_production_flag_stored() {
1528 let resolved = make_config(true).resolve(
1529 PathBuf::from("/project"),
1530 OutputFormat::Human,
1531 1,
1532 true,
1533 true,
1534 None,
1535 );
1536 assert!(resolved.production);
1537
1538 let resolved2 = make_config(false).resolve(
1539 PathBuf::from("/project"),
1540 OutputFormat::Human,
1541 1,
1542 true,
1543 true,
1544 None,
1545 );
1546 assert!(!resolved2.production);
1547 }
1548
1549 #[test]
1550 fn resolve_default_ignores_node_modules() {
1551 let resolved = make_config(false).resolve(
1552 PathBuf::from("/project"),
1553 OutputFormat::Human,
1554 1,
1555 true,
1556 true,
1557 None,
1558 );
1559 assert!(
1560 resolved
1561 .ignore_patterns
1562 .is_match("node_modules/lodash/index.js")
1563 );
1564 assert!(
1565 resolved
1566 .ignore_patterns
1567 .is_match("packages/a/node_modules/react/index.js")
1568 );
1569 }
1570
1571 #[test]
1572 fn resolve_default_ignores_dist() {
1573 let resolved = make_config(false).resolve(
1574 PathBuf::from("/project"),
1575 OutputFormat::Human,
1576 1,
1577 true,
1578 true,
1579 None,
1580 );
1581 assert!(resolved.ignore_patterns.is_match("dist/bundle.js"));
1582 assert!(
1583 resolved
1584 .ignore_patterns
1585 .is_match("packages/ui/dist/index.js")
1586 );
1587 }
1588
1589 #[test]
1594 fn the_compiled_ignore_union_puts_user_patterns_first_and_the_built_ins_in_order() {
1595 let mut config = make_config(false);
1596 config.ignore_patterns = vec!["vendor/**".to_owned(), "legacy/**".to_owned()];
1597 let resolved = config.resolve(
1598 PathBuf::from("/project"),
1599 OutputFormat::Human,
1600 1,
1601 true,
1602 true,
1603 None,
1604 );
1605
1606 assert_eq!(resolved.user_ignore_pattern_count, 2);
1607 assert_eq!(
1608 resolved.ignore_patterns.len(),
1609 2 + DEFAULT_IGNORE_PATTERNS.len(),
1610 "the union only ever adds"
1611 );
1612 assert_eq!(
1613 resolved.ignore_patterns.matches("vendor/a.ts"),
1614 vec![0],
1615 "a user pattern keeps its configured index"
1616 );
1617 for (offset, pattern) in DEFAULT_IGNORE_PATTERNS.iter().enumerate() {
1618 let sample = match *pattern {
1619 "**/node_modules/**" => "node_modules/react/index.js",
1620 "**/dist/**" => "dist/a.ts",
1621 "**/build/**" => "build/a.ts",
1622 "**/.git/**" => ".git/hooks/a.js",
1623 "**/coverage/**" => "coverage/a.ts",
1624 "**/*.min.js" => "a.min.js",
1625 "**/*.min.mjs" => "a.min.mjs",
1626 "**/*.min.cjs" => "a.min.cjs",
1627 "**/*.bundle.js" => "a.bundle.js",
1628 other => panic!("no sample path for built-in ignore {other}"),
1629 };
1630 assert_eq!(
1631 resolved.ignore_patterns.matches(sample),
1632 vec![resolved.user_ignore_pattern_count + offset],
1633 "{pattern} must sit at its DEFAULT_IGNORE_PATTERNS offset"
1634 );
1635 }
1636 }
1637
1638 #[test]
1642 fn a_user_pattern_that_overlaps_a_built_in_matches_at_the_lower_index() {
1643 let mut config = make_config(false);
1644 config.ignore_patterns = vec!["dist/**".to_owned()];
1645 let resolved = config.resolve(
1646 PathBuf::from("/project"),
1647 OutputFormat::Human,
1648 1,
1649 true,
1650 true,
1651 None,
1652 );
1653
1654 let matches = resolved.ignore_patterns.matches("dist/a.ts");
1655 assert_eq!(matches.first(), Some(&0));
1656 assert!(matches.len() > 1, "the built-in still matches too");
1657 }
1658
1659 #[test]
1660 fn resolve_default_ignores_build_at_any_depth() {
1661 let resolved = make_config(false).resolve(
1662 PathBuf::from("/project"),
1663 OutputFormat::Human,
1664 1,
1665 true,
1666 true,
1667 None,
1668 );
1669 assert!(
1670 resolved.ignore_patterns.is_match("build/output.js"),
1671 "root build/ should be ignored"
1672 );
1673 assert!(
1674 resolved.ignore_patterns.is_match("src/build/helper.ts"),
1675 "nested build/ should be ignored, like dist/ and coverage/"
1676 );
1677 assert!(
1678 resolved
1679 .ignore_patterns
1680 .is_match("projects/app/build/index.js"),
1681 "build/ inside a workspace package should be ignored"
1682 );
1683 }
1684
1685 #[test]
1686 fn resolve_default_ignores_match_build_only_as_a_whole_segment() {
1687 let resolved = make_config(false).resolve(
1688 PathBuf::from("/project"),
1689 OutputFormat::Human,
1690 1,
1691 true,
1692 true,
1693 None,
1694 );
1695 assert!(!resolved.ignore_patterns.is_match("src/build.ts"));
1696 assert!(!resolved.ignore_patterns.is_match("src/rebuild/helper.ts"));
1697 assert!(!resolved.ignore_patterns.is_match("src/buildings/a.ts"));
1698 assert!(!resolved.ignore_patterns.is_match("src/prebuild/a.ts"));
1699 }
1700
1701 #[test]
1710 fn resolve_default_ignores_cover_a_workspace_package_named_build() {
1711 let resolved = make_config(false).resolve(
1712 PathBuf::from("/project"),
1713 OutputFormat::Human,
1714 1,
1715 true,
1716 true,
1717 None,
1718 );
1719 assert!(
1720 resolved
1721 .ignore_patterns
1722 .is_match("packages/build/package.json"),
1723 "the manifest stops contributing unused-dependency findings"
1724 );
1725 assert!(
1726 resolved
1727 .ignore_patterns
1728 .is_match("packages/build/src/index.ts"),
1729 "the package's source stops being analyzed entirely"
1730 );
1731 assert!(
1732 resolved
1733 .ignore_patterns
1734 .is_match("packages/build/src/nested/deep.ts"),
1735 "including source below the package's own subdirectories"
1736 );
1737 }
1738
1739 #[test]
1744 fn resolve_default_ignores_cover_a_framework_config_inside_build() {
1745 let resolved = make_config(false).resolve(
1746 PathBuf::from("/project"),
1747 OutputFormat::Human,
1748 1,
1749 true,
1750 true,
1751 None,
1752 );
1753 assert!(
1754 resolved
1755 .ignore_patterns
1756 .is_match("app/build/webpack.config.js")
1757 );
1758 }
1759
1760 #[test]
1761 fn resolve_default_ignores_minified_files() {
1762 let resolved = make_config(false).resolve(
1763 PathBuf::from("/project"),
1764 OutputFormat::Human,
1765 1,
1766 true,
1767 true,
1768 None,
1769 );
1770 assert!(resolved.ignore_patterns.is_match("vendor/jquery.min.js"));
1771 assert!(resolved.ignore_patterns.is_match("lib/utils.min.mjs"));
1772 assert!(resolved.ignore_patterns.is_match("lib/legacy.min.cjs"));
1773 assert!(resolved.ignore_patterns.is_match("public/app.bundle.js"));
1774 assert!(
1775 resolved
1776 .ignore_patterns
1777 .is_match("src/vendor/app.bundle.js")
1778 );
1779 assert!(!resolved.ignore_patterns.is_match("src/bundle.ts"));
1781 assert!(!resolved.ignore_patterns.is_match("src/app.cjs"));
1782 }
1783
1784 #[test]
1785 fn resolve_max_file_size_bytes_default_and_unlimited() {
1786 assert_eq!(
1788 resolve_max_file_size_bytes(None),
1789 Some(DEFAULT_MAX_FILE_SIZE_BYTES)
1790 );
1791 assert_eq!(resolve_max_file_size_bytes(Some(0)), None);
1793 assert_eq!(resolve_max_file_size_bytes(Some(2)), Some(2 * 1024 * 1024));
1795 assert_eq!(DEFAULT_MAX_FILE_SIZE_MB, 5);
1796 }
1797
1798 #[test]
1799 fn resolve_sets_default_max_file_size() {
1800 let resolved = make_config(false).resolve(
1801 PathBuf::from("/project"),
1802 OutputFormat::Human,
1803 1,
1804 true,
1805 true,
1806 None,
1807 );
1808 assert_eq!(
1809 resolved.max_file_size_bytes,
1810 Some(DEFAULT_MAX_FILE_SIZE_BYTES)
1811 );
1812 }
1813
1814 #[test]
1815 fn resolve_default_ignores_git() {
1816 let resolved = make_config(false).resolve(
1817 PathBuf::from("/project"),
1818 OutputFormat::Human,
1819 1,
1820 true,
1821 true,
1822 None,
1823 );
1824 assert!(resolved.ignore_patterns.is_match(".git/objects/ab/123.js"));
1825 }
1826
1827 #[test]
1828 fn resolve_default_ignores_coverage() {
1829 let resolved = make_config(false).resolve(
1830 PathBuf::from("/project"),
1831 OutputFormat::Human,
1832 1,
1833 true,
1834 true,
1835 None,
1836 );
1837 assert!(
1838 resolved
1839 .ignore_patterns
1840 .is_match("coverage/lcov-report/index.js")
1841 );
1842 }
1843
1844 #[test]
1845 fn resolve_source_files_not_ignored_by_default() {
1846 let resolved = make_config(false).resolve(
1847 PathBuf::from("/project"),
1848 OutputFormat::Human,
1849 1,
1850 true,
1851 true,
1852 None,
1853 );
1854 assert!(!resolved.ignore_patterns.is_match("src/index.ts"));
1855 assert!(
1856 !resolved
1857 .ignore_patterns
1858 .is_match("src/components/Button.tsx")
1859 );
1860 assert!(!resolved.ignore_patterns.is_match("lib/utils.js"));
1861 }
1862
1863 #[test]
1864 fn resolve_custom_ignore_patterns_merged_with_defaults() {
1865 let mut config = make_config(false);
1866 config.ignore_patterns = vec!["**/__generated__/**".to_string()];
1867 let resolved = config.resolve(
1868 PathBuf::from("/project"),
1869 OutputFormat::Human,
1870 1,
1871 true,
1872 true,
1873 None,
1874 );
1875 assert!(
1876 resolved
1877 .ignore_patterns
1878 .is_match("src/__generated__/types.ts")
1879 );
1880 assert!(resolved.ignore_patterns.is_match("node_modules/foo/bar.js"));
1881 }
1882
1883 #[test]
1884 fn resolve_normalizes_leading_dot_ignore_patterns() {
1885 let mut config = make_config(false);
1886 config.ignore_patterns = vec!["./src/generated/**".to_string()];
1887 let resolved = config.resolve(
1888 PathBuf::from("/project"),
1889 OutputFormat::Human,
1890 1,
1891 true,
1892 true,
1893 None,
1894 );
1895
1896 assert!(resolved.ignore_patterns.is_match("src/generated/client.ts"));
1897 assert!(
1898 !resolved
1899 .ignore_patterns
1900 .is_match("./src/generated/client.ts")
1901 );
1902 }
1903
1904 #[test]
1905 fn resolve_normalizes_leading_dot_ignore_unresolved_imports() {
1906 let mut config = make_config(false);
1907 config.ignore_unresolved_imports = vec!["./src/generated/**".to_string()];
1908 let resolved = config.resolve(
1909 PathBuf::from("/project"),
1910 OutputFormat::Human,
1911 1,
1912 true,
1913 true,
1914 None,
1915 );
1916
1917 assert!(
1918 resolved
1919 .ignore_unresolved_imports
1920 .iter()
1921 .any(|matcher| matcher.is_match("src/generated/client"))
1922 );
1923 assert!(
1924 !resolved
1925 .ignore_unresolved_imports
1926 .iter()
1927 .any(|matcher| matcher.is_match("./src/generated/client"))
1928 );
1929 }
1930
1931 #[test]
1932 fn resolve_passes_through_entry_patterns() {
1933 let mut config = make_config(false);
1934 config.entry = vec!["src/**/*.ts".to_string(), "lib/**/*.js".to_string()];
1935 let resolved = config.resolve(
1936 PathBuf::from("/project"),
1937 OutputFormat::Human,
1938 1,
1939 true,
1940 true,
1941 None,
1942 );
1943 assert_eq!(resolved.entry_patterns, vec!["src/**/*.ts", "lib/**/*.js"]);
1944 }
1945
1946 #[test]
1947 fn resolve_passes_through_ignore_dependencies() {
1948 let mut config = make_config(false);
1949 config.ignore_dependencies = vec!["postcss".to_string(), "autoprefixer".to_string()];
1950 let resolved = config.resolve(
1951 PathBuf::from("/project"),
1952 OutputFormat::Human,
1953 1,
1954 true,
1955 true,
1956 None,
1957 );
1958 assert_eq!(
1959 resolved.ignore_dependencies,
1960 vec!["postcss", "autoprefixer"]
1961 );
1962 }
1963
1964 #[test]
1965 fn resolve_compiles_ignore_unresolved_imports_as_raw_specifier_globs() {
1966 let mut config = make_config(false);
1967 config.ignore_unresolved_imports = vec![
1968 "@example/icons".to_string(),
1969 "@example/icons/**".to_string(),
1970 "../generated/**".to_string(),
1971 ];
1972 let resolved = config.resolve(
1973 PathBuf::from("/project"),
1974 OutputFormat::Human,
1975 1,
1976 true,
1977 true,
1978 None,
1979 );
1980
1981 assert!(
1982 resolved
1983 .ignore_unresolved_imports
1984 .iter()
1985 .any(|matcher| matcher.is_match("@example/icons"))
1986 );
1987 assert!(
1988 resolved
1989 .ignore_unresolved_imports
1990 .iter()
1991 .any(|matcher| matcher.is_match("@example/icons/metadata"))
1992 );
1993 assert!(
1994 resolved
1995 .ignore_unresolved_imports
1996 .iter()
1997 .any(|matcher| matcher.is_match("../generated/client"))
1998 );
1999 }
2000
2001 #[test]
2002 fn ignore_unresolved_imports_subpath_glob_does_not_match_bare_specifier() {
2003 let mut config = make_config(false);
2004 config.ignore_unresolved_imports = vec!["@example/icons/**".to_string()];
2005 let resolved = config.resolve(
2006 PathBuf::from("/project"),
2007 OutputFormat::Human,
2008 1,
2009 true,
2010 true,
2011 None,
2012 );
2013
2014 assert!(
2015 !resolved.ignore_unresolved_imports[0].is_match("@example/icons"),
2016 "globset treats @example/icons/** as subpaths only; list the bare specifier separately"
2017 );
2018 assert!(resolved.ignore_unresolved_imports[0].is_match("@example/icons/metadata"));
2019 }
2020
2021 #[test]
2022 fn resolve_sets_cache_dir() {
2023 let resolved = make_config(false).resolve(
2024 PathBuf::from("/my/project"),
2025 OutputFormat::Human,
2026 1,
2027 true,
2028 true,
2029 None,
2030 );
2031 assert_eq!(resolved.cache_dir, PathBuf::from("/my/project/.fallow"));
2032 }
2033
2034 #[test]
2035 fn resolve_uses_relative_configured_cache_dir_from_root() {
2036 let config = FallowConfig {
2037 cache: crate::CacheConfig {
2038 dir: Some(PathBuf::from(".cache/fallow")),
2039 ..Default::default()
2040 },
2041 ..make_config(false)
2042 };
2043 let resolved = config.resolve(
2044 PathBuf::from("/my/project"),
2045 OutputFormat::Human,
2046 1,
2047 false,
2048 true,
2049 None,
2050 );
2051 assert_eq!(
2052 resolved.cache_dir,
2053 PathBuf::from("/my/project/.cache/fallow")
2054 );
2055 }
2056
2057 #[test]
2058 fn resolve_keeps_absolute_configured_cache_dir() {
2059 let config = FallowConfig {
2060 cache: crate::CacheConfig {
2061 dir: Some(PathBuf::from("/tmp/fallow-cache")),
2062 ..Default::default()
2063 },
2064 ..make_config(false)
2065 };
2066 let resolved = config.resolve(
2067 PathBuf::from("/my/project"),
2068 OutputFormat::Human,
2069 1,
2070 false,
2071 true,
2072 None,
2073 );
2074 assert_eq!(resolved.cache_dir, PathBuf::from("/tmp/fallow-cache"));
2075 }
2076
2077 #[test]
2078 fn resolve_passes_through_thread_count() {
2079 let resolved = make_config(false).resolve(
2080 PathBuf::from("/project"),
2081 OutputFormat::Human,
2082 8,
2083 true,
2084 true,
2085 None,
2086 );
2087 assert_eq!(resolved.threads, 8);
2088 }
2089
2090 #[test]
2091 fn resolve_passes_through_quiet_flag() {
2092 let resolved = make_config(false).resolve(
2093 PathBuf::from("/project"),
2094 OutputFormat::Human,
2095 1,
2096 true,
2097 false,
2098 None,
2099 );
2100 assert!(!resolved.quiet);
2101
2102 let resolved2 = make_config(false).resolve(
2103 PathBuf::from("/project"),
2104 OutputFormat::Human,
2105 1,
2106 true,
2107 true,
2108 None,
2109 );
2110 assert!(resolved2.quiet);
2111 }
2112
2113 #[test]
2114 fn resolve_passes_through_no_cache_flag() {
2115 let resolved_no_cache = make_config(false).resolve(
2116 PathBuf::from("/project"),
2117 OutputFormat::Human,
2118 1,
2119 true,
2120 true,
2121 None,
2122 );
2123 assert!(resolved_no_cache.no_cache);
2124
2125 let resolved_with_cache = make_config(false).resolve(
2126 PathBuf::from("/project"),
2127 OutputFormat::Human,
2128 1,
2129 false,
2130 true,
2131 None,
2132 );
2133 assert!(!resolved_with_cache.no_cache);
2134 }
2135
2136 #[test]
2137 #[should_panic(expected = "validated at config load time")]
2138 fn resolve_panics_on_unvalidated_invalid_override_glob() {
2139 let mut config = make_config(false);
2140 config.overrides = vec![ConfigOverride {
2141 files: vec!["[invalid".to_string()],
2142 rules: PartialRulesConfig {
2143 unused_files: Some(Severity::Off),
2144 ..Default::default()
2145 },
2146 }];
2147 let _ = config.resolve(
2148 PathBuf::from("/project"),
2149 OutputFormat::Human,
2150 1,
2151 true,
2152 true,
2153 None,
2154 );
2155 }
2156
2157 #[test]
2158 fn resolve_override_with_empty_files_skipped() {
2159 let mut config = make_config(false);
2160 config.overrides = vec![ConfigOverride {
2161 files: vec![],
2162 rules: PartialRulesConfig {
2163 unused_files: Some(Severity::Off),
2164 ..Default::default()
2165 },
2166 }];
2167 let resolved = config.resolve(
2168 PathBuf::from("/project"),
2169 OutputFormat::Human,
2170 1,
2171 true,
2172 true,
2173 None,
2174 );
2175 assert!(
2176 resolved.overrides.is_empty(),
2177 "override with no file patterns should be skipped"
2178 );
2179 }
2180
2181 #[test]
2182 fn resolve_multiple_valid_overrides() {
2183 let mut config = make_config(false);
2184 config.overrides = vec![
2185 ConfigOverride {
2186 files: vec!["*.test.ts".to_string()],
2187 rules: PartialRulesConfig {
2188 unused_exports: Some(Severity::Off),
2189 ..Default::default()
2190 },
2191 },
2192 ConfigOverride {
2193 files: vec!["*.stories.tsx".to_string()],
2194 rules: PartialRulesConfig {
2195 unused_files: Some(Severity::Off),
2196 ..Default::default()
2197 },
2198 },
2199 ];
2200 let resolved = config.resolve(
2201 PathBuf::from("/project"),
2202 OutputFormat::Human,
2203 1,
2204 true,
2205 true,
2206 None,
2207 );
2208 assert_eq!(resolved.overrides.len(), 2);
2209 }
2210
2211 #[test]
2212 fn ignore_export_rule_deserialize() {
2213 let json = r#"{"file": "src/types/*.ts", "exports": ["*"]}"#;
2214 let rule: IgnoreExportRule = serde_json::from_str(json).unwrap();
2215 assert_eq!(rule.file, "src/types/*.ts");
2216 assert_eq!(rule.exports, vec!["*"]);
2217 }
2218
2219 #[test]
2220 fn ignore_export_rule_specific_exports() {
2221 let json = r#"{"file": "src/constants.ts", "exports": ["FOO", "BAR", "BAZ"]}"#;
2222 let rule: IgnoreExportRule = serde_json::from_str(json).unwrap();
2223 assert_eq!(rule.exports.len(), 3);
2224 assert!(rule.exports.contains(&"FOO".to_string()));
2225 }
2226
2227 mod proptests {
2228 use super::*;
2229 use proptest::prelude::*;
2230
2231 fn arb_resolved_config(production: bool) -> ResolvedConfig {
2232 make_config(production).resolve(
2233 PathBuf::from("/project"),
2234 OutputFormat::Human,
2235 1,
2236 true,
2237 true,
2238 None,
2239 )
2240 }
2241
2242 proptest! {
2243 #[test]
2245 fn resolved_config_has_default_ignores(production in any::<bool>()) {
2246 let resolved = arb_resolved_config(production);
2247 prop_assert!(
2248 resolved.ignore_patterns.is_match("node_modules/foo/bar.js"),
2249 "Default ignore should match node_modules"
2250 );
2251 prop_assert!(
2252 resolved.ignore_patterns.is_match("dist/bundle.js"),
2253 "Default ignore should match dist"
2254 );
2255 }
2256
2257 #[test]
2259 fn production_forces_dev_deps_off(_unused in Just(())) {
2260 let resolved = arb_resolved_config(true);
2261 prop_assert_eq!(
2262 resolved.rules.unused_dev_dependencies,
2263 Severity::Off,
2264 "Production should force unused_dev_dependencies off"
2265 );
2266 prop_assert_eq!(
2267 resolved.rules.unused_optional_dependencies,
2268 Severity::Off,
2269 "Production should force unused_optional_dependencies off"
2270 );
2271 }
2272
2273 #[test]
2275 fn non_production_preserves_dev_deps_default(_unused in Just(())) {
2276 let resolved = arb_resolved_config(false);
2277 prop_assert_eq!(
2278 resolved.rules.unused_dev_dependencies,
2279 Severity::Warn,
2280 "Non-production should keep default dev dep severity"
2281 );
2282 }
2283
2284 #[test]
2286 fn cache_dir_defaults_to_root_fallow(dir_suffix in "[a-zA-Z0-9_]{1,20}") {
2287 let root = PathBuf::from(format!("/project/{dir_suffix}"));
2288 let expected_cache = root.join(".fallow");
2289 let resolved = make_config(false).resolve(
2290 root,
2291 OutputFormat::Human,
2292 1,
2293 true,
2294 true,
2295 None,
2296 );
2297 prop_assert_eq!(
2298 resolved.cache_dir, expected_cache,
2299 "Default cache dir should be root/.fallow"
2300 );
2301 }
2302
2303 #[test]
2305 fn threads_passed_through(threads in 1..64usize) {
2306 let resolved = make_config(false).resolve(
2307 PathBuf::from("/project"),
2308 OutputFormat::Human,
2309 threads,
2310 true,
2311 true, None,
2312 );
2313 prop_assert_eq!(
2314 resolved.threads, threads,
2315 "Thread count should be passed through"
2316 );
2317 }
2318
2319 #[test]
2323 fn custom_ignores_dont_replace_defaults(pattern in "[a-z_]{1,10}/[a-z_]{1,10}") {
2324 let mut config = make_config(false);
2325 config.ignore_patterns = vec![pattern];
2326 let resolved = config.resolve(
2327 PathBuf::from("/project"),
2328 OutputFormat::Human,
2329 1,
2330 true,
2331 true, None,
2332 );
2333 prop_assert!(
2334 resolved.ignore_patterns.is_match("node_modules/foo/bar.js"),
2335 "Default node_modules ignore should still be active"
2336 );
2337 }
2338 }
2339 }
2340
2341 #[test]
2342 fn resolve_expands_boundary_preset() {
2343 use crate::config::boundaries::BoundaryPreset;
2344
2345 let mut config = make_config(false);
2346 config.boundaries.preset = Some(BoundaryPreset::Hexagonal);
2347 let resolved = config.resolve(
2348 PathBuf::from("/project"),
2349 OutputFormat::Human,
2350 1,
2351 true,
2352 true,
2353 None,
2354 );
2355 assert_eq!(resolved.boundaries.zones.len(), 3);
2356 assert_eq!(resolved.boundaries.rules.len(), 3);
2357 assert_eq!(resolved.boundaries.zones[0].name, "adapters");
2358 assert_eq!(
2359 resolved.boundaries.classify_zone("src/adapters/http.ts"),
2360 Some("adapters")
2361 );
2362 }
2363
2364 #[test]
2365 fn resolve_boundary_preset_with_user_override() {
2366 use crate::config::boundaries::{BoundaryPreset, BoundaryZone};
2367
2368 let mut config = make_config(false);
2369 config.boundaries.preset = Some(BoundaryPreset::Hexagonal);
2370 config.boundaries.zones = vec![BoundaryZone {
2371 name: "domain".to_string(),
2372 patterns: vec!["src/core/**".to_string()],
2373 auto_discover: vec![],
2374 root: None,
2375 }];
2376 let resolved = config.resolve(
2377 PathBuf::from("/project"),
2378 OutputFormat::Human,
2379 1,
2380 true,
2381 true,
2382 None,
2383 );
2384 assert_eq!(resolved.boundaries.zones.len(), 3);
2385 assert_eq!(
2386 resolved.boundaries.classify_zone("src/core/user.ts"),
2387 Some("domain")
2388 );
2389 assert_eq!(
2390 resolved.boundaries.classify_zone("src/domain/user.ts"),
2391 None
2392 );
2393 }
2394
2395 #[test]
2396 fn resolve_no_preset_unchanged() {
2397 let config = make_config(false);
2398 let resolved = config.resolve(
2399 PathBuf::from("/project"),
2400 OutputFormat::Human,
2401 1,
2402 true,
2403 true,
2404 None,
2405 );
2406 assert!(resolved.boundaries.is_empty());
2407 }
2408}