Skip to main content

claude_native/rules/
quality_extra.rs

1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4// ── Rule 5.3: Descriptive test names ────────────────────────────────
5
6pub 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
67// ── Rule 5.4: Consistent patterns ──────────────────────────────────
68
69pub 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
100// ── Rule 5.5: Comments explain why ──────────────────────────────────
101
102pub 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
156// ── Rule 5.6: No dead code ─────────────────────────────────────────
157
158pub 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
189// ── Rule 5.7: Dependencies documented ───────────────────────────────
190
191pub 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
215// ── Rule 5.8: CI/CD exists ─────────────────────────────────────────
216
217pub 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}