use fallow_config::{ResolvedConfig, RulesConfig, Severity};
pub fn apply_rules(results: &mut fallow_core::results::AnalysisResults, config: &ResolvedConfig) {
let rules = &config.rules;
let has_overrides = !config.overrides.is_empty();
if has_overrides {
apply_file_override_rules(results, config);
apply_boundary_override_rules(results, config);
} else {
apply_base_file_rules(results, rules);
}
if rules.unused_dependencies == Severity::Off {
results.unused_dependencies.clear();
}
if rules.unused_dev_dependencies == Severity::Off {
results.unused_dev_dependencies.clear();
}
if rules.unused_optional_dependencies == Severity::Off {
results.unused_optional_dependencies.clear();
}
if rules.unlisted_dependencies == Severity::Off {
results.unlisted_dependencies.clear();
}
if rules.duplicate_exports == Severity::Off {
results.duplicate_exports.clear();
}
if rules.type_only_dependencies == Severity::Off {
results.type_only_dependencies.clear();
}
if rules.test_only_dependencies == Severity::Off {
results.test_only_dependencies.clear();
}
if rules.circular_dependencies == Severity::Off {
results.circular_dependencies.clear();
}
if rules.re_export_cycle == Severity::Off {
results.re_export_cycles.clear();
}
if rules.boundary_violation == Severity::Off {
results.boundary_violations.clear();
results.boundary_coverage_violations.clear();
results.boundary_call_violations.clear();
}
if rules.policy_violation == Severity::Off {
results.policy_violations.clear();
}
if rules.unused_catalog_entries == Severity::Off {
results.unused_catalog_entries.clear();
}
if rules.empty_catalog_groups == Severity::Off {
results.empty_catalog_groups.clear();
}
if rules.unresolved_catalog_references == Severity::Off {
results.unresolved_catalog_references.clear();
}
if rules.unused_dependency_overrides == Severity::Off {
results.unused_dependency_overrides.clear();
}
if rules.misconfigured_dependency_overrides == Severity::Off {
results.misconfigured_dependency_overrides.clear();
}
}
fn apply_file_override_rules(
results: &mut fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) {
apply_dead_code_override_rules(results, config);
apply_catalog_override_rules(results, config);
apply_framework_override_rules(results, config);
apply_circular_override_rules(results, config);
}
fn apply_dead_code_override_rules(
results: &mut fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) {
results
.unused_files
.retain(|f| config.resolve_rules_for_path(&f.file.path).unused_files != Severity::Off);
results
.unused_exports
.retain(|e| config.resolve_rules_for_path(&e.export.path).unused_exports != Severity::Off);
results
.unused_types
.retain(|e| config.resolve_rules_for_path(&e.export.path).unused_types != Severity::Off);
results.private_type_leaks.retain(|e| {
config
.resolve_rules_for_path(&e.leak.path)
.private_type_leaks
!= Severity::Off
});
results.unused_enum_members.retain(|m| {
config
.resolve_rules_for_path(&m.member.path)
.unused_enum_members
!= Severity::Off
});
results.unused_class_members.retain(|m| {
config
.resolve_rules_for_path(&m.member.path)
.unused_class_members
!= Severity::Off
});
results.unused_store_members.retain(|m| {
config
.resolve_rules_for_path(&m.member.path)
.unused_store_members
!= Severity::Off
});
results.unprovided_injects.retain(|f| {
config
.resolve_rules_for_path(&f.inject.path)
.unprovided_injects
!= Severity::Off
});
results.unrendered_components.retain(|c| {
config
.resolve_rules_for_path(&c.component.path)
.unrendered_components
!= Severity::Off
});
results.unused_component_props.retain(|p| {
config
.resolve_rules_for_path(&p.prop.path)
.unused_component_props
!= Severity::Off
});
results.unused_component_emits.retain(|e| {
config
.resolve_rules_for_path(&e.emit.path)
.unused_component_emits
!= Severity::Off
});
results.unused_component_inputs.retain(|i| {
config
.resolve_rules_for_path(&i.input.path)
.unused_component_inputs
!= Severity::Off
});
results.unused_component_outputs.retain(|o| {
config
.resolve_rules_for_path(&o.output.path)
.unused_component_outputs
!= Severity::Off
});
results.unused_svelte_events.retain(|e| {
config
.resolve_rules_for_path(&e.event.path)
.unused_svelte_events
!= Severity::Off
});
results.unused_server_actions.retain(|a| {
config
.resolve_rules_for_path(&a.action.path)
.unused_server_actions
!= Severity::Off
});
results.unused_load_data_keys.retain(|k| {
config
.resolve_rules_for_path(&k.key.path)
.unused_load_data_keys
!= Severity::Off
});
results.unresolved_imports.retain(|i| {
config
.resolve_rules_for_path(&i.import.path)
.unresolved_imports
!= Severity::Off
});
}
fn apply_catalog_override_rules(
results: &mut fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) {
results.stale_suppressions.retain(|s| {
let rules = config.resolve_rules_for_path(&s.path);
if s.missing_reason {
rules.require_suppression_reason != Severity::Off
} else {
rules.stale_suppressions != Severity::Off
}
});
results.unresolved_catalog_references.retain(|r| {
config
.resolve_rules_for_path(&r.reference.path)
.unresolved_catalog_references
!= Severity::Off
});
results.empty_catalog_groups.retain(|g| {
config
.resolve_rules_for_path(&g.group.path)
.empty_catalog_groups
!= Severity::Off
});
results.unused_dependency_overrides.retain(|o| {
config
.resolve_rules_for_path(&o.entry.path)
.unused_dependency_overrides
!= Severity::Off
});
results.misconfigured_dependency_overrides.retain(|o| {
config
.resolve_rules_for_path(&o.entry.path)
.misconfigured_dependency_overrides
!= Severity::Off
});
}
fn apply_framework_override_rules(
results: &mut fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) {
results.invalid_client_exports.retain(|e| {
config
.resolve_rules_for_path(&e.export.path)
.invalid_client_export
!= Severity::Off
});
results.mixed_client_server_barrels.retain(|b| {
config
.resolve_rules_for_path(&b.barrel.path)
.mixed_client_server_barrel
!= Severity::Off
});
results.misplaced_directives.retain(|d| {
config
.resolve_rules_for_path(&d.directive_site.path)
.misplaced_directive
!= Severity::Off
});
results.route_collisions.retain(|c| {
config
.resolve_rules_for_path(&c.collision.path)
.route_collision
!= Severity::Off
});
results.dynamic_segment_name_conflicts.retain(|c| {
config
.resolve_rules_for_path(&c.conflict.path)
.dynamic_segment_name_conflict
!= Severity::Off
});
}
fn apply_circular_override_rules(
results: &mut fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) {
results.circular_dependencies.retain(|c| {
c.cycle
.files
.iter()
.any(|path| config.resolve_rules_for_path(path).circular_dependencies != Severity::Off)
});
}
fn apply_base_file_rules(results: &mut fallow_core::results::AnalysisResults, rules: &RulesConfig) {
if rules.unused_files == Severity::Off {
results.unused_files.clear();
}
if rules.unused_exports == Severity::Off {
results.unused_exports.clear();
}
if rules.unused_types == Severity::Off {
results.unused_types.clear();
}
if rules.private_type_leaks == Severity::Off {
results.private_type_leaks.clear();
}
if rules.unused_enum_members == Severity::Off {
results.unused_enum_members.clear();
}
if rules.unused_class_members == Severity::Off {
results.unused_class_members.clear();
}
if rules.unused_store_members == Severity::Off {
results.unused_store_members.clear();
}
if rules.unprovided_injects == Severity::Off {
results.unprovided_injects.clear();
}
if rules.unrendered_components == Severity::Off {
results.unrendered_components.clear();
}
if rules.unused_component_props == Severity::Off {
results.unused_component_props.clear();
}
if rules.unused_component_emits == Severity::Off {
results.unused_component_emits.clear();
}
if rules.unused_component_inputs == Severity::Off {
results.unused_component_inputs.clear();
}
if rules.unused_component_outputs == Severity::Off {
results.unused_component_outputs.clear();
}
if rules.unused_svelte_events == Severity::Off {
results.unused_svelte_events.clear();
}
if rules.unused_server_actions == Severity::Off {
results.unused_server_actions.clear();
}
if rules.unused_load_data_keys == Severity::Off {
results.unused_load_data_keys.clear();
}
if rules.unresolved_imports == Severity::Off {
results.unresolved_imports.clear();
}
results.stale_suppressions.retain(|s| {
if s.missing_reason {
rules.require_suppression_reason != Severity::Off
} else {
rules.stale_suppressions != Severity::Off
}
});
if rules.invalid_client_export == Severity::Off {
results.invalid_client_exports.clear();
}
if rules.mixed_client_server_barrel == Severity::Off {
results.mixed_client_server_barrels.clear();
}
if rules.misplaced_directive == Severity::Off {
results.misplaced_directives.clear();
}
if rules.route_collision == Severity::Off {
results.route_collisions.clear();
}
if rules.dynamic_segment_name_conflict == Severity::Off {
results.dynamic_segment_name_conflicts.clear();
}
}
fn apply_boundary_override_rules(
results: &mut fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) {
results.boundary_violations.retain(|v| {
config
.resolve_rules_for_path(&v.violation.from_path)
.boundary_violation
!= Severity::Off
});
results.boundary_coverage_violations.retain(|v| {
config
.resolve_rules_for_path(&v.violation.path)
.boundary_violation
!= Severity::Off
});
results.boundary_call_violations.retain(|v| {
config
.resolve_rules_for_path(&v.violation.path)
.boundary_violation
!= Severity::Off
});
results.policy_violations.retain(|v| {
config
.resolve_rules_for_path(&v.violation.path)
.policy_violation
!= Severity::Off
});
}
pub fn has_error_severity_issues(
results: &fallow_core::results::AnalysisResults,
rules: &RulesConfig,
config: Option<&ResolvedConfig>,
) -> bool {
let has_overrides = config.is_some_and(|c| !c.overrides.is_empty());
let file_scoped_errors = if let Some(config) = config.filter(|c| !c.overrides.is_empty()) {
has_override_file_scoped_error(results, config)
} else {
has_default_file_scoped_error(results, rules)
};
file_scoped_errors || has_project_level_error(results, rules, has_overrides)
}
fn has_override_file_scoped_error(
results: &fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) -> bool {
has_override_dead_code_error(results, config)
|| has_override_catalog_boundary_error(results, config)
|| has_override_framework_error(results, config)
}
fn has_override_dead_code_error(
results: &fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) -> bool {
results
.unused_files
.iter()
.any(|f| config.resolve_rules_for_path(&f.file.path).unused_files == Severity::Error)
|| results.unused_exports.iter().any(|e| {
config.resolve_rules_for_path(&e.export.path).unused_exports == Severity::Error
})
|| results
.unused_types
.iter()
.any(|e| config.resolve_rules_for_path(&e.export.path).unused_types == Severity::Error)
|| results.private_type_leaks.iter().any(|e| {
config
.resolve_rules_for_path(&e.leak.path)
.private_type_leaks
== Severity::Error
})
|| results.unused_enum_members.iter().any(|m| {
config
.resolve_rules_for_path(&m.member.path)
.unused_enum_members
== Severity::Error
})
|| results.unused_class_members.iter().any(|m| {
config
.resolve_rules_for_path(&m.member.path)
.unused_class_members
== Severity::Error
})
|| results.unused_store_members.iter().any(|m| {
config
.resolve_rules_for_path(&m.member.path)
.unused_store_members
== Severity::Error
})
|| results.unprovided_injects.iter().any(|f| {
config
.resolve_rules_for_path(&f.inject.path)
.unprovided_injects
== Severity::Error
})
|| results.unrendered_components.iter().any(|c| {
config
.resolve_rules_for_path(&c.component.path)
.unrendered_components
== Severity::Error
})
|| results.unused_component_props.iter().any(|p| {
config
.resolve_rules_for_path(&p.prop.path)
.unused_component_props
== Severity::Error
})
|| results.unused_component_emits.iter().any(|e| {
config
.resolve_rules_for_path(&e.emit.path)
.unused_component_emits
== Severity::Error
})
|| results.unused_component_inputs.iter().any(|i| {
config
.resolve_rules_for_path(&i.input.path)
.unused_component_inputs
== Severity::Error
})
|| results.unused_component_outputs.iter().any(|o| {
config
.resolve_rules_for_path(&o.output.path)
.unused_component_outputs
== Severity::Error
})
|| results.unused_svelte_events.iter().any(|e| {
config
.resolve_rules_for_path(&e.event.path)
.unused_svelte_events
== Severity::Error
})
|| results.unused_server_actions.iter().any(|a| {
config
.resolve_rules_for_path(&a.action.path)
.unused_server_actions
== Severity::Error
})
|| results.unused_load_data_keys.iter().any(|k| {
config
.resolve_rules_for_path(&k.key.path)
.unused_load_data_keys
== Severity::Error
})
|| results.unresolved_imports.iter().any(|i| {
config
.resolve_rules_for_path(&i.import.path)
.unresolved_imports
== Severity::Error
})
}
fn has_override_catalog_boundary_error(
results: &fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) -> bool {
results.stale_suppressions.iter().any(|s| {
let rules = config.resolve_rules_for_path(&s.path);
if s.missing_reason {
rules.require_suppression_reason == Severity::Error
} else {
rules.stale_suppressions == Severity::Error
}
}) || results.unresolved_catalog_references.iter().any(|r| {
config
.resolve_rules_for_path(&r.reference.path)
.unresolved_catalog_references
== Severity::Error
}) || results.empty_catalog_groups.iter().any(|g| {
config
.resolve_rules_for_path(&g.group.path)
.empty_catalog_groups
== Severity::Error
}) || results.boundary_coverage_violations.iter().any(|v| {
config
.resolve_rules_for_path(&v.violation.path)
.boundary_violation
== Severity::Error
}) || results.boundary_call_violations.iter().any(|v| {
config
.resolve_rules_for_path(&v.violation.path)
.boundary_violation
== Severity::Error
}) || results.circular_dependencies.iter().any(|c| {
c.cycle.files.iter().any(|path| {
config.resolve_rules_for_path(path).circular_dependencies == Severity::Error
})
})
}
fn has_override_framework_error(
results: &fallow_core::results::AnalysisResults,
config: &ResolvedConfig,
) -> bool {
results.invalid_client_exports.iter().any(|e| {
config
.resolve_rules_for_path(&e.export.path)
.invalid_client_export
== Severity::Error
}) || results.mixed_client_server_barrels.iter().any(|b| {
config
.resolve_rules_for_path(&b.barrel.path)
.mixed_client_server_barrel
== Severity::Error
}) || results.misplaced_directives.iter().any(|d| {
config
.resolve_rules_for_path(&d.directive_site.path)
.misplaced_directive
== Severity::Error
}) || results.route_collisions.iter().any(|c| {
config
.resolve_rules_for_path(&c.collision.path)
.route_collision
== Severity::Error
}) || results.dynamic_segment_name_conflicts.iter().any(|c| {
config
.resolve_rules_for_path(&c.conflict.path)
.dynamic_segment_name_conflict
== Severity::Error
})
}
fn has_default_file_scoped_error(
results: &fallow_core::results::AnalysisResults,
rules: &RulesConfig,
) -> bool {
(rules.unused_files == Severity::Error && !results.unused_files.is_empty())
|| (rules.unused_exports == Severity::Error && !results.unused_exports.is_empty())
|| (rules.unused_types == Severity::Error && !results.unused_types.is_empty())
|| (rules.private_type_leaks == Severity::Error && !results.private_type_leaks.is_empty())
|| (rules.unused_enum_members == Severity::Error && !results.unused_enum_members.is_empty())
|| (rules.unused_class_members == Severity::Error
&& !results.unused_class_members.is_empty())
|| (rules.unused_store_members == Severity::Error
&& !results.unused_store_members.is_empty())
|| (rules.unprovided_injects == Severity::Error && !results.unprovided_injects.is_empty())
|| (rules.unrendered_components == Severity::Error
&& !results.unrendered_components.is_empty())
|| (rules.unused_component_props == Severity::Error
&& !results.unused_component_props.is_empty())
|| (rules.unused_component_emits == Severity::Error
&& !results.unused_component_emits.is_empty())
|| (rules.unused_component_inputs == Severity::Error
&& !results.unused_component_inputs.is_empty())
|| (rules.unused_component_outputs == Severity::Error
&& !results.unused_component_outputs.is_empty())
|| (rules.unused_svelte_events == Severity::Error
&& !results.unused_svelte_events.is_empty())
|| (rules.unused_server_actions == Severity::Error
&& !results.unused_server_actions.is_empty())
|| (rules.unused_load_data_keys == Severity::Error
&& !results.unused_load_data_keys.is_empty())
|| (rules.unresolved_imports == Severity::Error && !results.unresolved_imports.is_empty())
|| results.stale_suppressions.iter().any(|s| {
if s.missing_reason {
rules.require_suppression_reason == Severity::Error
} else {
rules.stale_suppressions == Severity::Error
}
})
|| (rules.unresolved_catalog_references == Severity::Error
&& !results.unresolved_catalog_references.is_empty())
|| (rules.empty_catalog_groups == Severity::Error
&& !results.empty_catalog_groups.is_empty())
|| (rules.invalid_client_export == Severity::Error
&& !results.invalid_client_exports.is_empty())
|| (rules.mixed_client_server_barrel == Severity::Error
&& !results.mixed_client_server_barrels.is_empty())
|| (rules.misplaced_directive == Severity::Error
&& !results.misplaced_directives.is_empty())
|| (rules.route_collision == Severity::Error && !results.route_collisions.is_empty())
|| (rules.dynamic_segment_name_conflict == Severity::Error
&& !results.dynamic_segment_name_conflicts.is_empty())
}
fn has_project_level_error(
results: &fallow_core::results::AnalysisResults,
rules: &RulesConfig,
has_overrides: bool,
) -> bool {
(rules.unused_dependencies == Severity::Error && !results.unused_dependencies.is_empty())
|| (rules.unused_dev_dependencies == Severity::Error
&& !results.unused_dev_dependencies.is_empty())
|| (rules.unused_optional_dependencies == Severity::Error
&& !results.unused_optional_dependencies.is_empty())
|| (rules.unlisted_dependencies == Severity::Error
&& !results.unlisted_dependencies.is_empty())
|| (rules.duplicate_exports == Severity::Error && !results.duplicate_exports.is_empty())
|| (rules.type_only_dependencies == Severity::Error
&& !results.type_only_dependencies.is_empty())
|| (rules.test_only_dependencies == Severity::Error
&& !results.test_only_dependencies.is_empty())
|| (!has_overrides
&& rules.circular_dependencies == Severity::Error
&& !results.circular_dependencies.is_empty())
|| (rules.re_export_cycle == Severity::Error && !results.re_export_cycles.is_empty())
|| (!has_overrides
&& rules.boundary_violation == Severity::Error
&& !results.boundary_violations.is_empty())
|| (!has_overrides
&& rules.boundary_violation == Severity::Error
&& !results.boundary_coverage_violations.is_empty())
|| (!has_overrides
&& rules.boundary_violation == Severity::Error
&& !results.boundary_call_violations.is_empty())
|| (rules.unused_catalog_entries == Severity::Error
&& !results.unused_catalog_entries.is_empty())
|| (rules.empty_catalog_groups == Severity::Error
&& !results.empty_catalog_groups.is_empty())
|| (rules.unused_dependency_overrides == Severity::Error
&& !results.unused_dependency_overrides.is_empty())
|| (rules.misconfigured_dependency_overrides == Severity::Error
&& !results.misconfigured_dependency_overrides.is_empty())
|| results
.policy_violations
.iter()
.any(|v| v.violation.severity == fallow_core::results::PolicyViolationSeverity::Error)
}
pub fn promote_warns_to_errors(rules: &mut RulesConfig) {
for rule in [
&mut rules.unused_files,
&mut rules.unused_exports,
&mut rules.unused_types,
&mut rules.private_type_leaks,
&mut rules.unused_dependencies,
&mut rules.unused_dev_dependencies,
&mut rules.unused_optional_dependencies,
&mut rules.unused_enum_members,
&mut rules.unused_class_members,
&mut rules.unused_store_members,
&mut rules.unprovided_injects,
&mut rules.unrendered_components,
&mut rules.unused_component_props,
&mut rules.unused_component_emits,
&mut rules.unused_component_inputs,
&mut rules.unused_component_outputs,
&mut rules.unused_svelte_events,
&mut rules.unused_server_actions,
&mut rules.unused_load_data_keys,
&mut rules.unresolved_imports,
&mut rules.unlisted_dependencies,
&mut rules.duplicate_exports,
&mut rules.type_only_dependencies,
&mut rules.test_only_dependencies,
&mut rules.circular_dependencies,
&mut rules.re_export_cycle,
&mut rules.boundary_violation,
&mut rules.coverage_gaps,
&mut rules.stale_suppressions,
&mut rules.require_suppression_reason,
&mut rules.unused_catalog_entries,
&mut rules.empty_catalog_groups,
&mut rules.unresolved_catalog_references,
&mut rules.unused_dependency_overrides,
&mut rules.misconfigured_dependency_overrides,
&mut rules.policy_violation,
&mut rules.invalid_client_export,
&mut rules.mixed_client_server_barrel,
&mut rules.misplaced_directive,
&mut rules.route_collision,
&mut rules.dynamic_segment_name_conflict,
] {
promote_warn_to_error(rule);
}
}
fn promote_warn_to_error(rule: &mut Severity) {
if *rule == Severity::Warn {
*rule = Severity::Error;
}
}
pub fn promote_policy_finding_warns(results: &mut fallow_core::results::AnalysisResults) {
use fallow_core::results::PolicyViolationSeverity;
for finding in &mut results.policy_violations {
if finding.violation.severity == PolicyViolationSeverity::Warn {
finding.violation.severity = PolicyViolationSeverity::Error;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type RuleFieldSetter = fn(&mut RulesConfig);
type ResultFieldCheck = fn(&AnalysisResults) -> bool;
use fallow_core::extract::MemberKind;
use fallow_core::results::*;
use std::path::PathBuf;
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,
}));
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,
}));
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,
extends: vec![],
entry: vec![],
ignore_patterns: 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![],
duplicates: fallow_config::DuplicatesConfig::default(),
health: fallow_config::HealthConfig::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,
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,
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,
unresolved_imports: Severity::Off,
unlisted_dependencies: Severity::Off,
duplicate_exports: Severity::Off,
type_only_dependencies: Severity::Off,
test_only_dependencies: 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.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));
}
#[test]
fn error_severity_with_issues_returns_true() {
let results = make_results();
let rules = RulesConfig::default(); assert!(has_error_severity_issues(&results, &rules, None));
}
#[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,
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,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: 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));
}
#[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,
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,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: 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));
rules.unused_files = Severity::Error;
assert!(has_error_severity_issues(&results, &rules, None));
}
#[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));
}
fn config_with_test_override() -> ResolvedConfig {
fallow_config::FallowConfig {
schema: None,
extends: vec![],
entry: vec![],
ignore_patterns: 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![],
duplicates: fallow_config::DuplicatesConfig::default(),
health: fallow_config::HealthConfig::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,
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,
extends: vec![],
entry: vec![],
ignore_patterns: 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![],
duplicates: fallow_config::DuplicatesConfig::default(),
health: fallow_config::HealthConfig::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,
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,
extends: vec![],
entry: vec![],
ignore_patterns: 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![],
duplicates: fallow_config::DuplicatesConfig::default(),
health: fallow_config::HealthConfig::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,
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,
}));
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,
}));
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,
}));
let config = config_with_test_override();
let rules = &config.rules;
assert!(
!has_error_severity_issues(&results, rules, Some(&config)),
"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,
}));
let config = config_with_test_override();
let rules = &config.rules;
assert!(
has_error_severity_issues(&results, rules, Some(&config)),
"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)),
"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)),
"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)),
"boundary findings downgraded to Warn should not produce an Error verdict"
);
}
#[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,
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,
unresolved_imports: Severity::Warn,
unlisted_dependencies: Severity::Warn,
duplicate_exports: Severity::Warn,
type_only_dependencies: Severity::Warn,
test_only_dependencies: 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.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,
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,
unresolved_imports: Severity::Off,
unlisted_dependencies: Severity::Off,
duplicate_exports: Severity::Off,
type_only_dependencies: Severity::Off,
test_only_dependencies: 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));
}
#[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));
}
#[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));
}
#[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));
}
#[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));
}
#[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));
}
}