claude_native/rules/
quality.rs1use crate::detection::Language;
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5pub struct TypeAnnotationsExist;
8
9impl Rule for TypeAnnotationsExist {
10 fn id(&self) -> &str { "5.1" }
11 fn name(&self) -> &str { "Type annotations exist" }
12 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
13 fn severity(&self) -> Severity { Severity::Medium }
14
15 fn check(&self, ctx: &ProjectContext) -> RuleResult {
16 let pt = match &ctx.project_type {
17 Some(pt) => pt,
18 None => return self.skip(),
19 };
20
21 let needs_types = pt.languages.iter().any(|l| matches!(l,
23 Language::JavaScript | Language::Python | Language::Ruby
24 ));
25
26 if !needs_types {
27 return self.pass(); }
29
30 let has_typescript = pt.languages.iter().any(|l| matches!(l, Language::TypeScript));
32 if has_typescript {
33 return self.pass();
34 }
35
36 if pt.languages.iter().any(|l| matches!(l, Language::Python)) {
38 let has_mypy = ctx.has_file("mypy.ini")
39 || ctx.has_file(".mypy.ini")
40 || ctx.has_file("setup.cfg")
41 || ctx.read_root_file("pyproject.toml")
42 .map(|c| c.contains("[tool.mypy]") || c.contains("mypy"))
43 .unwrap_or(false);
44 if has_mypy {
45 return self.pass();
46 }
47 }
48
49 self.fail(
50 "Dynamically-typed language detected without type annotations/checking",
51 Suggestion {
52 priority: SuggestionPriority::HighImpact,
53 title: "Add type annotations".into(),
54 description: "For JS: migrate to TypeScript or add JSDoc types. For Python: add type hints and mypy. Types give Claude contracts to work with — reducing hallucinated return values.".into(),
55 effort: Effort::HalfDay,
56 },
57 )
58 }
59}
60
61pub struct TestsExist;
64
65impl Rule for TestsExist {
66 fn id(&self) -> &str { "5.2" }
67 fn name(&self) -> &str { "Tests exist" }
68 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
69 fn severity(&self) -> Severity { Severity::High }
70
71 fn check(&self, ctx: &ProjectContext) -> RuleResult {
72 if !ctx.test_files.is_empty() {
73 let ratio = ctx.test_files.len() as f64 / ctx.source_file_count().max(1) as f64;
74 if ratio < 0.1 {
75 return self.warn(
76 &format!("Only {} test files for {} source files ({:.0}% ratio)", ctx.test_files.len(), ctx.source_file_count(), ratio * 100.0),
77 Suggestion {
78 priority: SuggestionPriority::HighImpact,
79 title: "Add more tests".into(),
80 description: "Test coverage is very low. Tests are Claude's primary way to verify changes. Aim for at least 1 test file per 3 source files.".into(),
81 effort: Effort::HalfDay,
82 },
83 );
84 }
85 self.pass()
86 } else {
87 self.fail(
88 "No test files found",
89 Suggestion {
90 priority: SuggestionPriority::HighImpact,
91 title: "Add tests".into(),
92 description: "Tests are Claude's primary verification mechanism. Without them, Claude can't validate its own changes. Start with tests for critical paths.".into(),
93 effort: Effort::HalfDay,
94 },
95 )
96 }
97 }
98}
99
100