use harn_rules::{Applicability, CompiledRule, Rule, RulesError, Safety};
fn compile(toml: &str) -> CompiledRule {
CompiledRule::compile(&Rule::from_toml_str(toml).expect("parses")).expect("compiles")
}
fn codemod(safety: &str) -> CompiledRule {
compile(&format!(
r#"
id = "rename"
language = "typescript"
safety = "{safety}"
fix = "bar()"
[rule]
pattern = "foo()"
"#
))
}
#[test]
fn default_safety_refuses_auto_apply() {
let rule = compile(
r#"
id = "rename"
language = "typescript"
fix = "bar()"
[rule]
pattern = "foo()"
"#,
);
assert_eq!(rule.safety(), Safety::ScopeLocal);
assert_eq!(rule.applicability(), Applicability::Suggestion);
let preview = rule.apply("foo();").unwrap();
assert!(preview.rewritten.contains("bar()"));
assert!(matches!(
rule.auto_apply("foo();"),
Err(RulesError::NotAutoApplicable { .. })
));
}
#[test]
fn machine_applicable_tiers_auto_apply() {
for safety in ["format-only", "behavior-preserving"] {
let rule = codemod(safety);
assert_eq!(rule.applicability(), Applicability::MachineApplicable);
let applied = rule.auto_apply("foo();").expect("auto-applies");
assert!(applied.rewritten.contains("bar()"));
assert_eq!(applied.safety, rule.safety());
}
}
#[test]
fn risky_tiers_are_suggestions() {
for safety in [
"scope-local",
"surface-changing",
"capability-changing",
"needs-human",
] {
let rule = codemod(safety);
assert_eq!(rule.applicability(), Applicability::Suggestion);
assert!(rule.auto_apply("foo();").is_err());
}
}
#[test]
fn idempotent_fix_passes_the_check() {
let rule = codemod("format-only");
let result = rule.apply("foo();\n").unwrap();
assert!(result.idempotent);
assert!(rule.apply_checked("foo();\n").is_ok());
}
#[test]
fn non_idempotent_fix_is_rejected() {
let rule = compile(
r#"
id = "double-call"
language = "typescript"
safety = "behavior-preserving"
fix = "foo()()"
[rule]
pattern = "foo()"
"#,
);
let result = rule.apply("foo();\n").unwrap();
assert!(!result.idempotent, "fix should be flagged non-idempotent");
assert!(matches!(
rule.apply_checked("foo();\n"),
Err(RulesError::NotIdempotent { .. })
));
}
#[test]
fn diagnostics_carry_message_severity_and_applicability() {
let rule = compile(
r#"
id = "no-foo"
language = "typescript"
severity = "error"
message = "Do not call foo()"
safety = "behavior-preserving"
fix = "bar()"
[rule]
pattern = "foo()"
"#,
);
let diags = rule.diagnostics("foo();\nfoo();\n").unwrap();
assert_eq!(diags.len(), 2);
assert_eq!(diags[0].message, "Do not call foo()");
assert_eq!(diags[0].rule_id, "no-foo");
assert_eq!(diags[0].applicability, Applicability::MachineApplicable);
assert_eq!(diags[0].fix.as_deref(), Some("bar()"));
}