Skip to main content

claude_native/rules/project_specific/
mobile.rs

1use 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
9// ── Rule MOB1: Platform build directories are ignored ───────────────
10
11pub 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
67// ── Rule MOB2: Generated code is ignored ────────────────────────────
68
69pub 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
119// ── Rule MOB3: Platform-specific build/test commands ────────────────
120
121pub 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
167// ── Rule MOB4: Binary assets excluded from search ───────────────────
168
169pub 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
209// ── Rule MOB5: Lightweight verification ─────────────────────────────
210
211pub 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}