claude_native/rules/
token_suggestions.rs1use crate::detection::{Language, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5pub struct LanguageAwareIndexFiles;
10
11impl Rule for LanguageAwareIndexFiles {
12 fn id(&self) -> &str { "7.6" }
13 fn name(&self) -> &str { "Modules have language-appropriate index files" }
14 fn dimension(&self) -> Dimension { Dimension::Navigation }
15 fn severity(&self) -> Severity { Severity::Low }
16
17 fn check(&self, ctx: &ProjectContext) -> RuleResult {
18 let pt = match &ctx.project_type {
19 Some(pt) => pt,
20 None => return self.skip(),
21 };
22
23 let expected = expected_index_files(&pt.languages);
24 if expected.is_empty() { return self.pass(); }
25
26 let source_dirs = significant_source_dirs(ctx);
27 if source_dirs.is_empty() { return self.pass(); }
28
29 let missing: Vec<String> = source_dirs.iter()
30 .filter(|d| !expected.iter().any(|idx| d.join(idx).exists()))
31 .filter_map(|d| d.strip_prefix(&ctx.root).ok())
32 .map(|p| p.to_string_lossy().to_string())
33 .collect();
34
35 if missing.is_empty() {
36 self.pass()
37 } else {
38 let idx_names = expected.join(" or ");
39 self.warn(
40 &format!("{} dirs lack {idx_names}", missing.len()),
41 Suggestion {
42 priority: SuggestionPriority::NiceToHave,
43 title: format!("Add {idx_names} to modules"),
44 description: format!(
45 "For your project ({:?}), add {idx_names} to each module directory. \
46 Claude reads the index file to understand module exports without scanning all files.\n\
47 Missing in: {}",
48 pt.languages.first().unwrap_or(&Language::Other("unknown".into())),
49 missing.iter().take(5).cloned().collect::<Vec<_>>().join(", ")
50 ),
51 effort: Effort::Hour,
52 },
53 )
54 }
55 }
56}
57
58fn expected_index_files(langs: &[Language]) -> Vec<&'static str> {
59 for lang in langs {
60 match lang {
61 Language::Rust => return vec!["mod.rs"],
62 Language::TypeScript => return vec!["index.ts", "index.tsx"],
63 Language::JavaScript => return vec!["index.js", "index.jsx"],
64 Language::Python => return vec!["__init__.py"],
65 Language::Dart => return vec!["index.dart"],
66 Language::Go => {}, Language::Ruby => return vec!["index.rb"],
68 Language::CSharp => {}, _ => {}
70 }
71 }
72 vec![]
73}
74
75fn significant_source_dirs(ctx: &ProjectContext) -> Vec<std::path::PathBuf> {
76 use std::collections::HashMap;
77 let skip = [".claude", ".github", "tests", "test", "docs", "examples", "target"];
78 let code_exts = ["rs", "ts", "tsx", "js", "py", "go", "dart", "kt", "java", "rb", "cs", "swift"];
79 let mut counts: HashMap<std::path::PathBuf, usize> = HashMap::new();
80
81 for f in ctx.source_files() {
82 let is_code = f.path.extension().and_then(|e| e.to_str())
83 .map(|e| code_exts.contains(&e)).unwrap_or(false);
84 if !is_code { continue; }
85 if let Some(parent) = f.path.parent() {
86 if parent == ctx.root { continue; }
87 if parent.file_name().map(|n| n == "src").unwrap_or(false)
89 && parent.parent().map(|p| p == ctx.root).unwrap_or(false) { continue; }
90 let name = parent.file_name().and_then(|n| n.to_str()).unwrap_or("");
91 if name.starts_with('.') || skip.contains(&name) { continue; }
92 *counts.entry(parent.to_path_buf()).or_insert(0) += 1;
93 }
94 }
95 counts.into_iter().filter(|(_, c)| *c >= 3).map(|(p, _)| p).collect()
96}
97
98pub struct PublicApiAtTop;
103
104impl Rule for PublicApiAtTop {
105 fn id(&self) -> &str { "7.7" }
106 fn name(&self) -> &str { "Public APIs/exports at top of files" }
107 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
108 fn severity(&self) -> Severity { Severity::Low }
109
110 fn check(&self, ctx: &ProjectContext) -> RuleResult {
111 let mut buried_count = 0;
112 let sample = ctx.source_files();
113 let sample: Vec<_> = sample.iter().take(20).collect();
114
115 for f in &sample {
116 if f.line_count < 50 { continue; } if let Ok(content) = std::fs::read_to_string(&f.path) {
118 if has_buried_public_api(&content) {
119 buried_count += 1;
120 }
121 }
122 }
123
124 if buried_count == 0 {
125 self.pass()
126 } else {
127 self.warn(
128 &format!("{buried_count} files have public APIs below private code"),
129 Suggestion {
130 priority: SuggestionPriority::NiceToHave,
131 title: "Move public APIs to top of files".into(),
132 description: "Public functions/exports should be above private helpers. \
133 Claude reads top-down — if exports are at line 200, it reads 200 lines to find the API. \
134 Put `pub fn`, `export`, or constants at the top. Saves ~100 tokens per file read.".into(),
135 effort: Effort::Hour,
136 },
137 )
138 }
139 }
140}
141
142fn has_buried_public_api(content: &str) -> bool {
143 if content.contains("impl Rule for") || content.contains("impl Handler for") {
146 return false;
147 }
148
149 let mut first_private_line: Option<usize> = None;
150 let mut last_public_line: Option<usize> = None;
151
152 for (i, line) in content.lines().enumerate() {
153 let t = line.trim();
154 if (t.starts_with("pub fn ") || t.starts_with("pub struct ")
156 || t.starts_with("pub enum ") || t.starts_with("pub type ")
157 || t.starts_with("export ") || t.starts_with("export default"))
158 && !t.contains("(&self") {
160 last_public_line = Some(i);
161 }
162 if (t.starts_with("fn ") && !t.starts_with("fn main") && !t.contains("(&self"))
164 || (t.starts_with("struct ") && !t.starts_with("pub"))
165 || (t.starts_with("def ") && !t.starts_with("def _"))
166 {
167 if first_private_line.is_none() {
168 first_private_line = Some(i);
169 }
170 }
171 }
172
173 match (first_private_line, last_public_line) {
174 (Some(priv_line), Some(pub_line)) => pub_line > priv_line + 30,
175 _ => false,
176 }
177}
178
179