claude_native/rules/
quality_extra.rs1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4pub struct DescriptiveTestNames;
7
8impl Rule for DescriptiveTestNames {
9 fn id(&self) -> &str { "5.3" }
10 fn name(&self) -> &str { "Tests have descriptive names" }
11 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
12 fn severity(&self) -> Severity { Severity::Low }
13
14 fn check(&self, ctx: &ProjectContext) -> RuleResult {
15 if ctx.test_files.is_empty() {
16 return self.skip();
17 }
18 let (bad_names, total_tests) = scan_test_names(ctx);
19 if total_tests == 0 || bad_names == 0 {
20 self.pass()
21 } else {
22 self.warn(
23 &format!("{bad_names}/{total_tests} test names are non-descriptive"),
24 Suggestion {
25 priority: SuggestionPriority::NiceToHave,
26 title: "Use descriptive test names".into(),
27 description: "Replace 'test1' with behavior: 'test_login_rejects_invalid_email'.".into(),
28 effort: Effort::Hour,
29 },
30 )
31 }
32 }
33}
34
35fn scan_test_names(ctx: &ProjectContext) -> (usize, usize) {
36 let bad_patterns = ["test1", "test2", "test3", "test_1", "test_2"];
37 let mut bad_names = 0;
38 let mut total_tests = 0;
39
40 for tf in &ctx.test_files {
41 let content = match std::fs::read_to_string(tf) {
42 Ok(c) => c,
43 Err(_) => continue,
44 };
45 for line in content.lines() {
46 if !is_test_declaration(line.trim()) { continue; }
47 total_tests += 1;
48 let lower = line.trim().to_lowercase();
49 if bad_patterns.iter().any(|p| lower.contains(p)) {
50 bad_names += 1;
51 }
52 }
53 }
54 (bad_names, total_tests)
55}
56
57fn is_test_declaration(trimmed: &str) -> bool {
58 trimmed.starts_with("fn test")
59 || trimmed.starts_with("#[test]")
60 || trimmed.starts_with("test(")
61 || trimmed.starts_with("test '")
62 || trimmed.starts_with("test \"")
63 || trimmed.starts_with("it(")
64 || trimmed.starts_with("def test_")
65}
66
67pub struct ConsistentPatterns;
70
71impl Rule for ConsistentPatterns {
72 fn id(&self) -> &str { "5.4" }
73 fn name(&self) -> &str { "Consistent patterns across codebase" }
74 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
75 fn severity(&self) -> Severity { Severity::Medium }
76
77 fn check(&self, ctx: &ProjectContext) -> RuleResult {
78 if let Some(content) = &ctx.claude_md_content {
79 let l = content.to_lowercase();
80 if l.contains("pattern") || l.contains("convention") || l.contains("style") {
81 return self.pass();
82 }
83 }
84 if ctx.source_file_count() > 10 {
85 self.warn(
86 "No documented code patterns found in CLAUDE.md",
87 Suggestion {
88 priority: SuggestionPriority::NiceToHave,
89 title: "Document code patterns in CLAUDE.md".into(),
90 description: "Describe error handling, data access, and API patterns.".into(),
91 effort: Effort::Minutes,
92 },
93 )
94 } else {
95 self.pass()
96 }
97 }
98}
99
100pub struct CommentsExplainWhy;
103
104const WHAT_PATTERNS: &[&str] = &[
105 "// set ", "// get ", "// return ", "// loop ", "// iterate ",
106 "// initialize ", "// create ", "// assign ", "// increment ",
107 "# set ", "# get ", "# return ", "# loop ", "# iterate ",
108 "# initialize ", "# create ", "# assign ",
109];
110
111impl Rule for CommentsExplainWhy {
112 fn id(&self) -> &str { "5.5" }
113 fn name(&self) -> &str { "Comments explain 'why', not 'what'" }
114 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
115 fn severity(&self) -> Severity { Severity::Low }
116
117 fn check(&self, ctx: &ProjectContext) -> RuleResult {
118 let (what_count, total) = count_what_comments(ctx);
119 if total < 5 { return self.pass(); }
120 let ratio = what_count as f64 / total as f64;
121 if ratio > 0.3 {
122 self.warn(
123 &format!("{what_count}/{total} comments restate the code"),
124 Suggestion {
125 priority: SuggestionPriority::NiceToHave,
126 title: "Improve comment quality".into(),
127 description: "Replace 'what' comments with 'why' comments.".into(),
128 effort: Effort::Hour,
129 },
130 )
131 } else {
132 self.pass()
133 }
134 }
135}
136
137fn count_what_comments(ctx: &ProjectContext) -> (usize, usize) {
138 let mut what_count = 0;
139 let mut total = 0;
140 for f in ctx.all_files.iter().filter(|f| !f.is_test && !f.is_generated).take(20) {
141 let content = match std::fs::read_to_string(&f.path) {
142 Ok(c) => c,
143 Err(_) => continue,
144 };
145 for line in content.lines() {
146 let t = line.trim().to_lowercase();
147 if t.starts_with("//") || (t.starts_with('#') && !t.starts_with("#[")) {
148 total += 1;
149 if WHAT_PATTERNS.iter().any(|p| t.starts_with(p)) { what_count += 1; }
150 }
151 }
152 }
153 (what_count, total)
154}
155
156pub struct NoDeadCode;
159
160impl Rule for NoDeadCode {
161 fn id(&self) -> &str { "5.6" }
162 fn name(&self) -> &str { "No dead code" }
163 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
164 fn severity(&self) -> Severity { Severity::Low }
165
166 fn check(&self, ctx: &ProjectContext) -> RuleResult {
167 let dead = ["deprecated", "old", "backup", "archive", "legacy"];
168 let found: Vec<String> = ctx.directories.iter()
169 .filter_map(|d| {
170 let name = d.file_name()?.to_str()?.to_lowercase();
171 dead.contains(&name.as_str()).then_some(name)
172 })
173 .collect();
174 if found.is_empty() { self.pass() }
175 else {
176 self.warn(
177 &format!("Dead code directories: {}", found.join(", ")),
178 Suggestion {
179 priority: SuggestionPriority::NiceToHave,
180 title: "Remove or ignore dead code".into(),
181 description: "Add to .claudeignore so Claude skips deprecated code.".into(),
182 effort: Effort::Hour,
183 },
184 )
185 }
186 }
187}
188
189pub struct DependenciesDocumented;
192
193impl Rule for DependenciesDocumented {
194 fn id(&self) -> &str { "5.7" }
195 fn name(&self) -> &str { "Dependencies are documented" }
196 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
197 fn severity(&self) -> Severity { Severity::Medium }
198
199 fn check(&self, ctx: &ProjectContext) -> RuleResult {
200 if !ctx.package_manifests.is_empty() { self.pass() }
201 else {
202 self.fail(
203 "No package manifest found",
204 Suggestion {
205 priority: SuggestionPriority::HighImpact,
206 title: "Add a package manifest".into(),
207 description: "Create package.json/Cargo.toml/requirements.txt.".into(),
208 effort: Effort::Minutes,
209 },
210 )
211 }
212 }
213}
214
215pub struct CiCdExists;
218
219impl Rule for CiCdExists {
220 fn id(&self) -> &str { "5.8" }
221 fn name(&self) -> &str { "CI/CD pipeline exists" }
222 fn dimension(&self) -> Dimension { Dimension::CodeQuality }
223 fn severity(&self) -> Severity { Severity::Low }
224
225 fn check(&self, ctx: &ProjectContext) -> RuleResult {
226 let has_ci = !ctx.ci_configs.is_empty()
227 || ctx.root.join(".github").join("workflows").is_dir()
228 || ctx.has_file(".gitlab-ci.yml")
229 || ctx.has_file("Jenkinsfile");
230 if has_ci { self.pass() }
231 else {
232 self.warn(
233 "No CI/CD configuration found",
234 Suggestion {
235 priority: SuggestionPriority::NiceToHave,
236 title: "Add CI/CD pipeline".into(),
237 description: "Add .github/workflows/ or equivalent.".into(),
238 effort: Effort::Hour,
239 },
240 )
241 }
242 }
243}