#![cfg(feature = "checks")]
use std::collections::BTreeMap;
use std::sync::Arc;
use pedant_core::check_config::{
CheckConfig, ConfigFile, ItemVisibilityRule, NamingCheck, PathOverride, PatternCheck,
};
use pedant_core::lint::analyze;
use pedant_core::violation::{Severity, ViolationType};
use pedant_core::{Config, lint_file, lint_str};
fn default_config() -> CheckConfig {
CheckConfig::default()
}
fn permissive_config() -> CheckConfig {
CheckConfig {
max_depth: 10,
forbid_unsafe: false,
..default_config()
}
}
#[test]
fn test_nested_if_detection() {
let source = include_str!("fixtures/nested_if.rs");
let violations = analyze("nested_if.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].violation_type, ViolationType::NestedIf);
assert_eq!(violations[0].line, 3);
}
#[test]
fn test_if_in_match_detection() {
let source = include_str!("fixtures/if_in_match.rs");
let violations = analyze("if_in_match.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].violation_type, ViolationType::IfInMatch);
assert_eq!(violations[0].line, 4);
}
#[test]
fn test_nested_match_detection() {
let source = include_str!("fixtures/nested_match.rs");
let violations = analyze("nested_match.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].violation_type, ViolationType::NestedMatch);
assert_eq!(violations[0].line, 4);
}
#[test]
fn test_deep_nesting_detection() {
let source = include_str!("fixtures/deep_nesting.rs");
let violations = analyze("deep_nesting.rs", source, &default_config(), None)
.unwrap()
.violations;
let max_depth_violations: Vec<_> = violations
.iter()
.filter(|v| v.violation_type == ViolationType::MaxDepth)
.collect();
assert!(!max_depth_violations.is_empty());
}
#[test]
fn test_else_chain_detection() {
let source = include_str!("fixtures/else_chain.rs");
let violations = analyze("else_chain.rs", source, &permissive_config(), None)
.unwrap()
.violations;
let else_chain_violations: Vec<_> = violations
.iter()
.filter(|v| v.violation_type == ViolationType::ElseChain)
.collect();
assert_eq!(else_chain_violations.len(), 1);
}
#[test]
fn test_clean_code_no_violations() {
let source = include_str!("fixtures/clean.rs");
let violations = analyze("clean.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(violations.is_empty());
}
#[test]
fn test_disabled_checks() {
let source = include_str!("fixtures/nested_if.rs");
let config = CheckConfig {
check_nested_if: false,
..permissive_config()
};
let violations = analyze("nested_if.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| v.violation_type != ViolationType::NestedIf)
);
}
#[test]
fn test_custom_max_depth() {
let source = include_str!("fixtures/deep_nesting.rs");
let violations = analyze("deep_nesting.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| v.violation_type != ViolationType::MaxDepth)
);
}
#[test]
fn test_forbidden_attribute_detection() {
let source = include_str!("fixtures/forbidden_attr.rs");
let config = CheckConfig {
forbid_attributes: PatternCheck {
enabled: true,
patterns: vec![
Arc::from("allow(dead_code)"),
Arc::from("allow(unused*)"),
Arc::from("allow(clippy::*)"),
]
.into(),
},
..permissive_config()
};
let violations = analyze("forbidden_attr.rs", source, &config, None)
.unwrap()
.violations;
assert_eq!(violations.len(), 3);
assert!(
violations
.iter()
.all(|v| matches!(v.violation_type, ViolationType::ForbiddenAttribute { .. }))
);
}
#[test]
fn test_forbidden_attribute_disabled() {
let source = include_str!("fixtures/forbidden_attr.rs");
let config = CheckConfig {
forbid_attributes: PatternCheck {
enabled: false,
patterns: vec![Arc::from("allow(dead_code)")].into(),
},
..permissive_config()
};
let violations = analyze("forbidden_attr.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::ForbiddenAttribute { .. }))
);
}
#[test]
fn test_forbidden_type_detection() {
let source = include_str!("fixtures/forbidden_types.rs");
let config = CheckConfig {
forbid_types: PatternCheck {
enabled: true,
patterns: vec![
Arc::from("Arc<String>"),
Arc::from("Arc<Vec<*>>"),
Arc::from("Box<dyn*Error*>"),
]
.into(),
},
..permissive_config()
};
let violations = analyze("forbidden_types.rs", source, &config, None)
.unwrap()
.violations;
let type_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::ForbiddenType { .. }))
.collect();
assert_eq!(type_violations.len(), 4);
}
#[test]
fn test_forbidden_type_disabled() {
let source = include_str!("fixtures/forbidden_types.rs");
let config = CheckConfig {
forbid_types: PatternCheck {
enabled: false,
patterns: vec![Arc::from("Arc<String>")].into(),
},
..permissive_config()
};
let violations = analyze("forbidden_types.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::ForbiddenType { .. }))
);
}
#[test]
fn test_forbidden_call_detection() {
let source = include_str!("fixtures/forbidden_calls.rs");
let config = CheckConfig {
forbid_calls: PatternCheck {
enabled: true,
patterns: vec![
Arc::from(".unwrap()"),
Arc::from(".expect(*)"),
Arc::from(".clone()"),
]
.into(),
},
..permissive_config()
};
let violations = analyze("forbidden_calls.rs", source, &config, None)
.unwrap()
.violations;
let call_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::ForbiddenCall { .. }))
.collect();
assert_eq!(call_violations.len(), 3);
}
#[test]
fn test_forbidden_macro_detection() {
let source = include_str!("fixtures/forbidden_macros.rs");
let config = CheckConfig {
forbid_macros: PatternCheck {
enabled: true,
patterns: vec![
Arc::from("panic!"),
Arc::from("todo!"),
Arc::from("unimplemented!"),
Arc::from("dbg!"),
Arc::from("println!"),
]
.into(),
},
..permissive_config()
};
let violations = analyze("forbidden_macros.rs", source, &config, None)
.unwrap()
.violations;
let macro_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::ForbiddenMacro { .. }))
.collect();
assert_eq!(macro_violations.len(), 5);
}
#[test]
fn test_lint_str_api() {
let source = r#"
fn nested() {
if true {
if false {
println!("nested");
}
}
}
"#;
let config = Config::default();
let violations = lint_str(source, &config).unwrap().violations;
assert!(!violations.is_empty());
assert!(
violations
.iter()
.any(|v| v.violation_type == ViolationType::NestedIf)
);
}
#[test]
fn test_lint_file_api() {
use std::path::Path;
let path = Path::new("tests/fixtures/nested_if.rs");
let config = Config {
max_depth: 10,
..Config::default()
};
let violations = lint_file(path, &config).unwrap().violations;
assert!(!violations.is_empty());
assert!(
violations
.iter()
.any(|v| v.violation_type == ViolationType::NestedIf)
);
}
#[test]
fn test_forbidden_else() {
let source = include_str!("fixtures/forbidden_keywords.rs");
let config = CheckConfig {
forbid_else: true,
..permissive_config()
};
let violations = analyze("forbidden_keywords.rs", source, &config, None)
.unwrap()
.violations;
let else_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::ForbiddenElse))
.collect();
assert_eq!(else_violations.len(), 1);
}
#[test]
fn test_forbidden_unsafe() {
let source = include_str!("fixtures/forbidden_keywords.rs");
let config = CheckConfig {
forbid_unsafe: true,
..permissive_config()
};
let violations = analyze("forbidden_keywords.rs", source, &config, None)
.unwrap()
.violations;
let unsafe_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::ForbiddenUnsafe))
.collect();
assert_eq!(unsafe_violations.len(), 1);
}
#[test]
fn test_dyn_return_detection() {
let source = include_str!("fixtures/dyn_return.rs");
let config = CheckConfig {
check_dyn_return: true,
..permissive_config()
};
let violations = analyze("dyn_return.rs", source, &config, None)
.unwrap()
.violations;
let dyn_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::DynReturn))
.collect();
assert_eq!(dyn_violations.len(), 2);
}
#[test]
fn test_dyn_return_disabled() {
let source = include_str!("fixtures/dyn_return.rs");
let violations = analyze("dyn_return.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::DynReturn))
);
}
#[test]
fn test_dyn_param_detection() {
let source = include_str!("fixtures/dyn_param.rs");
let config = CheckConfig {
check_dyn_param: true,
..permissive_config()
};
let violations = analyze("dyn_param.rs", source, &config, None)
.unwrap()
.violations;
let dyn_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::DynParam))
.collect();
assert_eq!(dyn_violations.len(), 2);
}
#[test]
fn test_dyn_param_disabled() {
let source = include_str!("fixtures/dyn_param.rs");
let violations = analyze("dyn_param.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::DynParam))
);
}
#[test]
fn test_vec_box_dyn_detection() {
let source = include_str!("fixtures/vec_box_dyn.rs");
let config = CheckConfig {
check_vec_box_dyn: true,
..permissive_config()
};
let violations = analyze("vec_box_dyn.rs", source, &config, None)
.unwrap()
.violations;
let vbd_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::VecBoxDyn))
.collect();
assert_eq!(vbd_violations.len(), 2);
}
#[test]
fn test_dyn_field_detection() {
let source = include_str!("fixtures/dyn_field.rs");
let config = CheckConfig {
check_dyn_field: true,
..permissive_config()
};
let violations = analyze("dyn_field.rs", source, &config, None)
.unwrap()
.violations;
let field_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::DynField))
.collect();
assert_eq!(field_violations.len(), 3);
}
#[test]
fn test_dyn_field_disabled() {
let source = include_str!("fixtures/dyn_field.rs");
let violations = analyze("dyn_field.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::DynField))
);
}
#[test]
fn test_clone_in_loop_detection() {
let source = include_str!("fixtures/clone_in_loop.rs");
let config = CheckConfig {
check_clone_in_loop: true,
..permissive_config()
};
let violations = analyze("clone_in_loop.rs", source, &config, None)
.unwrap()
.violations;
let clone_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::CloneInLoop))
.collect();
assert_eq!(clone_violations.len(), 4);
}
#[test]
fn test_clone_in_loop_disabled() {
let source = include_str!("fixtures/clone_in_loop.rs");
let violations = analyze("clone_in_loop.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::CloneInLoop))
);
}
#[test]
fn test_default_hasher_detection() {
let source = include_str!("fixtures/default_hasher.rs");
let config = CheckConfig {
check_default_hasher: true,
..permissive_config()
};
let violations = analyze("default_hasher.rs", source, &config, None)
.unwrap()
.violations;
let hasher_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::DefaultHasher))
.collect();
assert_eq!(hasher_violations.len(), 4);
}
#[test]
fn test_default_hasher_disabled() {
let source = include_str!("fixtures/default_hasher.rs");
let violations = analyze("default_hasher.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::DefaultHasher))
);
}
#[test]
fn test_mixed_concerns_detection() {
let source = include_str!("fixtures/mixed_concerns.rs");
let config = CheckConfig {
check_mixed_concerns: true,
..permissive_config()
};
let violations = analyze("mixed_concerns.rs", source, &config, None)
.unwrap()
.violations;
let mc_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::MixedConcerns))
.collect();
assert_eq!(mc_violations.len(), 1);
assert!(
mc_violations[0]
.message
.contains("disconnected type groups")
);
}
#[test]
fn test_mixed_concerns_clean() {
let source = include_str!("fixtures/mixed_concerns_clean.rs");
let config = CheckConfig {
check_mixed_concerns: true,
..permissive_config()
};
let violations = analyze("mixed_concerns_clean.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::MixedConcerns))
);
}
#[test]
fn test_mixed_concerns_body_coupling() {
let source = include_str!("fixtures/mixed_concerns_body.rs");
let config = CheckConfig {
check_mixed_concerns: true,
..permissive_config()
};
let violations = analyze("mixed_concerns_body.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::MixedConcerns))
);
}
#[test]
fn test_mixed_concerns_connects_generic_alias_targets_and_arguments() {
let source = include_str!("fixtures/mixed_concerns_generic_aliases.rs");
let config = CheckConfig {
check_mixed_concerns: true,
..permissive_config()
};
let violations = analyze("mixed_concerns_generic_aliases.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|violation| !matches!(violation.violation_type, ViolationType::MixedConcerns)),
"generic aliases connect their authority, marker arguments, and consumers: {violations:?}",
);
}
#[test]
fn test_mixed_concerns_disabled() {
let source = include_str!("fixtures/mixed_concerns.rs");
let violations = analyze("mixed_concerns.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::MixedConcerns))
);
}
#[test]
fn test_inline_tests_detection() {
let source = include_str!("fixtures/inline_tests.rs");
let config = CheckConfig {
check_inline_tests: true,
..permissive_config()
};
let violations = analyze("inline_tests.rs", source, &config, None)
.unwrap()
.violations;
let it_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::InlineTests))
.collect();
assert_eq!(it_violations.len(), 1);
assert!(it_violations[0].message.contains("tests"));
}
#[test]
fn test_inline_tests_disabled() {
let source = include_str!("fixtures/inline_tests.rs");
let violations = analyze("inline_tests.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::InlineTests))
);
}
#[test]
fn test_let_underscore_result_detection() {
let source = include_str!("fixtures/let_underscore_result.rs");
let config = CheckConfig {
check_let_underscore_result: true,
..permissive_config()
};
let violations = analyze("let_underscore_result.rs", source, &config, None)
.unwrap()
.violations;
let lur_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::LetUnderscoreResult))
.collect();
assert_eq!(lur_violations.len(), 3);
assert_eq!(lur_violations[0].line, 5);
assert_eq!(lur_violations[1].line, 9);
assert_eq!(lur_violations[2].line, 13);
assert!(
lur_violations
.iter()
.all(|v| v.violation_type.code() == "let-underscore-result")
);
}
#[test]
fn test_let_underscore_result_disabled() {
let source = include_str!("fixtures/let_underscore_result.rs");
let violations = analyze(
"let_underscore_result.rs",
source,
&permissive_config(),
None,
)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::LetUnderscoreResult))
);
}
#[test]
fn test_let_underscore_result_no_init() {
let source = r#"
fn no_init() {
let _;
let x = 5;
}
"#;
let config = CheckConfig {
check_let_underscore_result: true,
..permissive_config()
};
let violations = analyze("no_init.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::LetUnderscoreResult))
);
}
#[test]
fn test_let_underscore_result_write_to_string_suppressed() {
let source = include_str!("fixtures/let_underscore_result_suppressed.rs");
let config = CheckConfig {
check_let_underscore_result: true,
..permissive_config()
};
let violations = analyze("let_underscore_result_suppressed.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::LetUnderscoreResult))
);
}
#[test]
fn test_let_underscore_result_write_to_non_string_not_suppressed() {
let source = r#"
use std::fmt::Write;
fn write_to_vec() {
let mut buf: Vec<u8> = Vec::new();
let _ = write!(buf, "x");
}
fn write_to_unknown() {
let _ = write!(stderr, "y");
}
"#;
let config = CheckConfig {
check_let_underscore_result: true,
..permissive_config()
};
let violations = analyze("non_string.rs", source, &config, None)
.unwrap()
.violations;
let lur_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::LetUnderscoreResult))
.collect();
assert_eq!(lur_violations.len(), 2);
}
#[test]
fn test_let_underscore_result_write_untyped_not_suppressed() {
let source = r#"
use std::fmt::Write;
fn write_to_untyped() {
let mut buf = String::new();
let _ = write!(buf, "x");
}
"#;
let config = CheckConfig {
check_let_underscore_result: true,
..permissive_config()
};
let violations = analyze("untyped.rs", source, &config, None)
.unwrap()
.violations;
let lur_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::LetUnderscoreResult))
.collect();
assert_eq!(lur_violations.len(), 1);
}
#[test]
fn test_high_param_count_detected() {
let source = include_str!("fixtures/high_param_count.rs");
let config = CheckConfig {
check_high_param_count: true,
max_params: 5,
..permissive_config()
};
let violations = analyze("high_param_count.rs", source, &config, None)
.unwrap()
.violations;
let hpc_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::HighParamCount))
.collect();
assert_eq!(hpc_violations.len(), 1);
assert_eq!(hpc_violations[0].line, 1);
}
#[test]
fn test_high_param_count_below_threshold() {
let source = include_str!("fixtures/high_param_count.rs");
let config = CheckConfig {
check_high_param_count: true,
max_params: 6,
..permissive_config()
};
let violations = analyze("high_param_count.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::HighParamCount))
);
}
#[test]
fn test_high_param_count_excludes_self() {
let source = include_str!("fixtures/high_param_count.rs");
let config = CheckConfig {
check_high_param_count: true,
max_params: 5,
..permissive_config()
};
let violations = analyze("high_param_count.rs", source, &config, None)
.unwrap()
.violations;
let hpc_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::HighParamCount))
.collect();
assert_eq!(hpc_violations.len(), 1);
assert!(hpc_violations[0].message.contains("many_params"));
}
#[test]
fn test_high_param_count_disabled_by_default() {
let source = include_str!("fixtures/high_param_count.rs");
let violations = analyze("high_param_count.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::HighParamCount))
);
}
#[test]
fn test_long_function_body_detected() {
let source = include_str!("fixtures/long_function_body.rs");
let config = CheckConfig {
check_long_function_body: true,
max_function_body_lines: 10,
..permissive_config()
};
let violations = analyze("long_function_body.rs", source, &config, None)
.unwrap()
.violations;
let lfb: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::LongFunctionBody))
.collect();
assert_eq!(lfb.len(), 1);
assert_eq!(lfb[0].line, 3);
assert!(lfb[0].message.contains("long_body"));
assert!(lfb[0].message.contains("14 lines"));
}
#[test]
fn test_long_function_body_at_threshold_is_clean() {
let source = include_str!("fixtures/long_function_body.rs");
let config = CheckConfig {
check_long_function_body: true,
max_function_body_lines: 14,
..permissive_config()
};
let violations = analyze("long_function_body.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::LongFunctionBody))
);
}
#[test]
fn test_long_function_body_disabled_by_default() {
let source = include_str!("fixtures/long_function_body.rs");
let config = CheckConfig {
max_function_body_lines: 1,
..permissive_config()
};
let violations = analyze("long_function_body.rs", source, &config, None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::LongFunctionBody))
);
}
fn module_root_config() -> CheckConfig {
CheckConfig {
check_module_root_definitions: true,
..permissive_config()
}
}
#[test]
fn test_module_root_definitions_detected() {
let source = include_str!("fixtures/module_root_definitions.rs");
let violations = analyze("src/lib.rs", source, &module_root_config(), None)
.unwrap()
.violations;
let mrd: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::ModuleRootDefinitions))
.collect();
assert_eq!(mrd.len(), 6);
assert!(mrd.iter().all(|v| !v.message.contains("do_it")));
for needle in [
"struct Local",
"enum Kind",
"union Bytes",
"trait Doer",
"impl Local",
"fn helper",
] {
assert!(
mrd.iter().any(|v| v.message.contains(needle)),
"missing finding for {needle}"
);
}
}
#[test]
fn test_module_root_definitions_clean_root() {
let source = include_str!("fixtures/module_root_clean.rs");
let violations = analyze("src/lib.rs", source, &module_root_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::ModuleRootDefinitions))
);
}
#[test]
fn test_module_root_definitions_ignores_non_root_files() {
let source = include_str!("fixtures/module_root_definitions.rs");
let violations = analyze("src/widgets.rs", source, &module_root_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::ModuleRootDefinitions))
);
}
#[test]
fn test_module_root_definitions_disabled_by_default() {
let source = include_str!("fixtures/module_root_definitions.rs");
let violations = analyze("src/lib.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::ModuleRootDefinitions))
);
}
#[test]
fn test_module_root_definitions_honors_custom_root_list() {
let source = include_str!("fixtures/module_root_definitions.rs");
let config = CheckConfig {
check_module_root_definitions: true,
module_root_files: [Arc::from("prelude.rs")].into(),
..permissive_config()
};
assert!(
analyze("src/lib.rs", source, &config, None)
.unwrap()
.violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::ModuleRootDefinitions))
);
let hits = analyze("src/prelude.rs", source, &config, None)
.unwrap()
.violations
.into_iter()
.filter(|v| matches!(v.violation_type, ViolationType::ModuleRootDefinitions))
.count();
assert_eq!(hits, 6);
}
fn source_with_lines(n: usize) -> String {
(0..n)
.map(|i| format!("const N{i}: usize = {i};\n"))
.collect()
}
fn large_file_config(warn: usize, deny: usize) -> CheckConfig {
CheckConfig {
check_large_source_file: true,
source_file_warn_lines: warn,
source_file_deny_lines: deny,
..permissive_config()
}
}
fn large_source_findings(
source: &str,
config: &CheckConfig,
) -> Vec<pedant_core::violation::Violation> {
analyze("big.rs", source, config, None)
.unwrap()
.violations
.into_iter()
.filter(|v| matches!(v.violation_type, ViolationType::LargeSourceFile))
.collect()
}
#[test]
fn test_large_source_file_deny_tier() {
let source = source_with_lines(30);
let hits = large_source_findings(&source, &large_file_config(10, 20));
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Deny);
assert_eq!(hits[0].line, 1);
assert!(hits[0].message.contains("30 lines"));
assert!(hits[0].message.contains("denial"));
}
#[test]
fn test_large_source_file_warn_tier() {
let source = source_with_lines(15);
let hits = large_source_findings(&source, &large_file_config(10, 20));
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
assert!(hits[0].message.contains("warning"));
}
#[test]
fn test_large_source_file_below_warn_is_clean() {
let source = source_with_lines(5);
assert!(large_source_findings(&source, &large_file_config(10, 20)).is_empty());
}
#[test]
fn test_large_source_file_deny_takes_precedence_over_warn() {
let source = source_with_lines(30);
let hits = large_source_findings(&source, &large_file_config(25, 10));
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Deny);
}
#[test]
fn test_large_source_file_zero_threshold_disables_tier() {
let source = source_with_lines(30);
let hits = large_source_findings(&source, &large_file_config(10, 0));
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].severity, Severity::Warn);
}
#[test]
fn test_large_source_file_disabled_by_default() {
let source = source_with_lines(30);
assert!(large_source_findings(&source, &permissive_config()).is_empty());
}
fn high_method_findings(config: &CheckConfig) -> Vec<pedant_core::violation::Violation> {
let source = include_str!("fixtures/high_method_count.rs");
analyze("engine.rs", source, config, None)
.unwrap()
.violations
.into_iter()
.filter(|v| matches!(v.violation_type, ViolationType::HighMethodCount { .. }))
.collect()
}
#[test]
fn test_high_method_count_flags_god_object_excluding_forwarders() {
let config = CheckConfig {
check_high_method_count: true,
max_methods: 5,
..permissive_config()
};
let hits = high_method_findings(&config);
assert_eq!(hits.len(), 1);
assert!(hits[0].message.contains("Engine"));
assert!(hits[0].message.contains("6 inherent methods"));
}
#[test]
fn test_high_method_count_counts_forwarders_when_configured() {
let config = CheckConfig {
check_high_method_count: true,
count_forwarders: true,
max_methods: 5,
..permissive_config()
};
let hits = high_method_findings(&config);
assert_eq!(hits.len(), 2);
assert!(hits.iter().any(|v| v.message.contains("`Engine`")));
assert!(hits.iter().any(|v| v.message.contains("`Facade`")));
}
#[test]
fn test_high_method_count_below_threshold_is_clean() {
let config = CheckConfig {
check_high_method_count: true,
max_methods: 10,
..permissive_config()
};
assert!(high_method_findings(&config).is_empty());
}
#[test]
fn test_high_method_count_disabled_by_default() {
assert!(high_method_findings(&permissive_config()).is_empty());
}
const VISIBILITY_PATH: &str = "d9x-daemon/src/server/state.rs";
fn vis_rule(kind: &str, name: &str, visibility: &str) -> ItemVisibilityRule {
ItemVisibilityRule {
path: VISIBILITY_PATH.into(),
kind: kind.into(),
name: name.into(),
visibility: visibility.into(),
}
}
fn vis_config(rules: Vec<ItemVisibilityRule>) -> CheckConfig {
CheckConfig {
check_item_visibility_policy: true,
item_visibility_policy: rules.into(),
..permissive_config()
}
}
fn vis_findings(
path: &str,
source: &str,
config: &CheckConfig,
) -> Vec<pedant_core::violation::Violation> {
analyze(path, source, config, None)
.unwrap()
.violations
.into_iter()
.filter(|v| matches!(v.violation_type, ViolationType::ItemVisibilityPolicy { .. }))
.collect()
}
#[test]
fn test_item_visibility_all_labels_clean() {
let source = include_str!("fixtures/item_visibility.rs");
let config = vis_config(vec![
vis_rule("struct", "DaemonState", "pub(super)"),
vis_rule("struct", "Widget", "pub"),
vis_rule("enum", "Mode", "pub(crate)"),
vis_rule("struct", "Scoped", "pub(in crate::server)"),
vis_rule("fn", "helper", "private"),
]);
assert!(vis_findings(VISIBILITY_PATH, source, &config).is_empty());
}
#[test]
fn test_item_visibility_wrong_visibility() {
let source = include_str!("fixtures/item_visibility.rs");
let config = vis_config(vec![vis_rule("struct", "DaemonState", "pub")]);
let hits = vis_findings(VISIBILITY_PATH, source, &config);
assert_eq!(hits.len(), 1);
let detail = hits[0].violation_type.visibility_detail().unwrap();
assert_eq!(&*detail.subject, "DaemonState");
assert_eq!(&*detail.expected, "pub");
assert_eq!(&*detail.observed, "pub(super)");
}
#[test]
fn test_item_visibility_missing() {
let source = include_str!("fixtures/item_visibility.rs");
let config = vis_config(vec![vis_rule("struct", "Absent", "pub")]);
let hits = vis_findings(VISIBILITY_PATH, source, &config);
assert_eq!(hits.len(), 1);
assert_eq!(
&*hits[0].violation_type.visibility_detail().unwrap().observed,
"missing"
);
}
#[test]
fn test_item_visibility_wrong_kind() {
let source = include_str!("fixtures/item_visibility.rs");
let config = vis_config(vec![vis_rule("struct", "Mode", "pub(crate)")]);
let hits = vis_findings(VISIBILITY_PATH, source, &config);
assert_eq!(hits.len(), 1);
assert_eq!(
&*hits[0].violation_type.visibility_detail().unwrap().observed,
"wrong-kind"
);
}
#[test]
fn test_item_visibility_duplicate() {
let config = vis_config(vec![vis_rule("struct", "Dup", "pub")]);
let hits = vis_findings(VISIBILITY_PATH, "pub struct Dup; pub struct Dup;", &config);
assert_eq!(hits.len(), 1);
assert_eq!(
&*hits[0].violation_type.visibility_detail().unwrap().observed,
"duplicate"
);
}
#[test]
fn test_item_visibility_ignores_non_matching_path() {
let source = include_str!("fixtures/item_visibility.rs");
let config = vis_config(vec![vis_rule("struct", "DaemonState", "pub")]);
assert!(vis_findings("other/module.rs", source, &config).is_empty());
}
#[test]
fn test_item_visibility_disabled() {
let source = include_str!("fixtures/item_visibility.rs");
let config = CheckConfig {
check_item_visibility_policy: false,
item_visibility_policy: vec![vis_rule("struct", "DaemonState", "pub")].into(),
..permissive_config()
};
assert!(vis_findings(VISIBILITY_PATH, source, &config).is_empty());
}
#[test]
fn test_item_visibility_json_exposes_expected_and_observed() {
use pedant_core::json_format::JsonViolation;
let source = include_str!("fixtures/item_visibility.rs");
let config = vis_config(vec![vis_rule("struct", "DaemonState", "pub")]);
let hits = vis_findings(VISIBILITY_PATH, source, &config);
let json = serde_json::to_value(JsonViolation::from(&hits[0])).unwrap();
assert_eq!(json["check"], "item-visibility-policy");
assert_eq!(json["subject"], "DaemonState");
assert_eq!(json["expected"], "pub");
assert_eq!(json["observed"], "pub(super)");
}
const TEST_API_SRC_PATH: &str = "mycrate/src/helpers.rs";
fn ungated_findings(path: &str, config: &CheckConfig) -> Vec<pedant_core::violation::Violation> {
let source = include_str!("fixtures/ungated_test_api.rs");
analyze(path, source, config, None)
.unwrap()
.violations
.into_iter()
.filter(|v| matches!(v.violation_type, ViolationType::UngatedTestApi))
.collect()
}
#[test]
fn test_ungated_test_api_flags_ungated_items_only() {
let config = CheckConfig {
check_ungated_test_api: true,
..permissive_config()
};
let hits = ungated_findings(TEST_API_SRC_PATH, &config);
let names: Vec<&str> = hits.iter().map(|v| v.message.as_ref()).collect();
assert_eq!(hits.len(), 3, "got: {names:?}");
assert!(names.iter().any(|m| m.contains("build_for_tests")));
assert!(names.iter().any(|m| m.contains("cleanup_for_tests")));
assert!(names.iter().any(|m| m.contains("scratch_for_tests")));
assert!(names.iter().all(|m| !m.contains("make_fixture_for_tests")));
assert!(names.iter().all(|m| !m.contains("seed_for_tests")));
}
#[test]
fn test_ungated_test_api_honors_configured_feature() {
let config = CheckConfig {
check_ungated_test_api: true,
test_support_feature: Arc::from("internal-only"),
..permissive_config()
};
assert_eq!(ungated_findings(TEST_API_SRC_PATH, &config).len(), 5);
}
#[test]
fn test_ungated_test_api_honors_custom_patterns() {
let config = CheckConfig {
check_ungated_test_api: true,
test_api_patterns: [Arc::from("build_*")].into(),
..permissive_config()
};
let hits = ungated_findings(TEST_API_SRC_PATH, &config);
assert_eq!(hits.len(), 1);
assert!(hits[0].message.contains("build_for_tests"));
}
#[test]
fn test_ungated_test_api_only_under_src() {
let config = CheckConfig {
check_ungated_test_api: true,
..permissive_config()
};
assert!(ungated_findings("mycrate/tests/helpers.rs", &config).is_empty());
}
#[test]
fn test_ungated_test_api_disabled_by_default() {
assert!(ungated_findings(TEST_API_SRC_PATH, &permissive_config()).is_empty());
}
#[test]
fn test_path_override_precedence_matches_documented_behavior() {
let base = CheckConfig::default();
let mut overrides = BTreeMap::new();
overrides.insert(
Box::from("src/**"),
PathOverride {
max_depth: Some(10),
..PathOverride::default()
},
);
overrides.insert(
Box::from("src/hot/**"),
PathOverride {
max_depth: Some(2),
..PathOverride::default()
},
);
let fc = ConfigFile {
overrides,
..ConfigFile::default()
};
let resolved = base
.resolve_for_path("src/hot/inner.rs", Some(&fc))
.expect("override should not disable analysis");
assert_eq!(resolved.max_depth, 2, "most-specific override should win");
let resolved = base
.resolve_for_path("src/other.rs", Some(&fc))
.expect("override should not disable analysis");
assert_eq!(
resolved.max_depth, 10,
"only matching override should apply"
);
let resolved = base
.resolve_for_path("lib/foo.rs", Some(&fc))
.expect("no override, base config returned");
assert_eq!(
resolved.max_depth,
CheckConfig::default().max_depth,
"non-matching path gets base config"
);
}
#[test]
fn test_path_override_disabled_most_specific_wins() {
let base = CheckConfig::default();
let mut overrides = BTreeMap::new();
overrides.insert(
Box::from("tests/**"),
PathOverride {
max_depth: Some(10),
..PathOverride::default()
},
);
overrides.insert(
Box::from("tests/generated/**"),
PathOverride {
enabled: Some(false),
..PathOverride::default()
},
);
let fc = ConfigFile {
overrides,
..ConfigFile::default()
};
let resolved = base.resolve_for_path("tests/generated/foo.rs", Some(&fc));
assert!(
resolved.is_none(),
"most-specific override disables analysis"
);
let resolved = base
.resolve_for_path("tests/unit.rs", Some(&fc))
.expect("broad override applies");
assert_eq!(resolved.max_depth, 10);
}
#[test]
fn test_config_file_rejects_unknown_top_level_key() {
let error = toml::from_str::<ConfigFile>(
r#"
max_depth = 4
unknown_key = true
"#,
)
.expect_err("unknown top-level key should fail");
assert!(error.to_string().contains("unknown field `unknown_key`"));
}
#[test]
fn test_config_file_rejects_unknown_path_override_key() {
let error = toml::from_str::<ConfigFile>(
r#"
[overrides."src/**"]
max_depth = 4
unknown_key = true
"#,
)
.expect_err("unknown override key should fail");
assert!(error.to_string().contains("unknown field `unknown_key`"));
}
fn naming_config() -> CheckConfig {
CheckConfig {
check_naming: NamingCheck {
enabled: true,
..NamingCheck::default()
},
..permissive_config()
}
}
#[test]
fn test_generic_naming_detection() {
let source = include_str!("fixtures/generic_naming.rs");
let violations = analyze("generic_naming.rs", source, &naming_config(), None)
.unwrap()
.violations;
let naming_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::GenericNaming))
.collect();
assert_eq!(naming_violations.len(), 2);
}
#[test]
fn test_generic_naming_disabled() {
let source = include_str!("fixtures/generic_naming.rs");
let violations = analyze("generic_naming.rs", source, &permissive_config(), None)
.unwrap()
.violations;
assert!(
violations
.iter()
.all(|v| !matches!(v.violation_type, ViolationType::GenericNaming))
);
}
#[test]
fn test_generic_naming_custom_config() {
let source = include_str!("fixtures/generic_naming.rs");
let config = CheckConfig {
check_naming: NamingCheck {
enabled: true,
generic_names: vec![Arc::from("config")].into(),
min_generic_count: 1,
..NamingCheck::default()
},
..permissive_config()
};
let violations = analyze("generic_naming.rs", source, &config, None)
.unwrap()
.violations;
let naming_violations: Vec<_> = violations
.iter()
.filter(|v| matches!(v.violation_type, ViolationType::GenericNaming))
.collect();
assert!(
naming_violations
.iter()
.any(|v| v.message.contains("config"))
);
}