claude_native/rules/project_specific/
mobile.rs1use crate::detection::{MobileFramework, PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_mobile(pt: &ProjectType) -> bool {
6 matches!(pt.primary, PrimaryType::Mobile(_))
7}
8
9pub struct PlatformBuildDirsIgnored;
12
13impl Rule for PlatformBuildDirsIgnored {
14 fn id(&self) -> &str { "MOB1" }
15 fn name(&self) -> &str { "Platform build dirs are ignored" }
16 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
17 fn severity(&self) -> Severity { Severity::Critical }
18
19 fn applies_to(&self, pt: &ProjectType) -> bool { is_mobile(pt) }
20
21 fn check(&self, ctx: &ProjectContext) -> RuleResult {
22 if ctx.claudeignore_content.is_none() {
23 return self.fail(
24 "No .claudeignore — mobile platform build dirs (50K+ files) are visible to Claude",
25 Suggestion {
26 priority: SuggestionPriority::QuickWin,
27 title: "Create .claudeignore for mobile project".into(),
28 description: "Mobile projects generate massive build artifacts. Add: build/, .dart_tool/, ios/Pods/, android/.gradle/, android/build/, .expo/, DerivedData/, node_modules/".into(),
29 effort: Effort::Minutes,
30 },
31 );
32 }
33
34 let pt = ctx.project_type.as_ref().unwrap();
35 let required: Vec<&str> = match &pt.primary {
36 PrimaryType::Mobile(MobileFramework::Flutter) =>
37 vec!["build", ".dart_tool", "Pods", ".gradle"],
38 PrimaryType::Mobile(MobileFramework::ReactNative) =>
39 vec!["node_modules", "Pods", ".gradle", ".expo"],
40 PrimaryType::Mobile(MobileFramework::IosNative) =>
41 vec!["Pods", "DerivedData", ".build"],
42 PrimaryType::Mobile(MobileFramework::AndroidNative) =>
43 vec![".gradle", "build", "intermediates"],
44 _ => vec![],
45 };
46
47 let missing: Vec<&&str> = required.iter()
48 .filter(|p| !ctx.claudeignore_contains(p))
49 .collect();
50
51 if missing.is_empty() {
52 self.pass()
53 } else {
54 self.fail(
55 &format!("Platform dirs not in .claudeignore: {}", missing.iter().map(|m| m.to_string()).collect::<Vec<_>>().join(", ")),
56 Suggestion {
57 priority: SuggestionPriority::QuickWin,
58 title: "Add platform dirs to .claudeignore".into(),
59 description: format!("Add these to .claudeignore: {}", missing.iter().map(|m| format!("{}/", m)).collect::<Vec<_>>().join(", ")),
60 effort: Effort::Minutes,
61 },
62 )
63 }
64 }
65}
66
67pub struct GeneratedCodeIgnored;
70
71impl Rule for GeneratedCodeIgnored {
72 fn id(&self) -> &str { "MOB2" }
73 fn name(&self) -> &str { "Generated code is in .claudeignore" }
74 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
75 fn severity(&self) -> Severity { Severity::High }
76
77 fn applies_to(&self, pt: &ProjectType) -> bool { is_mobile(pt) }
78
79 fn check(&self, ctx: &ProjectContext) -> RuleResult {
80 let pt = ctx.project_type.as_ref().unwrap();
81 let patterns: Vec<&str> = match &pt.primary {
82 PrimaryType::Mobile(MobileFramework::Flutter) =>
83 vec!["*.g.dart", "*.freezed.dart", "*.pb.dart"],
84 PrimaryType::Mobile(MobileFramework::ReactNative) =>
85 vec!["*.pb.js", ".bundle"],
86 PrimaryType::Mobile(MobileFramework::IosNative) =>
87 vec!["*.generated.swift", "*.pb.swift"],
88 PrimaryType::Mobile(MobileFramework::AndroidNative) =>
89 vec!["BuildConfig", "R.java", "R.kt", "databinding"],
90 _ => vec![],
91 };
92
93 if patterns.is_empty() {
94 return self.pass();
95 }
96
97 let gen_count = ctx.all_files.iter().filter(|f| f.is_generated).count();
98 if gen_count == 0 {
99 return self.pass();
100 }
101
102 let has_gen_patterns = patterns.iter().any(|p| ctx.claudeignore_contains(p));
103 if has_gen_patterns {
104 self.pass()
105 } else {
106 self.warn(
107 &format!("{gen_count} generated files found — consider adding codegen patterns to .claudeignore"),
108 Suggestion {
109 priority: SuggestionPriority::QuickWin,
110 title: "Ignore generated mobile code".into(),
111 description: format!("Add these patterns to .claudeignore: {}", patterns.join(", ")),
112 effort: Effort::Minutes,
113 },
114 )
115 }
116 }
117}
118
119pub struct PlatformCommands;
122
123impl Rule for PlatformCommands {
124 fn id(&self) -> &str { "MOB3" }
125 fn name(&self) -> &str { "Platform-specific commands documented" }
126 fn dimension(&self) -> Dimension { Dimension::Foundation }
127 fn severity(&self) -> Severity { Severity::High }
128
129 fn applies_to(&self, pt: &ProjectType) -> bool { is_mobile(pt) }
130
131 fn check(&self, ctx: &ProjectContext) -> RuleResult {
132 let content = match &ctx.claude_md_content {
133 Some(c) => c.to_lowercase(),
134 None => return self.skip(),
135 };
136
137 let pt = ctx.project_type.as_ref().unwrap();
138 let expected_cmds: Vec<&str> = match &pt.primary {
139 PrimaryType::Mobile(MobileFramework::Flutter) =>
140 vec!["flutter analyze", "flutter test"],
141 PrimaryType::Mobile(MobileFramework::ReactNative) =>
142 vec!["npm test", "npx expo"],
143 PrimaryType::Mobile(MobileFramework::IosNative) =>
144 vec!["swift build", "swift test", "xcodebuild"],
145 PrimaryType::Mobile(MobileFramework::AndroidNative) =>
146 vec!["gradlew", "gradle"],
147 _ => vec![],
148 };
149
150 let found = expected_cmds.iter().any(|cmd| content.contains(cmd));
151 if found {
152 self.pass()
153 } else {
154 self.fail(
155 "CLAUDE.md lacks platform-specific build/test commands",
156 Suggestion {
157 priority: SuggestionPriority::QuickWin,
158 title: "Add platform-specific commands".into(),
159 description: format!("Add to CLAUDE.md: {}", expected_cmds.join(", ")),
160 effort: Effort::Minutes,
161 },
162 )
163 }
164 }
165}
166
167pub struct BinaryAssetsExcluded;
170
171impl Rule for BinaryAssetsExcluded {
172 fn id(&self) -> &str { "MOB4" }
173 fn name(&self) -> &str { "Binary assets excluded from search" }
174 fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
175 fn severity(&self) -> Severity { Severity::Medium }
176
177 fn applies_to(&self, pt: &ProjectType) -> bool { is_mobile(pt) }
178
179 fn check(&self, ctx: &ProjectContext) -> RuleResult {
180 let asset_patterns = ["*.png", "*.jpg", "*.svg", "*.ttf", "*.mp3", "*.wav"];
181 let has_assets_ignored = ctx.claudeignore_content.as_ref().map(|c| {
182 asset_patterns.iter().any(|p| c.contains(p))
183 }).unwrap_or(false);
184
185 let has_assets_dir = ctx.root.join("assets").is_dir()
186 || ctx.root.join("Assets").is_dir()
187 || ctx.root.join("res").is_dir();
188
189 if !has_assets_dir {
190 return self.pass();
191 }
192
193 if has_assets_ignored {
194 self.pass()
195 } else {
196 self.warn(
197 "Binary assets (images, fonts, audio) may clutter Claude's searches",
198 Suggestion {
199 priority: SuggestionPriority::NiceToHave,
200 title: "Exclude binary assets from .claudeignore".into(),
201 description: "Add to .claudeignore: *.png, *.jpg, *.svg, *.ttf, *.otf, *.mp3, *.wav. Binary files can't be read but slow down Glob/Grep searches.".into(),
202 effort: Effort::Minutes,
203 },
204 )
205 }
206 }
207}
208
209pub struct LightweightVerification;
212
213impl Rule for LightweightVerification {
214 fn id(&self) -> &str { "MOB5" }
215 fn name(&self) -> &str { "Fast verification prioritized over full builds" }
216 fn dimension(&self) -> Dimension { Dimension::Foundation }
217 fn severity(&self) -> Severity { Severity::Medium }
218
219 fn applies_to(&self, pt: &ProjectType) -> bool { is_mobile(pt) }
220
221 fn check(&self, ctx: &ProjectContext) -> RuleResult {
222 let content = match &ctx.claude_md_content {
223 Some(c) => c.to_lowercase(),
224 None => return self.skip(),
225 };
226
227 let has_fast_cmd = content.contains("analyze")
228 || content.contains("lint")
229 || content.contains("type-check")
230 || content.contains("typecheck")
231 || content.contains("--no-emit");
232
233 if has_fast_cmd {
234 self.pass()
235 } else {
236 self.warn(
237 "CLAUDE.md doesn't mention fast verification (analyze/lint/type-check)",
238 Suggestion {
239 priority: SuggestionPriority::NiceToHave,
240 title: "Prioritize fast checks in CLAUDE.md".into(),
241 description: "Add fast verification commands before full builds: `flutter analyze` (seconds) before `flutter build` (minutes). Claude should use the fast path for quick feedback.".into(),
242 effort: Effort::Minutes,
243 },
244 )
245 }
246 }
247}