use fallow_config::ResolvedConfig;
pub fn apply_rules(results: &mut fallow_types::results::AnalysisResults, config: &ResolvedConfig) {
fallow_engine::dead_code::apply_rule_severities(results, config);
}
pub use fallow_engine::error_severity::{
has_error_severity_issues, promote_policy_finding_warns, promote_warns_to_errors,
};
pub fn promote_finding_warns(results: &mut fallow_types::results::AnalysisResults) {
promote_policy_finding_warns(results);
fallow_engine::dead_code::promote_effective_warns(results);
}
#[cfg(test)]
mod tests {
use super::*;
use fallow_config::{RulesConfig, Severity};
type RuleFieldSetter = fn(&mut RulesConfig);
type ResultFieldCheck = fn(&AnalysisResults) -> bool;
use fallow_types::extract::MemberKind;
use fallow_types::output_dead_code::*;
use fallow_types::results::*;
use std::path::PathBuf;
#[expect(
clippy::too_many_lines,
reason = "test fixture; linear setup/assert, length is not a maintainability concern"
)]
fn make_results() -> AnalysisResults {
let mut r = AnalysisResults::default();
r.unused_files
.push(UnusedFileFinding::with_actions(UnusedFile {
path: PathBuf::from("/project/src/a.ts"),
}));
r.unused_exports
.push(UnusedExportFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/b.ts"),
export_name: "foo".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
r.unused_types
.push(UnusedTypeFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/c.ts"),
export_name: "MyType".into(),
is_type_only: true,
line: 5,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
r.unused_dependencies
.push(UnusedDependencyFinding::with_actions(UnusedDependency {
package_name: "lodash".into(),
location: DependencyLocation::Dependencies,
path: PathBuf::from("/project/package.json"),
line: 5,
used_in_workspaces: Vec::new(),
}));
r.unused_dev_dependencies
.push(UnusedDevDependencyFinding::with_actions(UnusedDependency {
package_name: "jest".into(),
location: DependencyLocation::DevDependencies,
path: PathBuf::from("/project/package.json"),
line: 5,
used_in_workspaces: Vec::new(),
}));
r.unused_enum_members
.push(UnusedEnumMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/d.ts"),
parent_name: "Status".into(),
member_name: "Pending".into(),
kind: MemberKind::EnumMember,
line: 3,
col: 0,
}));
r.unused_class_members
.push(UnusedClassMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/e.ts"),
parent_name: "Service".into(),
member_name: "helper".into(),
kind: MemberKind::ClassMethod,
line: 10,
col: 0,
}));
r.unused_store_members
.push(UnusedStoreMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/store.ts"),
parent_name: "useStore".into(),
member_name: "unusedAction".into(),
kind: MemberKind::StoreMember,
line: 12,
col: 0,
}));
r.unresolved_imports
.push(UnresolvedImportFinding::with_actions(UnresolvedImport {
path: PathBuf::from("/project/src/f.ts"),
specifier: "./missing".into(),
line: 1,
col: 0,
specifier_col: 0,
}));
r.unlisted_dependencies
.push(UnlistedDependencyFinding::with_actions(
UnlistedDependency {
package_name: "chalk".into(),
imported_from: vec![ImportSite {
path: PathBuf::from("/project/src/g.ts"),
line: 1,
col: 0,
}],
},
));
r.duplicate_exports
.push(DuplicateExportFinding::with_actions(DuplicateExport {
export_name: "helper".into(),
locations: vec![
DuplicateLocation {
path: PathBuf::from("/project/src/h.ts"),
line: 15,
col: 0,
},
DuplicateLocation {
path: PathBuf::from("/project/src/i.ts"),
line: 30,
col: 0,
},
],
}));
r
}
fn config_with_rules(rules: RulesConfig) -> ResolvedConfig {
fallow_config::FallowConfig {
schema: None,
minimum_version: None,
extends: vec![],
entry: vec![],
ignore_patterns: vec![],
ignore_findings: vec![],
framework: vec![],
workspaces: None,
ignore_dependencies: vec![],
ignore_unresolved_imports: vec![],
ignore_exports: vec![],
ignore_catalog_references: vec![],
ignore_dependency_overrides: vec![],
ignore_exports_used_in_file: fallow_config::IgnoreExportsUsedInFileConfig::default(),
used_class_members: vec![],
ignore_decorators: vec![],
unused_component_props: fallow_config::UnusedComponentPropsConfig::default(),
duplicates: fallow_config::DuplicatesConfig::default(),
similar_code: fallow_config::SimilarCodeConfig::default(),
health: fallow_config::HealthConfig::default(),
type_aware: fallow_config::TypeAwareConfig::default(),
rules,
boundaries: fallow_config::BoundaryConfig::default(),
production: false.into(),
plugins: vec![],
rule_packs: vec![],
dynamically_loaded: vec![],
overrides: vec![],
regression: None,
audit: fallow_config::AuditConfig::default(),
codeowners: None,
public_packages: vec![],
flags: fallow_config::FlagsConfig::default(),
security: fallow_config::SecurityConfig::default(),
fix: fallow_config::FixConfig::default(),
resolve: fallow_config::ResolveConfig::default(),
sealed: false,
include_entry_exports: false,
auto_imports: false,
fail_on_parse_error: false,
cache: fallow_config::CacheConfig::default(),
}
.resolve(
PathBuf::from("/project"),
fallow_config::OutputFormat::Human,
1,
true,
true,
None,
)
}
#[test]
fn apply_rules_default_error_preserves_all() {
let mut results = make_results();
let config = config_with_rules(RulesConfig::default());
let original_total = results.total_issues();
apply_rules(&mut results, &config);
assert_eq!(results.total_issues(), original_total);
}
#[test]
fn apply_rules_off_clears_that_issue_type() {
let mut results = make_results();
let rules = RulesConfig {
unused_files: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_files.is_empty());
assert!(!results.unused_exports.is_empty());
}
#[test]
fn apply_rules_warn_preserves_issues() {
let mut results = make_results();
let rules = RulesConfig {
unused_exports: Severity::Warn,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert_eq!(results.unused_exports.len(), 1);
}
#[test]
fn apply_rules_all_off_clears_everything() {
let mut results = make_results();
let rules = RulesConfig {
unused_files: Severity::Off,
unused_exports: Severity::Off,
unused_types: Severity::Off,
private_type_leaks: Severity::Off,
private_type_leaks_configured: false,
deprecated_exports_in_use: Severity::Off,
unused_dependencies: Severity::Off,
unused_dev_dependencies: Severity::Off,
unused_optional_dependencies: Severity::Off,
unused_enum_members: Severity::Off,
unused_class_members: Severity::Off,
unused_store_members: Severity::Off,
unprovided_injects: Severity::Off,
unrendered_components: Severity::Off,
unused_component_props: Severity::Off,
unused_component_emits: Severity::Off,
unused_component_inputs: Severity::Off,
unused_component_outputs: Severity::Off,
unused_svelte_events: Severity::Off,
unused_server_actions: Severity::Off,
unused_load_data_keys: Severity::Off,
prop_drilling: Severity::Off,
thin_wrapper: Severity::Off,
duplicate_prop_shape: Severity::Off,
css_token_drift: Severity::Off,
css_duplicate_block: Severity::Off,
css_selector_complexity: Severity::Off,
css_dead_surface: Severity::Off,
css_broken_reference: Severity::Off,
complexity_cyclomatic: Severity::Off,
complexity_cognitive: Severity::Off,
complexity_crap: Severity::Off,
unresolved_imports: Severity::Off,
unlisted_dependencies: Severity::Off,
duplicate_exports: Severity::Off,
type_only_dependencies: Severity::Off,
test_only_dependencies: Severity::Off,
dev_dependencies_in_production: Severity::Off,
boundary_violation: Severity::Error,
circular_dependencies: Severity::Off,
re_export_cycle: Severity::Warn,
coverage_gaps: Severity::Off,
feature_flags: Severity::Off,
stale_suppressions: Severity::Off,
require_suppression_reason: Severity::Off,
unused_catalog_entries: Severity::Off,
empty_catalog_groups: Severity::Off,
unresolved_catalog_references: Severity::Off,
unused_dependency_overrides: Severity::Off,
misconfigured_dependency_overrides: Severity::Off,
security_client_server_leak: Severity::Off,
security_sink: Severity::Off,
policy_violation: Severity::Warn,
invalid_client_export: Severity::Warn,
mixed_client_server_barrel: Severity::Warn,
misplaced_directive: Severity::Warn,
route_collision: Severity::Warn,
dynamic_segment_name_conflict: Severity::Warn,
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert_eq!(results.total_issues(), 0);
}
#[test]
fn apply_rules_off_each_type_individually() {
let field_setters: Vec<(RuleFieldSetter, ResultFieldCheck)> = vec![
(
|r| r.unused_files = Severity::Off,
|res| res.unused_files.is_empty(),
),
(
|r| r.unused_exports = Severity::Off,
|res| res.unused_exports.is_empty(),
),
(
|r| r.unused_types = Severity::Off,
|res| res.unused_types.is_empty(),
),
(
|r| r.private_type_leaks = Severity::Off,
|res| res.private_type_leaks.is_empty(),
),
(
|r| r.deprecated_exports_in_use = Severity::Off,
|res| res.deprecated_exports_in_use.is_empty(),
),
(
|r| r.unused_dependencies = Severity::Off,
|res| res.unused_dependencies.is_empty(),
),
(
|r| r.unused_dev_dependencies = Severity::Off,
|res| res.unused_dev_dependencies.is_empty(),
),
(
|r| r.unused_enum_members = Severity::Off,
|res| res.unused_enum_members.is_empty(),
),
(
|r| r.unused_class_members = Severity::Off,
|res| res.unused_class_members.is_empty(),
),
(
|r| r.unused_store_members = Severity::Off,
|res| res.unused_store_members.is_empty(),
),
(
|r| r.unresolved_imports = Severity::Off,
|res| res.unresolved_imports.is_empty(),
),
(
|r| r.unlisted_dependencies = Severity::Off,
|res| res.unlisted_dependencies.is_empty(),
),
(
|r| r.duplicate_exports = Severity::Off,
|res| res.duplicate_exports.is_empty(),
),
];
for (set_off, check_empty) in field_setters {
let mut results = make_results();
let mut rules = RulesConfig::default();
set_off(&mut rules);
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(
check_empty(&results),
"Setting a rule to Off should clear the corresponding results"
);
}
}
#[test]
fn empty_results_no_error_issues() {
let results = AnalysisResults::default();
let rules = RulesConfig::default();
assert!(!has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn error_severity_with_issues_returns_true() {
let results = make_results();
let rules = RulesConfig::default(); assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn warn_severity_with_issues_returns_false() {
let results = make_results();
let rules = RulesConfig {
unused_files: Severity::Warn,
unused_exports: Severity::Warn,
unused_types: Severity::Warn,
private_type_leaks: Severity::Warn,
private_type_leaks_configured: false,
deprecated_exports_in_use: Severity::Warn,
unused_dependencies: Severity::Warn,
unused_dev_dependencies: Severity::Warn,
unused_optional_dependencies: Severity::Warn,
unused_enum_members: Severity::Warn,
unused_class_members: Severity::Warn,
unused_store_members: Severity::Warn,
unprovided_injects: Severity::Warn,
unrendered_components: Severity::Warn,
unused_component_props: Severity::Warn,
unused_component_emits: Severity::Warn,
unused_component_inputs: Severity::Warn,
unused_component_outputs: Severity::Warn,
unused_svelte_events: Severity::Warn,
unused_server_actions: Severity::Warn,
unused_load_data_keys: Severity::Warn,
prop_drilling: Severity::Off,
thin_wrapper: Severity::Off,
duplicate_prop_shape: Severity::Off,
css_token_drift: Severity::Off,
css_duplicate_block: Severity::Off,
css_selector_complexity: Severity::Off,
css_dead_surface: Severity::Off,
css_broken_reference: Severity::Off,
complexity_cyclomatic: Severity::Off,
complexity_cognitive: Severity::Off,
complexity_crap: Severity::Off,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: Severity::Warn,
dev_dependencies_in_production: Severity::Warn,
boundary_violation: Severity::Error,
circular_dependencies: Severity::Warn,
re_export_cycle: Severity::Warn,
coverage_gaps: Severity::Warn,
feature_flags: Severity::Warn,
stale_suppressions: Severity::Warn,
require_suppression_reason: Severity::Warn,
unused_catalog_entries: Severity::Warn,
empty_catalog_groups: Severity::Warn,
unresolved_catalog_references: Severity::Error,
unused_dependency_overrides: Severity::Warn,
misconfigured_dependency_overrides: Severity::Error,
security_client_server_leak: Severity::Off,
security_sink: Severity::Off,
policy_violation: Severity::Warn,
invalid_client_export: Severity::Warn,
mixed_client_server_barrel: Severity::Warn,
misplaced_directive: Severity::Warn,
route_collision: Severity::Warn,
dynamic_segment_name_conflict: Severity::Warn,
};
assert!(!has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn mixed_severity_returns_true_for_error_with_issues() {
let mut results = AnalysisResults::default();
results
.unused_files
.push(UnusedFileFinding::with_actions(UnusedFile {
path: PathBuf::from("/project/src/a.ts"),
}));
let mut rules = RulesConfig {
unused_files: Severity::Warn,
unused_exports: Severity::Warn,
unused_types: Severity::Warn,
private_type_leaks: Severity::Warn,
private_type_leaks_configured: false,
deprecated_exports_in_use: Severity::Warn,
unused_dependencies: Severity::Warn,
unused_dev_dependencies: Severity::Warn,
unused_optional_dependencies: Severity::Warn,
unused_enum_members: Severity::Warn,
unused_class_members: Severity::Warn,
unused_store_members: Severity::Warn,
unprovided_injects: Severity::Warn,
unrendered_components: Severity::Warn,
unused_component_props: Severity::Warn,
unused_component_emits: Severity::Warn,
unused_component_inputs: Severity::Warn,
unused_component_outputs: Severity::Warn,
unused_svelte_events: Severity::Warn,
unused_server_actions: Severity::Warn,
unused_load_data_keys: Severity::Warn,
prop_drilling: Severity::Off,
thin_wrapper: Severity::Off,
duplicate_prop_shape: Severity::Off,
css_token_drift: Severity::Off,
css_duplicate_block: Severity::Off,
css_selector_complexity: Severity::Off,
css_dead_surface: Severity::Off,
css_broken_reference: Severity::Off,
complexity_cyclomatic: Severity::Off,
complexity_cognitive: Severity::Off,
complexity_crap: Severity::Off,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: Severity::Warn,
dev_dependencies_in_production: Severity::Warn,
boundary_violation: Severity::Error,
circular_dependencies: Severity::Warn,
re_export_cycle: Severity::Warn,
coverage_gaps: Severity::Warn,
feature_flags: Severity::Warn,
stale_suppressions: Severity::Warn,
require_suppression_reason: Severity::Warn,
unused_catalog_entries: Severity::Warn,
empty_catalog_groups: Severity::Warn,
unresolved_catalog_references: Severity::Error,
unused_dependency_overrides: Severity::Warn,
misconfigured_dependency_overrides: Severity::Error,
security_client_server_leak: Severity::Off,
security_sink: Severity::Off,
policy_violation: Severity::Warn,
invalid_client_export: Severity::Warn,
mixed_client_server_barrel: Severity::Warn,
misplaced_directive: Severity::Warn,
route_collision: Severity::Warn,
dynamic_segment_name_conflict: Severity::Warn,
};
assert!(!has_error_severity_issues(&results, &rules, None, false));
rules.unused_files = Severity::Error;
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn off_severity_with_issues_returns_false() {
let mut results = AnalysisResults::default();
results
.unresolved_imports
.push(UnresolvedImportFinding::with_actions(UnresolvedImport {
path: PathBuf::from("/project/src/a.ts"),
specifier: "./missing".into(),
line: 1,
col: 0,
specifier_col: 0,
}));
let rules = RulesConfig {
unresolved_imports: Severity::Off,
..RulesConfig::default()
};
assert!(!has_error_severity_issues(&results, &rules, None, false));
}
fn config_with_test_override() -> ResolvedConfig {
fallow_config::FallowConfig {
schema: None,
minimum_version: None,
extends: vec![],
entry: vec![],
ignore_patterns: vec![],
ignore_findings: vec![],
framework: vec![],
workspaces: None,
ignore_dependencies: vec![],
ignore_unresolved_imports: vec![],
ignore_exports: vec![],
ignore_catalog_references: vec![],
ignore_dependency_overrides: vec![],
ignore_exports_used_in_file: fallow_config::IgnoreExportsUsedInFileConfig::default(),
used_class_members: vec![],
ignore_decorators: vec![],
unused_component_props: fallow_config::UnusedComponentPropsConfig::default(),
duplicates: fallow_config::DuplicatesConfig::default(),
similar_code: fallow_config::SimilarCodeConfig::default(),
health: fallow_config::HealthConfig::default(),
type_aware: fallow_config::TypeAwareConfig::default(),
rules: RulesConfig::default(), boundaries: fallow_config::BoundaryConfig::default(),
production: false.into(),
plugins: vec![],
rule_packs: vec![],
dynamically_loaded: vec![],
regression: None,
audit: fallow_config::AuditConfig::default(),
codeowners: None,
public_packages: vec![],
flags: fallow_config::FlagsConfig::default(),
security: fallow_config::SecurityConfig::default(),
fix: fallow_config::FixConfig::default(),
resolve: fallow_config::ResolveConfig::default(),
sealed: false,
include_entry_exports: false,
auto_imports: false,
fail_on_parse_error: false,
cache: fallow_config::CacheConfig::default(),
overrides: vec![fallow_config::ConfigOverride {
files: vec!["**/*.test.ts".to_string()],
rules: fallow_config::PartialRulesConfig {
unused_exports: Some(Severity::Off),
..Default::default()
},
}],
}
.resolve(
PathBuf::from("/project"),
fallow_config::OutputFormat::Human,
1,
true,
true,
None,
)
}
fn config_with_circular_override(pattern: &str, severity: Severity) -> ResolvedConfig {
fallow_config::FallowConfig {
schema: None,
minimum_version: None,
extends: vec![],
entry: vec![],
ignore_patterns: vec![],
ignore_findings: vec![],
framework: vec![],
workspaces: None,
ignore_dependencies: vec![],
ignore_unresolved_imports: vec![],
ignore_exports: vec![],
ignore_catalog_references: vec![],
ignore_dependency_overrides: vec![],
ignore_exports_used_in_file: fallow_config::IgnoreExportsUsedInFileConfig::default(),
used_class_members: vec![],
ignore_decorators: vec![],
unused_component_props: fallow_config::UnusedComponentPropsConfig::default(),
duplicates: fallow_config::DuplicatesConfig::default(),
similar_code: fallow_config::SimilarCodeConfig::default(),
health: fallow_config::HealthConfig::default(),
type_aware: fallow_config::TypeAwareConfig::default(),
rules: RulesConfig::default(),
boundaries: fallow_config::BoundaryConfig::default(),
production: false.into(),
plugins: vec![],
rule_packs: vec![],
dynamically_loaded: vec![],
regression: None,
audit: fallow_config::AuditConfig::default(),
codeowners: None,
public_packages: vec![],
flags: fallow_config::FlagsConfig::default(),
security: fallow_config::SecurityConfig::default(),
fix: fallow_config::FixConfig::default(),
resolve: fallow_config::ResolveConfig::default(),
sealed: false,
include_entry_exports: false,
auto_imports: false,
fail_on_parse_error: false,
cache: fallow_config::CacheConfig::default(),
overrides: vec![fallow_config::ConfigOverride {
files: vec![pattern.to_string()],
rules: fallow_config::PartialRulesConfig {
circular_dependencies: Some(severity),
..Default::default()
},
}],
}
.resolve(
PathBuf::from("/project"),
fallow_config::OutputFormat::Human,
1,
true,
true,
None,
)
}
fn config_with_boundary_override(pattern: &str, severity: Severity) -> ResolvedConfig {
fallow_config::FallowConfig {
schema: None,
minimum_version: None,
extends: vec![],
entry: vec![],
ignore_patterns: vec![],
ignore_findings: vec![],
framework: vec![],
workspaces: None,
ignore_dependencies: vec![],
ignore_unresolved_imports: vec![],
ignore_exports: vec![],
ignore_catalog_references: vec![],
ignore_dependency_overrides: vec![],
ignore_exports_used_in_file: fallow_config::IgnoreExportsUsedInFileConfig::default(),
used_class_members: vec![],
ignore_decorators: vec![],
unused_component_props: fallow_config::UnusedComponentPropsConfig::default(),
duplicates: fallow_config::DuplicatesConfig::default(),
similar_code: fallow_config::SimilarCodeConfig::default(),
health: fallow_config::HealthConfig::default(),
type_aware: fallow_config::TypeAwareConfig::default(),
rules: RulesConfig::default(),
boundaries: fallow_config::BoundaryConfig::default(),
production: false.into(),
plugins: vec![],
rule_packs: vec![],
dynamically_loaded: vec![],
regression: None,
audit: fallow_config::AuditConfig::default(),
codeowners: None,
public_packages: vec![],
flags: fallow_config::FlagsConfig::default(),
security: fallow_config::SecurityConfig::default(),
fix: fallow_config::FixConfig::default(),
resolve: fallow_config::ResolveConfig::default(),
sealed: false,
include_entry_exports: false,
auto_imports: false,
fail_on_parse_error: false,
cache: fallow_config::CacheConfig::default(),
overrides: vec![fallow_config::ConfigOverride {
files: vec![pattern.to_string()],
rules: fallow_config::PartialRulesConfig {
boundary_violation: Some(severity),
..Default::default()
},
}],
}
.resolve(
PathBuf::from("/project"),
fallow_config::OutputFormat::Human,
1,
true,
true,
None,
)
}
fn circular_dependency(files: &[&str]) -> CircularDependencyFinding {
CircularDependencyFinding::with_actions(CircularDependency {
files: files.iter().map(PathBuf::from).collect(),
length: files.len(),
line: 1,
col: 0,
edges: Vec::new(),
is_cross_package: false,
})
}
fn boundary_violation(path: &str) -> BoundaryViolationFinding {
BoundaryViolationFinding::with_actions(BoundaryViolation {
from_path: PathBuf::from(path),
to_path: PathBuf::from("/project/src/db/query.ts"),
from_zone: "ui".to_string(),
to_zone: "db".to_string(),
import_specifier: "../db/query".to_string(),
line: 1,
col: 0,
})
}
fn boundary_coverage_violation(path: &str) -> BoundaryCoverageViolationFinding {
BoundaryCoverageViolationFinding::with_actions(BoundaryCoverageViolation {
path: PathBuf::from(path),
line: 1,
col: 0,
})
}
#[test]
fn apply_rules_with_override_filters_matching_files() {
let mut results = AnalysisResults::default();
results
.unused_exports
.push(UnusedExportFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/utils.test.ts"),
export_name: "testHelper".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
results
.unused_exports
.push(UnusedExportFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/utils.ts"),
export_name: "realExport".into(),
is_type_only: false,
line: 5,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_test_override();
apply_rules(&mut results, &config);
assert_eq!(results.unused_exports.len(), 1);
assert_eq!(results.unused_exports[0].export.export_name, "realExport");
}
#[test]
fn apply_rules_with_override_preserves_non_matching_files() {
let mut results = AnalysisResults::default();
results
.unused_files
.push(UnusedFileFinding::with_actions(UnusedFile {
path: PathBuf::from("/project/src/dead.ts"),
}));
let config = config_with_test_override();
apply_rules(&mut results, &config);
assert_eq!(results.unused_files.len(), 1);
}
#[test]
fn apply_rules_with_override_filters_circular_cycle_when_all_files_off() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(circular_dependency(&[
"/project/src/generated/a.ts",
"/project/src/generated/b.ts",
]));
let config = config_with_circular_override("src/generated/**", Severity::Off);
apply_rules(&mut results, &config);
assert!(results.circular_dependencies.is_empty());
}
#[test]
fn apply_rules_with_override_preserves_circular_cycle_when_any_file_is_on() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(circular_dependency(&[
"/project/src/generated/a.ts",
"/project/src/live/b.ts",
]));
let config = config_with_circular_override("src/generated/**", Severity::Off);
apply_rules(&mut results, &config);
assert_eq!(results.circular_dependencies.len(), 1);
}
#[test]
fn apply_rules_with_override_filters_boundary_findings() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/generated/a.ts"));
results
.boundary_coverage_violations
.push(boundary_coverage_violation("/project/src/generated/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Off);
apply_rules(&mut results, &config);
assert!(results.boundary_violations.is_empty());
assert!(results.boundary_coverage_violations.is_empty());
}
#[test]
fn apply_rules_with_override_preserves_unmatched_boundary_findings() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/live/a.ts"));
results
.boundary_coverage_violations
.push(boundary_coverage_violation("/project/src/live/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Off);
apply_rules(&mut results, &config);
assert_eq!(results.boundary_violations.len(), 1);
assert_eq!(results.boundary_coverage_violations.len(), 1);
}
#[test]
fn has_error_with_override_per_file_resolution() {
let mut results = AnalysisResults::default();
results
.unused_exports
.push(UnusedExportFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/utils.test.ts"),
export_name: "testHelper".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_test_override();
let rules = &config.rules;
assert!(
!has_error_severity_issues(&results, rules, Some(&config), false),
"test file override should suppress error"
);
}
#[test]
fn has_error_with_override_non_matching_file_still_error() {
let mut results = AnalysisResults::default();
results
.unused_exports
.push(UnusedExportFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/utils.ts"),
export_name: "realExport".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_test_override();
let rules = &config.rules;
assert!(
has_error_severity_issues(&results, rules, Some(&config), false),
"non-test file should still have Error severity"
);
}
#[test]
fn has_error_with_override_circular_cycle_uses_file_severity() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(circular_dependency(&[
"/project/src/generated/a.ts",
"/project/src/generated/b.ts",
]));
let config = config_with_circular_override("src/generated/**", Severity::Warn);
let rules = &config.rules;
assert!(
!has_error_severity_issues(&results, rules, Some(&config), false),
"cycle files downgraded to Warn should not produce an Error verdict"
);
}
#[test]
fn has_error_with_override_circular_cycle_keeps_error_for_unmatched_file() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(circular_dependency(&[
"/project/src/generated/a.ts",
"/project/src/live/b.ts",
]));
let config = config_with_circular_override("src/generated/**", Severity::Off);
let rules = &config.rules;
assert!(
has_error_severity_issues(&results, rules, Some(&config), false),
"a cycle touching any Error-severity file should still fail"
);
}
#[test]
fn has_error_with_override_boundary_findings_use_file_severity() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/generated/a.ts"));
results
.boundary_coverage_violations
.push(boundary_coverage_violation("/project/src/generated/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Warn);
let rules = &config.rules;
assert!(
!has_error_severity_issues(&results, rules, Some(&config), false),
"boundary findings downgraded to Warn should not produce an Error verdict"
);
}
#[test]
fn has_error_with_override_boundary_violation_keeps_error_for_unmatched_file() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/live/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Off);
let rules = &config.rules;
assert!(
has_error_severity_issues(&results, rules, Some(&config), false),
"an import-direction violation outside the override must still fail the run"
);
}
#[test]
fn has_error_with_override_boundary_violation_off_for_matched_file() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/generated/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Off);
let rules = &config.rules;
assert!(
!has_error_severity_issues(&results, rules, Some(&config), false),
"a violation whose importer matches an Off override must not fail the run"
);
}
#[test]
fn has_error_with_override_promotes_per_path_warn_when_requested() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/generated/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Warn);
let mut rules = config.rules.clone();
promote_warns_to_errors(&mut rules);
assert!(
!has_error_severity_issues(&results, &rules, Some(&config), false),
"without promotion a per-path Warn stays a warning"
);
assert!(
has_error_severity_issues(&results, &rules, Some(&config), true),
"fail-on-issues must promote a per-path Warn after override resolution"
);
}
#[test]
fn has_error_with_override_promotion_leaves_off_alone() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/generated/a.ts"));
let config = config_with_boundary_override("src/generated/**", Severity::Off);
let rules = &config.rules;
assert!(
!has_error_severity_issues(&results, rules, Some(&config), true),
"promotion only lifts Warn; an Off override stays silent"
);
}
#[test]
fn promote_warns_to_errors_promotes_all_warns() {
let mut rules = RulesConfig {
unused_files: Severity::Warn,
unused_exports: Severity::Warn,
unused_types: Severity::Warn,
private_type_leaks: Severity::Warn,
private_type_leaks_configured: false,
deprecated_exports_in_use: Severity::Warn,
unused_dependencies: Severity::Warn,
unused_dev_dependencies: Severity::Warn,
unused_optional_dependencies: Severity::Warn,
unused_enum_members: Severity::Warn,
unused_class_members: Severity::Warn,
unused_store_members: Severity::Warn,
unprovided_injects: Severity::Warn,
unrendered_components: Severity::Warn,
unused_component_props: Severity::Warn,
unused_component_emits: Severity::Warn,
unused_component_inputs: Severity::Warn,
unused_component_outputs: Severity::Warn,
unused_svelte_events: Severity::Warn,
unused_server_actions: Severity::Warn,
unused_load_data_keys: Severity::Warn,
prop_drilling: Severity::Off,
thin_wrapper: Severity::Off,
duplicate_prop_shape: Severity::Off,
css_token_drift: Severity::Off,
css_duplicate_block: Severity::Off,
css_selector_complexity: Severity::Off,
css_dead_surface: Severity::Off,
css_broken_reference: Severity::Off,
complexity_cyclomatic: Severity::Off,
complexity_cognitive: Severity::Off,
complexity_crap: Severity::Off,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: Severity::Warn,
dev_dependencies_in_production: Severity::Warn,
boundary_violation: Severity::Error,
circular_dependencies: Severity::Warn,
re_export_cycle: Severity::Warn,
coverage_gaps: Severity::Warn,
feature_flags: Severity::Warn,
stale_suppressions: Severity::Warn,
require_suppression_reason: Severity::Warn,
unused_catalog_entries: Severity::Warn,
empty_catalog_groups: Severity::Warn,
unresolved_catalog_references: Severity::Error,
unused_dependency_overrides: Severity::Warn,
misconfigured_dependency_overrides: Severity::Error,
security_client_server_leak: Severity::Off,
security_sink: Severity::Off,
policy_violation: Severity::Warn,
invalid_client_export: Severity::Warn,
mixed_client_server_barrel: Severity::Warn,
misplaced_directive: Severity::Warn,
route_collision: Severity::Warn,
dynamic_segment_name_conflict: Severity::Warn,
};
promote_warns_to_errors(&mut rules);
assert_eq!(rules.unused_files, Severity::Error);
assert_eq!(rules.unused_exports, Severity::Error);
assert_eq!(rules.unused_types, Severity::Error);
assert_eq!(rules.private_type_leaks, Severity::Error);
assert_eq!(rules.deprecated_exports_in_use, Severity::Error);
assert_eq!(rules.unused_dependencies, Severity::Error);
assert_eq!(rules.unused_dev_dependencies, Severity::Error);
assert_eq!(rules.unused_optional_dependencies, Severity::Error);
assert_eq!(rules.unused_enum_members, Severity::Error);
assert_eq!(rules.unused_class_members, Severity::Error);
assert_eq!(rules.unused_store_members, Severity::Error);
assert_eq!(rules.unresolved_imports, Severity::Error);
assert_eq!(rules.unlisted_dependencies, Severity::Error);
assert_eq!(rules.duplicate_exports, Severity::Error);
assert_eq!(rules.type_only_dependencies, Severity::Error);
assert_eq!(rules.test_only_dependencies, Severity::Error);
assert_eq!(rules.circular_dependencies, Severity::Error);
assert_eq!(rules.coverage_gaps, Severity::Error);
assert_eq!(rules.unused_catalog_entries, Severity::Error);
}
#[test]
fn promote_warns_to_errors_preserves_off() {
let mut rules = RulesConfig {
unused_files: Severity::Off,
unused_exports: Severity::Off,
unused_types: Severity::Off,
private_type_leaks: Severity::Off,
private_type_leaks_configured: false,
deprecated_exports_in_use: Severity::Off,
unused_dependencies: Severity::Off,
unused_dev_dependencies: Severity::Off,
unused_optional_dependencies: Severity::Off,
unused_enum_members: Severity::Off,
unused_class_members: Severity::Off,
unused_store_members: Severity::Off,
unprovided_injects: Severity::Off,
unrendered_components: Severity::Off,
unused_component_props: Severity::Off,
unused_component_emits: Severity::Off,
unused_component_inputs: Severity::Off,
unused_component_outputs: Severity::Off,
unused_svelte_events: Severity::Off,
unused_server_actions: Severity::Off,
unused_load_data_keys: Severity::Off,
prop_drilling: Severity::Off,
thin_wrapper: Severity::Off,
duplicate_prop_shape: Severity::Off,
css_token_drift: Severity::Off,
css_duplicate_block: Severity::Off,
css_selector_complexity: Severity::Off,
css_dead_surface: Severity::Off,
css_broken_reference: Severity::Off,
complexity_cyclomatic: Severity::Off,
complexity_cognitive: Severity::Off,
complexity_crap: Severity::Off,
unresolved_imports: Severity::Off,
unlisted_dependencies: Severity::Off,
duplicate_exports: Severity::Off,
type_only_dependencies: Severity::Off,
test_only_dependencies: Severity::Off,
dev_dependencies_in_production: Severity::Off,
boundary_violation: Severity::Error,
circular_dependencies: Severity::Off,
re_export_cycle: Severity::Warn,
coverage_gaps: Severity::Off,
feature_flags: Severity::Off,
stale_suppressions: Severity::Off,
require_suppression_reason: Severity::Off,
unused_catalog_entries: Severity::Off,
empty_catalog_groups: Severity::Off,
unresolved_catalog_references: Severity::Off,
unused_dependency_overrides: Severity::Off,
misconfigured_dependency_overrides: Severity::Off,
security_client_server_leak: Severity::Off,
security_sink: Severity::Off,
policy_violation: Severity::Warn,
invalid_client_export: Severity::Warn,
mixed_client_server_barrel: Severity::Warn,
misplaced_directive: Severity::Warn,
route_collision: Severity::Warn,
dynamic_segment_name_conflict: Severity::Warn,
};
promote_warns_to_errors(&mut rules);
assert_eq!(rules.unused_files, Severity::Off);
assert_eq!(rules.unused_exports, Severity::Off);
assert_eq!(rules.unused_types, Severity::Off);
assert_eq!(rules.private_type_leaks, Severity::Off);
assert_eq!(rules.circular_dependencies, Severity::Off);
assert_eq!(rules.coverage_gaps, Severity::Off);
}
#[test]
fn promote_warns_to_errors_preserves_existing_errors() {
let mut rules = RulesConfig::default(); promote_warns_to_errors(&mut rules);
assert_eq!(rules.unused_files, Severity::Error);
assert_eq!(rules.unused_exports, Severity::Error);
}
#[test]
fn promote_warns_to_errors_mixed_severities() {
let mut rules = RulesConfig {
unused_files: Severity::Error,
unused_exports: Severity::Warn,
unused_types: Severity::Off,
..RulesConfig::default()
};
promote_warns_to_errors(&mut rules);
assert_eq!(rules.unused_files, Severity::Error);
assert_eq!(rules.unused_exports, Severity::Error);
assert_eq!(rules.unused_types, Severity::Off);
}
#[test]
fn has_error_circular_deps_detected() {
let mut results = AnalysisResults::default();
results
.circular_dependencies
.push(CircularDependencyFinding::with_actions(
CircularDependency {
files: vec![
PathBuf::from("/project/src/a.ts"),
PathBuf::from("/project/src/b.ts"),
],
length: 2,
line: 1,
col: 0,
edges: Vec::new(),
is_cross_package: false,
},
));
let rules = RulesConfig::default();
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn has_error_circular_deps_warn_not_detected() {
let mut results = AnalysisResults::default();
results
.circular_dependencies
.push(CircularDependencyFinding::with_actions(
CircularDependency {
files: vec![
PathBuf::from("/project/src/a.ts"),
PathBuf::from("/project/src/b.ts"),
],
length: 2,
line: 1,
col: 0,
edges: Vec::new(),
is_cross_package: false,
},
));
let rules = RulesConfig {
circular_dependencies: Severity::Warn,
re_export_cycle: Severity::Warn,
..RulesConfig::default()
};
assert!(!has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn has_error_optional_deps_warn_by_default() {
let mut results = AnalysisResults::default();
results
.unused_optional_dependencies
.push(UnusedOptionalDependencyFinding::with_actions(
UnusedDependency {
package_name: "optional-pkg".into(),
location: DependencyLocation::OptionalDependencies,
path: PathBuf::from("/project/package.json"),
line: 5,
used_in_workspaces: Vec::new(),
},
));
let rules = RulesConfig::default();
assert!(!has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn has_error_optional_deps_detected_when_error() {
let mut results = AnalysisResults::default();
results
.unused_optional_dependencies
.push(UnusedOptionalDependencyFinding::with_actions(
UnusedDependency {
package_name: "optional-pkg".into(),
location: DependencyLocation::OptionalDependencies,
path: PathBuf::from("/project/package.json"),
line: 5,
used_in_workspaces: Vec::new(),
},
));
let rules = RulesConfig {
unused_optional_dependencies: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn has_error_type_only_deps_warn_by_default() {
let mut results = AnalysisResults::default();
results
.type_only_dependencies
.push(TypeOnlyDependencyFinding::with_actions(
TypeOnlyDependency {
package_name: "zod".into(),
path: PathBuf::from("/project/package.json"),
line: 8,
},
));
let rules = RulesConfig::default();
assert!(!has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn has_error_type_only_deps_detected_when_error() {
let mut results = AnalysisResults::default();
results
.type_only_dependencies
.push(TypeOnlyDependencyFinding::with_actions(
TypeOnlyDependency {
package_name: "zod".into(),
path: PathBuf::from("/project/package.json"),
line: 8,
},
));
let rules = RulesConfig {
type_only_dependencies: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
fn config_with_override_for_rule(
pattern: &str,
configure: impl FnOnce(&mut fallow_config::PartialRulesConfig),
) -> ResolvedConfig {
let mut partial = fallow_config::PartialRulesConfig::default();
configure(&mut partial);
fallow_config::FallowConfig {
schema: None,
minimum_version: None,
extends: vec![],
entry: vec![],
ignore_patterns: vec![],
ignore_findings: vec![],
framework: vec![],
workspaces: None,
ignore_dependencies: vec![],
ignore_unresolved_imports: vec![],
ignore_exports: vec![],
ignore_catalog_references: vec![],
ignore_dependency_overrides: vec![],
ignore_exports_used_in_file: fallow_config::IgnoreExportsUsedInFileConfig::default(),
used_class_members: vec![],
ignore_decorators: vec![],
unused_component_props: fallow_config::UnusedComponentPropsConfig::default(),
duplicates: fallow_config::DuplicatesConfig::default(),
similar_code: fallow_config::SimilarCodeConfig::default(),
health: fallow_config::HealthConfig::default(),
type_aware: fallow_config::TypeAwareConfig::default(),
rules: RulesConfig::default(),
boundaries: fallow_config::BoundaryConfig::default(),
production: false.into(),
plugins: vec![],
rule_packs: vec![],
dynamically_loaded: vec![],
regression: None,
audit: fallow_config::AuditConfig::default(),
codeowners: None,
public_packages: vec![],
flags: fallow_config::FlagsConfig::default(),
security: fallow_config::SecurityConfig::default(),
fix: fallow_config::FixConfig::default(),
resolve: fallow_config::ResolveConfig::default(),
sealed: false,
include_entry_exports: false,
auto_imports: false,
fail_on_parse_error: false,
cache: fallow_config::CacheConfig::default(),
overrides: vec![fallow_config::ConfigOverride {
files: vec![pattern.to_string()],
rules: partial,
}],
}
.resolve(
PathBuf::from("/project"),
fallow_config::OutputFormat::Human,
1,
true,
true,
None,
)
}
fn stale_suppression(path: &str, missing_reason: bool) -> StaleSuppression {
StaleSuppression {
path: PathBuf::from(path),
line: 1,
col: 0,
origin: SuppressionOrigin::Comment {
issue_kind: Some("unused-exports".to_string()),
reason: if missing_reason {
None
} else {
Some("no longer needed".to_string())
},
is_file_level: false,
kind_known: true,
},
missing_reason,
actions: StaleSuppression::actions_for(missing_reason),
effective_severity: None,
}
}
#[test]
fn base_collection_re_export_cycle_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.re_export_cycles
.push(ReExportCycleFinding::with_actions(ReExportCycle {
files: vec![
PathBuf::from("/project/src/a.ts"),
PathBuf::from("/project/src/b.ts"),
],
kind: ReExportCycleKind::MultiNode,
}));
let rules = RulesConfig {
re_export_cycle: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.re_export_cycles.is_empty());
}
#[test]
fn base_collection_boundary_violation_off_clears_all_three_vecs() {
let mut results = AnalysisResults::default();
results
.boundary_violations
.push(boundary_violation("/project/src/a.ts"));
results
.boundary_coverage_violations
.push(boundary_coverage_violation("/project/src/a.ts"));
results
.boundary_call_violations
.push(BoundaryCallViolationFinding::with_actions(
BoundaryCallViolation {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
zone: "ui".to_string(),
callee: "fs.readFileSync".to_string(),
pattern: "fs.*".to_string(),
},
));
let rules = RulesConfig {
boundary_violation: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.boundary_violations.is_empty());
assert!(results.boundary_coverage_violations.is_empty());
assert!(results.boundary_call_violations.is_empty());
}
#[test]
fn base_collection_policy_violation_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.policy_violations
.push(PolicyViolationFinding::with_actions(PolicyViolation {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
pack: "my-pack".to_string(),
rule_id: "no-exec".to_string(),
kind: PolicyRuleKind::BannedCall,
matched: "cp.exec".to_string(),
severity: PolicyViolationSeverity::Error,
message: None,
}));
let rules = RulesConfig {
policy_violation: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.policy_violations.is_empty());
}
#[test]
fn base_collection_unused_catalog_entries_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_catalog_entries
.push(UnusedCatalogEntryFinding::with_actions(
UnusedCatalogEntry {
entry_name: "lodash".to_string(),
catalog_name: "default".to_string(),
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 5,
hardcoded_consumers: vec![],
},
));
let rules = RulesConfig {
unused_catalog_entries: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_catalog_entries.is_empty());
}
#[test]
fn base_collection_empty_catalog_groups_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.empty_catalog_groups
.push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
catalog_name: "tools".to_string(),
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 10,
}));
let rules = RulesConfig {
empty_catalog_groups: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.empty_catalog_groups.is_empty());
}
#[test]
fn base_collection_unresolved_catalog_references_off_clears_findings() {
let mut results = AnalysisResults::default();
results.unresolved_catalog_references.push(
UnresolvedCatalogReferenceFinding::with_actions(UnresolvedCatalogReference {
entry_name: "missing-pkg".to_string(),
catalog_name: "default".to_string(),
path: PathBuf::from("/project/package.json"),
line: 3,
available_in_catalogs: vec![],
}),
);
let rules = RulesConfig {
unresolved_catalog_references: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unresolved_catalog_references.is_empty());
}
#[test]
fn base_collection_unused_dependency_overrides_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_dependency_overrides
.push(UnusedDependencyOverrideFinding::with_actions(
UnusedDependencyOverride {
raw_key: "old-dep".to_string(),
target_package: "old-dep".to_string(),
parent_package: None,
version_constraint: None,
version_range: "^1.0.0".to_string(),
source: DependencyOverrideSource::PnpmWorkspaceYaml,
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 7,
hint: None,
},
));
let rules = RulesConfig {
unused_dependency_overrides: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_dependency_overrides.is_empty());
}
#[test]
fn base_collection_misconfigured_dependency_overrides_off_clears_findings() {
let mut results = AnalysisResults::default();
results.misconfigured_dependency_overrides.push(
MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
raw_key: "bad>".to_string(),
target_package: None,
raw_value: "1.0.0".to_string(),
reason: DependencyOverrideMisconfigReason::UnparsableKey,
source: DependencyOverrideSource::PnpmPackageJson,
path: PathBuf::from("/project/package.json"),
line: 4,
}),
);
let rules = RulesConfig {
misconfigured_dependency_overrides: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.misconfigured_dependency_overrides.is_empty());
}
#[test]
fn override_drops_unused_types_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_types
.push(UnusedTypeFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/generated/types.ts"),
export_name: "GenType".to_string(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_types = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_types.is_empty());
}
#[test]
fn override_keeps_unused_types_for_non_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_types
.push(UnusedTypeFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/core/types.ts"),
export_name: "CoreType".to_string(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_types = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert_eq!(results.unused_types.len(), 1);
}
#[test]
fn override_drops_private_type_leaks_for_matching_file() {
let mut results = AnalysisResults::default();
results
.private_type_leaks
.push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
path: PathBuf::from("/project/src/generated/api.ts"),
export_name: "publicFn".to_string(),
type_name: "_PrivateHelper".to_string(),
line: 5,
col: 0,
span_start: 0,
semantic: None,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.private_type_leaks = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.private_type_leaks.is_empty());
}
#[test]
fn override_drops_unused_enum_members_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_enum_members
.push(UnusedEnumMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/generated/enums.ts"),
parent_name: "Status".to_string(),
member_name: "Legacy".to_string(),
kind: MemberKind::EnumMember,
line: 8,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_enum_members = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_enum_members.is_empty());
}
#[test]
fn override_drops_unused_class_members_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_class_members
.push(UnusedClassMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/generated/service.ts"),
parent_name: "MyService".to_string(),
member_name: "_legacyHelper".to_string(),
kind: MemberKind::ClassMethod,
line: 20,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_class_members = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_class_members.is_empty());
}
#[test]
fn override_drops_unused_store_members_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_store_members
.push(UnusedStoreMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/generated/store.ts"),
parent_name: "useGenStore".to_string(),
member_name: "unusedAction".to_string(),
kind: MemberKind::StoreMember,
line: 12,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_store_members = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_store_members.is_empty());
}
#[test]
fn override_drops_unprovided_injects_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unprovided_injects
.push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
path: PathBuf::from("/project/src/generated/child.vue"),
key_name: "INJECT_KEY".to_string(),
framework: "vue".to_string(),
line: 3,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unprovided_injects = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unprovided_injects.is_empty());
}
#[test]
fn override_drops_unresolved_imports_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unresolved_imports
.push(UnresolvedImportFinding::with_actions(UnresolvedImport {
path: PathBuf::from("/project/src/generated/mod.ts"),
specifier: "./missing-gen".to_string(),
line: 1,
col: 0,
specifier_col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unresolved_imports = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unresolved_imports.is_empty());
}
#[test]
fn override_drops_unrendered_components_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unrendered_components
.push(UnrenderedComponentFinding::with_actions(
UnrenderedComponent {
path: PathBuf::from("/project/src/legacy/LegacyWidget.vue"),
component_name: "LegacyWidget".to_string(),
framework: "vue".to_string(),
reachable_via: None,
line: 1,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unrendered_components = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unrendered_components.is_empty());
}
#[test]
fn override_drops_unused_component_props_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_props
.push(UnusedComponentPropFinding::with_actions(
UnusedComponentProp {
path: PathBuf::from("/project/src/legacy/Card.vue"),
component_name: "Card".to_string(),
prop_name: "unusedColor".to_string(),
line: 5,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_props = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_component_props.is_empty());
}
#[test]
fn override_drops_unused_component_emits_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_emits
.push(UnusedComponentEmitFinding::with_actions(
UnusedComponentEmit {
path: PathBuf::from("/project/src/legacy/Button.vue"),
component_name: "Button".to_string(),
emit_name: "legacy-click".to_string(),
line: 7,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_emits = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_component_emits.is_empty());
}
#[test]
fn override_drops_unused_component_inputs_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_inputs
.push(UnusedComponentInputFinding::with_actions(
UnusedComponentInput {
path: PathBuf::from("/project/src/legacy/table.component.ts"),
component_name: "table".to_string(),
input_name: "deprecated".to_string(),
line: 12,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_inputs = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_component_inputs.is_empty());
}
#[test]
fn override_drops_unused_component_outputs_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_outputs
.push(UnusedComponentOutputFinding::with_actions(
UnusedComponentOutput {
path: PathBuf::from("/project/src/legacy/table.component.ts"),
component_name: "table".to_string(),
output_name: "deprecatedChange".to_string(),
line: 15,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_outputs = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_component_outputs.is_empty());
}
#[test]
fn override_drops_unused_svelte_events_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_svelte_events
.push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
path: PathBuf::from("/project/src/legacy/Widget.svelte"),
component_name: "Widget".to_string(),
event_name: "legacy".to_string(),
line: 10,
col: 0,
}));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_svelte_events = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_svelte_events.is_empty());
}
#[test]
fn override_drops_unused_server_actions_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_server_actions
.push(UnusedServerActionFinding::with_actions(
UnusedServerAction {
path: PathBuf::from("/project/src/legacy/actions.ts"),
action_name: "deprecatedAction".to_string(),
line: 3,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_server_actions = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_server_actions.is_empty());
}
#[test]
fn override_drops_unused_load_data_keys_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_load_data_keys
.push(UnusedLoadDataKeyFinding::with_actions(UnusedLoadDataKey {
path: PathBuf::from("/project/src/legacy/+page.server.ts"),
key_name: "oldKey".to_string(),
line: 4,
col: 0,
route_dir: None,
}));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_load_data_keys = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_load_data_keys.is_empty());
}
#[test]
fn override_drops_stale_suppression_when_stale_rule_off() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/generated/a.ts", false));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.stale_suppressions = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.stale_suppressions.is_empty());
}
#[test]
fn override_drops_missing_reason_suppression_when_require_reason_off() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/generated/b.ts", true));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.require_suppression_reason = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.stale_suppressions.is_empty());
}
#[test]
fn override_keeps_stale_suppression_for_non_matching_file() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/core/a.ts", false));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.stale_suppressions = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert_eq!(results.stale_suppressions.len(), 1);
}
#[test]
fn override_drops_unresolved_catalog_references_for_matching_file() {
let mut results = AnalysisResults::default();
results.unresolved_catalog_references.push(
UnresolvedCatalogReferenceFinding::with_actions(UnresolvedCatalogReference {
entry_name: "react".to_string(),
catalog_name: "default".to_string(),
path: PathBuf::from("/project/packages/app/package.json"),
line: 5,
available_in_catalogs: vec![],
}),
);
let config = config_with_override_for_rule("packages/app/**", |p| {
p.unresolved_catalog_references = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unresolved_catalog_references.is_empty());
}
#[test]
fn override_drops_empty_catalog_groups_for_matching_file() {
let mut results = AnalysisResults::default();
results
.empty_catalog_groups
.push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
catalog_name: "legacy".to_string(),
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 20,
}));
let config = config_with_override_for_rule("pnpm-workspace.yaml", |p| {
p.empty_catalog_groups = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.empty_catalog_groups.is_empty());
}
#[test]
fn override_drops_unused_dependency_overrides_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_dependency_overrides
.push(UnusedDependencyOverrideFinding::with_actions(
UnusedDependencyOverride {
raw_key: "old-pkg".to_string(),
target_package: "old-pkg".to_string(),
parent_package: None,
version_constraint: None,
version_range: "^2.0.0".to_string(),
source: DependencyOverrideSource::PnpmWorkspaceYaml,
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 8,
hint: None,
},
));
let config = config_with_override_for_rule("pnpm-workspace.yaml", |p| {
p.unused_dependency_overrides = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.unused_dependency_overrides.is_empty());
}
#[test]
fn override_drops_misconfigured_dependency_overrides_for_matching_file() {
let mut results = AnalysisResults::default();
results.misconfigured_dependency_overrides.push(
MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
raw_key: "bad>".to_string(),
target_package: None,
raw_value: "1.0.0".to_string(),
reason: DependencyOverrideMisconfigReason::UnparsableKey,
source: DependencyOverrideSource::PnpmWorkspaceYaml,
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 12,
}),
);
let config = config_with_override_for_rule("pnpm-workspace.yaml", |p| {
p.misconfigured_dependency_overrides = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.misconfigured_dependency_overrides.is_empty());
}
#[test]
fn override_drops_invalid_client_exports_for_matching_file() {
let mut results = AnalysisResults::default();
results
.invalid_client_exports
.push(InvalidClientExportFinding::with_actions(
InvalidClientExport {
path: PathBuf::from("/project/src/app/page.ts"),
export_name: "metadata".to_string(),
directive: "use client".to_string(),
line: 3,
col: 0,
},
));
let config = config_with_override_for_rule("src/app/**", |p| {
p.invalid_client_export = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.invalid_client_exports.is_empty());
}
#[test]
fn override_drops_mixed_client_server_barrels_for_matching_file() {
let mut results = AnalysisResults::default();
results
.mixed_client_server_barrels
.push(MixedClientServerBarrelFinding::with_actions(
MixedClientServerBarrel {
path: PathBuf::from("/project/src/app/index.ts"),
client_origin: "./client".to_string(),
server_origin: "./server".to_string(),
line: 2,
col: 0,
},
));
let config = config_with_override_for_rule("src/app/**", |p| {
p.mixed_client_server_barrel = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.mixed_client_server_barrels.is_empty());
}
#[test]
fn override_drops_misplaced_directives_for_matching_file() {
let mut results = AnalysisResults::default();
results
.misplaced_directives
.push(MisplacedDirectiveFinding::with_actions(
MisplacedDirective {
path: PathBuf::from("/project/src/app/widget.ts"),
directive: "use client".to_string(),
line: 5,
col: 0,
},
));
let config = config_with_override_for_rule("src/app/**", |p| {
p.misplaced_directive = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.misplaced_directives.is_empty());
}
#[test]
fn override_drops_route_collisions_for_matching_file() {
let mut results = AnalysisResults::default();
results
.route_collisions
.push(RouteCollisionFinding::with_actions(RouteCollision {
path: PathBuf::from("/project/src/app/(a)/about/page.tsx"),
url: "/about".to_string(),
conflicting_paths: vec![PathBuf::from("/project/src/app/(b)/about/page.tsx")],
line: 1,
col: 0,
}));
let config = config_with_override_for_rule("src/app/(a)/**", |p| {
p.route_collision = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.route_collisions.is_empty());
}
#[test]
fn override_drops_dynamic_segment_name_conflicts_for_matching_file() {
let mut results = AnalysisResults::default();
results.dynamic_segment_name_conflicts.push(
DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
path: PathBuf::from("/project/src/app/routes/page.tsx"),
position: "/".to_string(),
conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
conflicting_paths: vec![PathBuf::from("/project/src/app/other/page.tsx")],
line: 1,
col: 0,
}),
);
let config = config_with_override_for_rule("src/app/**", |p| {
p.dynamic_segment_name_conflict = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.dynamic_segment_name_conflicts.is_empty());
}
#[test]
fn base_private_type_leaks_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.private_type_leaks
.push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
path: PathBuf::from("/project/src/api.ts"),
export_name: "publicFn".to_string(),
type_name: "_Internal".to_string(),
line: 3,
col: 0,
span_start: 0,
semantic: None,
}));
let rules = RulesConfig {
private_type_leaks: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.private_type_leaks.is_empty());
}
#[test]
fn base_stale_suppression_off_clears_non_missing_reason_suppressions() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/a.ts", false));
let rules = RulesConfig {
stale_suppressions: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.stale_suppressions.is_empty());
}
#[test]
fn base_require_suppression_reason_off_clears_missing_reason_suppressions() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/a.ts", true));
let rules = RulesConfig {
require_suppression_reason: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.stale_suppressions.is_empty());
}
#[test]
fn base_require_suppression_reason_off_keeps_normal_stale_suppressions() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/a.ts", false));
let rules = RulesConfig {
require_suppression_reason: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert_eq!(results.stale_suppressions.len(), 1);
}
#[test]
fn base_invalid_client_export_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.invalid_client_exports
.push(InvalidClientExportFinding::with_actions(
InvalidClientExport {
path: PathBuf::from("/project/src/page.ts"),
export_name: "metadata".to_string(),
directive: "use client".to_string(),
line: 1,
col: 0,
},
));
let rules = RulesConfig {
invalid_client_export: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.invalid_client_exports.is_empty());
}
#[test]
fn base_mixed_client_server_barrel_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.mixed_client_server_barrels
.push(MixedClientServerBarrelFinding::with_actions(
MixedClientServerBarrel {
path: PathBuf::from("/project/src/index.ts"),
client_origin: "./client".to_string(),
server_origin: "./server".to_string(),
line: 1,
col: 0,
},
));
let rules = RulesConfig {
mixed_client_server_barrel: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.mixed_client_server_barrels.is_empty());
}
#[test]
fn base_misplaced_directive_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.misplaced_directives
.push(MisplacedDirectiveFinding::with_actions(
MisplacedDirective {
path: PathBuf::from("/project/src/widget.ts"),
directive: "use client".to_string(),
line: 5,
col: 0,
},
));
let rules = RulesConfig {
misplaced_directive: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.misplaced_directives.is_empty());
}
#[test]
fn base_route_collision_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.route_collisions
.push(RouteCollisionFinding::with_actions(RouteCollision {
path: PathBuf::from("/project/src/app/(a)/about/page.tsx"),
url: "/about".to_string(),
conflicting_paths: vec![PathBuf::from("/project/src/app/(b)/about/page.tsx")],
line: 1,
col: 0,
}));
let rules = RulesConfig {
route_collision: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.route_collisions.is_empty());
}
#[test]
fn base_dynamic_segment_name_conflict_off_clears_findings() {
let mut results = AnalysisResults::default();
results.dynamic_segment_name_conflicts.push(
DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
path: PathBuf::from("/project/src/app/[id]/page.tsx"),
position: "/".to_string(),
conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
conflicting_paths: vec![PathBuf::from("/project/src/app/[slug]/page.tsx")],
line: 1,
col: 0,
}),
);
let rules = RulesConfig {
dynamic_segment_name_conflict: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.dynamic_segment_name_conflicts.is_empty());
}
#[test]
fn override_drops_boundary_call_violations_for_matching_file() {
let mut results = AnalysisResults::default();
results
.boundary_call_violations
.push(BoundaryCallViolationFinding::with_actions(
BoundaryCallViolation {
path: PathBuf::from("/project/src/ui/Component.ts"),
line: 10,
col: 0,
zone: "ui".to_string(),
callee: "cp.exec".to_string(),
pattern: "child_process.*".to_string(),
},
));
let config = config_with_override_for_rule("src/ui/**", |p| {
p.boundary_violation = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.boundary_call_violations.is_empty());
}
#[test]
fn override_keeps_boundary_call_violations_for_non_matching_file() {
let mut results = AnalysisResults::default();
results
.boundary_call_violations
.push(BoundaryCallViolationFinding::with_actions(
BoundaryCallViolation {
path: PathBuf::from("/project/src/core/Service.ts"),
line: 10,
col: 0,
zone: "core".to_string(),
callee: "cp.exec".to_string(),
pattern: "child_process.*".to_string(),
},
));
let config = config_with_override_for_rule("src/ui/**", |p| {
p.boundary_violation = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert_eq!(results.boundary_call_violations.len(), 1);
}
#[test]
fn override_drops_policy_violations_for_matching_file() {
let mut results = AnalysisResults::default();
results
.policy_violations
.push(PolicyViolationFinding::with_actions(PolicyViolation {
path: PathBuf::from("/project/src/ui/Component.ts"),
line: 5,
col: 0,
pack: "no-exec".to_string(),
rule_id: "exec-banned".to_string(),
kind: PolicyRuleKind::BannedCall,
matched: "cp.exec".to_string(),
severity: PolicyViolationSeverity::Error,
message: None,
}));
let config = config_with_override_for_rule("src/ui/**", |p| {
p.policy_violation = Some(Severity::Off);
});
apply_rules(&mut results, &config);
assert!(results.policy_violations.is_empty());
}
#[test]
fn has_error_override_unused_types_error_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_types
.push(UnusedTypeFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/core/types.ts"),
export_name: "CoreType".to_string(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_types = Some(Severity::Off);
});
let rules = &config.rules;
assert!(has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_types_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_types
.push(UnusedTypeFinding::with_actions(UnusedExport {
path: PathBuf::from("/project/src/generated/types.ts"),
export_name: "GenType".to_string(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
deprecated: false,
deprecated_reason: None,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_types = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_private_type_leaks_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.private_type_leaks
.push(PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
path: PathBuf::from("/project/src/generated/api.ts"),
export_name: "publicFn".to_string(),
type_name: "_Internal".to_string(),
line: 3,
col: 0,
span_start: 0,
semantic: None,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.private_type_leaks = Some(Severity::Warn);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_enum_members_error_for_non_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_enum_members
.push(UnusedEnumMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/core/enums.ts"),
parent_name: "Status".to_string(),
member_name: "Legacy".to_string(),
kind: MemberKind::EnumMember,
line: 8,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_enum_members = Some(Severity::Off);
});
let rules = &config.rules;
assert!(has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_class_members_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_class_members
.push(UnusedClassMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/generated/service.ts"),
parent_name: "Service".to_string(),
member_name: "_hidden".to_string(),
kind: MemberKind::ClassMethod,
line: 20,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_class_members = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_store_members_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_store_members
.push(UnusedStoreMemberFinding::with_actions(UnusedMember {
path: PathBuf::from("/project/src/generated/store.ts"),
parent_name: "useGenStore".to_string(),
member_name: "unused".to_string(),
kind: MemberKind::StoreMember,
line: 5,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unused_store_members = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unprovided_injects_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unprovided_injects
.push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
path: PathBuf::from("/project/src/generated/child.vue"),
key_name: "KEY".to_string(),
framework: "vue".to_string(),
line: 2,
col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unprovided_injects = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unresolved_imports_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unresolved_imports
.push(UnresolvedImportFinding::with_actions(UnresolvedImport {
path: PathBuf::from("/project/src/generated/mod.ts"),
specifier: "./missing".to_string(),
line: 1,
col: 0,
specifier_col: 0,
}));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.unresolved_imports = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unrendered_components_warn_for_non_matching_file() {
let mut results = AnalysisResults::default();
results
.unrendered_components
.push(UnrenderedComponentFinding::with_actions(
UnrenderedComponent {
path: PathBuf::from("/project/src/core/Widget.vue"),
component_name: "Widget".to_string(),
framework: "vue".to_string(),
reachable_via: None,
line: 1,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unrendered_components = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_component_props_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_props
.push(UnusedComponentPropFinding::with_actions(
UnusedComponentProp {
path: PathBuf::from("/project/src/legacy/Card.vue"),
component_name: "Card".to_string(),
prop_name: "oldProp".to_string(),
line: 3,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_props = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_component_emits_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_emits
.push(UnusedComponentEmitFinding::with_actions(
UnusedComponentEmit {
path: PathBuf::from("/project/src/legacy/Button.vue"),
component_name: "Button".to_string(),
emit_name: "old-click".to_string(),
line: 6,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_emits = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_component_inputs_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_inputs
.push(UnusedComponentInputFinding::with_actions(
UnusedComponentInput {
path: PathBuf::from("/project/src/legacy/table.component.ts"),
component_name: "table".to_string(),
input_name: "deprecated".to_string(),
line: 10,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_inputs = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_component_outputs_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_component_outputs
.push(UnusedComponentOutputFinding::with_actions(
UnusedComponentOutput {
path: PathBuf::from("/project/src/legacy/table.component.ts"),
component_name: "table".to_string(),
output_name: "legacyChange".to_string(),
line: 14,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_component_outputs = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_svelte_events_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_svelte_events
.push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
path: PathBuf::from("/project/src/legacy/Widget.svelte"),
component_name: "Widget".to_string(),
event_name: "oldEvent".to_string(),
line: 8,
col: 0,
}));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_svelte_events = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_server_actions_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_server_actions
.push(UnusedServerActionFinding::with_actions(
UnusedServerAction {
path: PathBuf::from("/project/src/legacy/actions.ts"),
action_name: "oldAction".to_string(),
line: 3,
col: 0,
},
));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_server_actions = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unused_load_data_keys_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.unused_load_data_keys
.push(UnusedLoadDataKeyFinding::with_actions(UnusedLoadDataKey {
path: PathBuf::from("/project/src/legacy/+page.server.ts"),
key_name: "oldKey".to_string(),
line: 2,
col: 0,
route_dir: None,
}));
let config = config_with_override_for_rule("src/legacy/**", |p| {
p.unused_load_data_keys = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_stale_suppression_missing_reason_not_error_for_non_matching_file() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/core/a.ts", true));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.require_suppression_reason = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_stale_suppression_missing_reason_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/generated/b.ts", true));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.require_suppression_reason = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_stale_suppression_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/generated/a.ts", false));
let config = config_with_override_for_rule("src/generated/**", |p| {
p.stale_suppressions = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_unresolved_catalog_references_off_for_matching_file() {
let mut results = AnalysisResults::default();
results.unresolved_catalog_references.push(
UnresolvedCatalogReferenceFinding::with_actions(UnresolvedCatalogReference {
entry_name: "react".to_string(),
catalog_name: "default".to_string(),
path: PathBuf::from("/project/packages/app/package.json"),
line: 5,
available_in_catalogs: vec![],
}),
);
let config = config_with_override_for_rule("packages/app/**", |p| {
p.unresolved_catalog_references = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_empty_catalog_groups_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.empty_catalog_groups
.push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
catalog_name: "tools".to_string(),
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 10,
}));
let config = config_with_override_for_rule("pnpm-workspace.yaml", |p| {
p.empty_catalog_groups = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_invalid_client_exports_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.invalid_client_exports
.push(InvalidClientExportFinding::with_actions(
InvalidClientExport {
path: PathBuf::from("/project/src/app/page.ts"),
export_name: "metadata".to_string(),
directive: "use client".to_string(),
line: 1,
col: 0,
},
));
let config = config_with_override_for_rule("src/app/**", |p| {
p.invalid_client_export = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_mixed_client_server_barrel_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.mixed_client_server_barrels
.push(MixedClientServerBarrelFinding::with_actions(
MixedClientServerBarrel {
path: PathBuf::from("/project/src/app/index.ts"),
client_origin: "./client".to_string(),
server_origin: "./server".to_string(),
line: 1,
col: 0,
},
));
let config = config_with_override_for_rule("src/app/**", |p| {
p.mixed_client_server_barrel = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_misplaced_directive_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.misplaced_directives
.push(MisplacedDirectiveFinding::with_actions(
MisplacedDirective {
path: PathBuf::from("/project/src/app/widget.ts"),
directive: "use client".to_string(),
line: 5,
col: 0,
},
));
let config = config_with_override_for_rule("src/app/**", |p| {
p.misplaced_directive = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_route_collision_off_for_matching_file() {
let mut results = AnalysisResults::default();
results
.route_collisions
.push(RouteCollisionFinding::with_actions(RouteCollision {
path: PathBuf::from("/project/src/app/(a)/about/page.tsx"),
url: "/about".to_string(),
conflicting_paths: vec![PathBuf::from("/project/src/app/(b)/about/page.tsx")],
line: 1,
col: 0,
}));
let config = config_with_override_for_rule("src/app/(a)/**", |p| {
p.route_collision = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn has_error_override_dynamic_segment_name_conflict_off_for_matching_file() {
let mut results = AnalysisResults::default();
results.dynamic_segment_name_conflicts.push(
DynamicSegmentNameConflictFinding::with_actions(DynamicSegmentNameConflict {
path: PathBuf::from("/project/src/app/routes/page.tsx"),
position: "/".to_string(),
conflicting_segments: vec!["[id]".to_string(), "[slug]".to_string()],
conflicting_paths: vec![PathBuf::from("/project/src/app/other/page.tsx")],
line: 1,
col: 0,
}),
);
let config = config_with_override_for_rule("src/app/**", |p| {
p.dynamic_segment_name_conflict = Some(Severity::Off);
});
let rules = &config.rules;
assert!(!has_error_severity_issues(
&results,
rules,
Some(&config),
false
));
}
#[test]
fn default_file_scoped_stale_suppression_missing_reason_error() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/a.ts", true));
let rules = RulesConfig {
require_suppression_reason: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn default_file_scoped_stale_suppression_missing_reason_warn_not_error() {
let mut results = AnalysisResults::default();
results
.stale_suppressions
.push(stale_suppression("/project/src/a.ts", true));
let all_warn = RulesConfig {
unused_files: Severity::Warn,
unused_exports: Severity::Warn,
unused_types: Severity::Warn,
private_type_leaks: Severity::Warn,
private_type_leaks_configured: false,
deprecated_exports_in_use: Severity::Warn,
unused_dependencies: Severity::Warn,
unused_dev_dependencies: Severity::Warn,
unused_optional_dependencies: Severity::Warn,
unused_enum_members: Severity::Warn,
unused_class_members: Severity::Warn,
unused_store_members: Severity::Warn,
unprovided_injects: Severity::Warn,
unrendered_components: Severity::Warn,
unused_component_props: Severity::Warn,
unused_component_emits: Severity::Warn,
unused_component_inputs: Severity::Warn,
unused_component_outputs: Severity::Warn,
unused_svelte_events: Severity::Warn,
unused_server_actions: Severity::Warn,
unused_load_data_keys: Severity::Warn,
prop_drilling: Severity::Off,
thin_wrapper: Severity::Off,
duplicate_prop_shape: Severity::Off,
css_token_drift: Severity::Off,
css_duplicate_block: Severity::Off,
css_selector_complexity: Severity::Off,
css_dead_surface: Severity::Off,
css_broken_reference: Severity::Off,
complexity_cyclomatic: Severity::Off,
complexity_cognitive: Severity::Off,
complexity_crap: Severity::Off,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: Severity::Warn,
dev_dependencies_in_production: Severity::Warn,
boundary_violation: Severity::Warn,
circular_dependencies: Severity::Warn,
re_export_cycle: Severity::Warn,
coverage_gaps: Severity::Warn,
feature_flags: Severity::Warn,
stale_suppressions: Severity::Warn,
require_suppression_reason: Severity::Warn,
unused_catalog_entries: Severity::Warn,
empty_catalog_groups: Severity::Warn,
unresolved_catalog_references: Severity::Warn,
unused_dependency_overrides: Severity::Warn,
misconfigured_dependency_overrides: Severity::Warn,
security_client_server_leak: Severity::Off,
security_sink: Severity::Off,
policy_violation: Severity::Warn,
invalid_client_export: Severity::Warn,
mixed_client_server_barrel: Severity::Warn,
misplaced_directive: Severity::Warn,
route_collision: Severity::Warn,
dynamic_segment_name_conflict: Severity::Warn,
};
assert!(!has_error_severity_issues(&results, &all_warn, None, false));
}
#[test]
fn project_level_unused_dep_overrides_error() {
let mut results = AnalysisResults::default();
results
.unused_dependency_overrides
.push(UnusedDependencyOverrideFinding::with_actions(
UnusedDependencyOverride {
raw_key: "old-pkg".to_string(),
target_package: "old-pkg".to_string(),
parent_package: None,
version_constraint: None,
version_range: "^1.0.0".to_string(),
source: DependencyOverrideSource::PnpmWorkspaceYaml,
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 5,
hint: None,
},
));
let rules = RulesConfig {
unused_dependency_overrides: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn project_level_misconfigured_dep_overrides_error() {
let mut results = AnalysisResults::default();
results.misconfigured_dependency_overrides.push(
MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
raw_key: "bad>".to_string(),
target_package: None,
raw_value: "1.0.0".to_string(),
reason: DependencyOverrideMisconfigReason::UnparsableKey,
source: DependencyOverrideSource::PnpmPackageJson,
path: PathBuf::from("/project/package.json"),
line: 3,
}),
);
let rules = RulesConfig {
misconfigured_dependency_overrides: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn project_level_re_export_cycle_error() {
let mut results = AnalysisResults::default();
results
.re_export_cycles
.push(ReExportCycleFinding::with_actions(ReExportCycle {
files: vec![
PathBuf::from("/project/src/a.ts"),
PathBuf::from("/project/src/b.ts"),
],
kind: ReExportCycleKind::MultiNode,
}));
let rules = RulesConfig {
re_export_cycle: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn project_level_unused_catalog_entries_error() {
let mut results = AnalysisResults::default();
results
.unused_catalog_entries
.push(UnusedCatalogEntryFinding::with_actions(
UnusedCatalogEntry {
entry_name: "lodash".to_string(),
catalog_name: "default".to_string(),
path: PathBuf::from("/project/pnpm-workspace.yaml"),
line: 5,
hardcoded_consumers: vec![],
},
));
let rules = RulesConfig {
unused_catalog_entries: Severity::Error,
..RulesConfig::default()
};
assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn project_level_boundary_violation_no_override_error_for_call_violation() {
let mut results = AnalysisResults::default();
results
.boundary_call_violations
.push(BoundaryCallViolationFinding::with_actions(
BoundaryCallViolation {
path: PathBuf::from("/project/src/ui/a.ts"),
line: 1,
col: 0,
zone: "ui".to_string(),
callee: "cp.exec".to_string(),
pattern: "child_process.*".to_string(),
},
));
let rules = RulesConfig::default(); assert!(has_error_severity_issues(&results, &rules, None, false));
}
#[test]
fn promote_policy_finding_warns_flips_warn_to_error() {
let mut results = AnalysisResults::default();
results
.policy_violations
.push(PolicyViolationFinding::with_actions(PolicyViolation {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
pack: "pack".to_string(),
rule_id: "rule".to_string(),
kind: PolicyRuleKind::BannedCall,
matched: "exec".to_string(),
severity: PolicyViolationSeverity::Warn,
message: None,
}));
promote_policy_finding_warns(&mut results);
assert_eq!(
results.policy_violations[0].violation.severity,
PolicyViolationSeverity::Error
);
}
#[test]
fn promote_policy_finding_warns_preserves_error_severity() {
let mut results = AnalysisResults::default();
results
.policy_violations
.push(PolicyViolationFinding::with_actions(PolicyViolation {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
pack: "pack".to_string(),
rule_id: "rule".to_string(),
kind: PolicyRuleKind::BannedCall,
matched: "exec".to_string(),
severity: PolicyViolationSeverity::Error,
message: None,
}));
promote_policy_finding_warns(&mut results);
assert_eq!(
results.policy_violations[0].violation.severity,
PolicyViolationSeverity::Error
);
}
#[test]
fn promote_policy_finding_warns_noop_on_empty() {
let mut results = AnalysisResults::default();
promote_policy_finding_warns(&mut results);
assert!(results.policy_violations.is_empty());
}
#[test]
fn base_unrendered_components_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unrendered_components
.push(UnrenderedComponentFinding::with_actions(
UnrenderedComponent {
path: PathBuf::from("/project/src/Widget.vue"),
component_name: "Widget".to_string(),
framework: "vue".to_string(),
reachable_via: None,
line: 1,
col: 0,
},
));
let rules = RulesConfig {
unrendered_components: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unrendered_components.is_empty());
}
#[test]
fn base_unused_component_props_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_component_props
.push(UnusedComponentPropFinding::with_actions(
UnusedComponentProp {
path: PathBuf::from("/project/src/Card.vue"),
component_name: "Card".to_string(),
prop_name: "color".to_string(),
line: 4,
col: 0,
},
));
let rules = RulesConfig {
unused_component_props: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_component_props.is_empty());
}
#[test]
fn base_unused_component_emits_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_component_emits
.push(UnusedComponentEmitFinding::with_actions(
UnusedComponentEmit {
path: PathBuf::from("/project/src/Button.vue"),
component_name: "Button".to_string(),
emit_name: "click".to_string(),
line: 6,
col: 0,
},
));
let rules = RulesConfig {
unused_component_emits: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_component_emits.is_empty());
}
#[test]
fn base_unused_component_inputs_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_component_inputs
.push(UnusedComponentInputFinding::with_actions(
UnusedComponentInput {
path: PathBuf::from("/project/src/table.component.ts"),
component_name: "table".to_string(),
input_name: "rows".to_string(),
line: 10,
col: 0,
},
));
let rules = RulesConfig {
unused_component_inputs: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_component_inputs.is_empty());
}
#[test]
fn base_unused_component_outputs_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_component_outputs
.push(UnusedComponentOutputFinding::with_actions(
UnusedComponentOutput {
path: PathBuf::from("/project/src/table.component.ts"),
component_name: "table".to_string(),
output_name: "rowClick".to_string(),
line: 14,
col: 0,
},
));
let rules = RulesConfig {
unused_component_outputs: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_component_outputs.is_empty());
}
#[test]
fn base_unused_svelte_events_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_svelte_events
.push(UnusedSvelteEventFinding::with_actions(UnusedSvelteEvent {
path: PathBuf::from("/project/src/Widget.svelte"),
component_name: "Widget".to_string(),
event_name: "toggle".to_string(),
line: 7,
col: 0,
}));
let rules = RulesConfig {
unused_svelte_events: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_svelte_events.is_empty());
}
#[test]
fn base_unused_server_actions_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_server_actions
.push(UnusedServerActionFinding::with_actions(
UnusedServerAction {
path: PathBuf::from("/project/src/actions.ts"),
action_name: "submitForm".to_string(),
line: 2,
col: 0,
},
));
let rules = RulesConfig {
unused_server_actions: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_server_actions.is_empty());
}
#[test]
fn base_unused_load_data_keys_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unused_load_data_keys
.push(UnusedLoadDataKeyFinding::with_actions(UnusedLoadDataKey {
path: PathBuf::from("/project/src/routes/+page.server.ts"),
key_name: "userData".to_string(),
line: 3,
col: 0,
route_dir: Some("src/routes".to_string()),
}));
let rules = RulesConfig {
unused_load_data_keys: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unused_load_data_keys.is_empty());
}
#[test]
fn base_unprovided_injects_off_clears_findings() {
let mut results = AnalysisResults::default();
results
.unprovided_injects
.push(UnprovidedInjectFinding::with_actions(UnprovidedInject {
path: PathBuf::from("/project/src/Child.vue"),
key_name: "MY_KEY".to_string(),
framework: "vue".to_string(),
line: 2,
col: 0,
}));
let rules = RulesConfig {
unprovided_injects: Severity::Off,
..RulesConfig::default()
};
let config = config_with_rules(rules);
apply_rules(&mut results, &config);
assert!(results.unprovided_injects.is_empty());
}
fn config_with_private_type_leak_override() -> ResolvedConfig {
let base = config_with_rules(RulesConfig {
private_type_leaks: Severity::Warn,
..RulesConfig::default()
});
ResolvedConfig {
overrides: vec![fallow_config::ResolvedOverride {
matchers: vec![
globset::Glob::new("src/ui/**")
.expect("override glob compiles")
.compile_matcher(),
],
rules: fallow_config::PartialRulesConfig {
private_type_leaks: Some(Severity::Off),
..fallow_config::PartialRulesConfig::default()
},
}],
..base
}
}
fn private_type_leak_at(path: &str) -> PrivateTypeLeakFinding {
PrivateTypeLeakFinding::with_actions(PrivateTypeLeak {
path: PathBuf::from(path),
export_name: "build".to_string(),
type_name: "Internal".to_string(),
line: 1,
col: 0,
span_start: 0,
semantic: None,
})
}
#[test]
fn override_off_removes_a_finding_appended_after_the_first_pass() {
let config = config_with_private_type_leak_override();
let mut results = AnalysisResults::default();
results
.private_type_leaks
.push(private_type_leak_at("/project/src/lib/util.ts"));
apply_rules(&mut results, &config);
assert_eq!(results.private_type_leaks.len(), 1);
results
.private_type_leaks
.push(private_type_leak_at("/project/src/ui/kit.ts"));
apply_rules(&mut results, &config);
let paths: Vec<PathBuf> = results
.private_type_leaks
.iter()
.map(|finding| finding.leak.path.clone())
.collect();
assert_eq!(paths, vec![PathBuf::from("/project/src/lib/util.ts")]);
}
}