Skip to main content

claude_native/rules/project_specific/
serverless.rs

1use crate::detection::{PrimaryType, ProjectType};
2use crate::rules::*;
3use crate::scan::ProjectContext;
4
5fn is_serverless(pt: &ProjectType) -> bool {
6    matches!(pt.primary, PrimaryType::Serverless(_))
7}
8
9// ── Rule SLS1: Deployment artifacts ignored ─────────────────────────
10
11pub struct DeployArtifactsIgnored;
12
13impl Rule for DeployArtifactsIgnored {
14    fn id(&self) -> &str { "SLS1" }
15    fn name(&self) -> &str { "Deployment artifacts are ignored" }
16    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
17    fn severity(&self) -> Severity { Severity::High }
18
19    fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
20
21    fn check(&self, ctx: &ProjectContext) -> RuleResult {
22        let artifacts = [".aws-sam", ".serverless", ".vercel", "cdk.out"];
23        let present: Vec<&&str> = artifacts.iter().filter(|a| ctx.root.join(a).is_dir()).collect();
24
25        if present.is_empty() {
26            return self.pass();
27        }
28
29        let all_ignored = present.iter().all(|a| ctx.claudeignore_contains(a));
30        if all_ignored {
31            self.pass()
32        } else {
33            self.fail(
34                &format!("Deployment artifacts not in .claudeignore: {}", present.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(", ")),
35                Suggestion {
36                    priority: SuggestionPriority::QuickWin,
37                    title: "Ignore deployment artifacts".into(),
38                    description: format!("Add to .claudeignore: {}. sam build generates 10,000+ line CloudFormation templates.", present.iter().map(|a| format!("{a}/")).collect::<Vec<_>>().join(", ")),
39                    effort: Effort::Minutes,
40                },
41            )
42        }
43    }
44}
45
46// ── Rule SLS2: Functions have focused scope ─────────────────────────
47
48pub struct FunctionsFocused;
49
50impl Rule for FunctionsFocused {
51    fn id(&self) -> &str { "SLS2" }
52    fn name(&self) -> &str { "Function handlers are <100 lines" }
53    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
54    fn severity(&self) -> Severity { Severity::Medium }
55
56    fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
57
58    fn check(&self, ctx: &ProjectContext) -> RuleResult {
59        let handler_patterns = ["handler", "index", "lambda", "function"];
60        let large_handlers: Vec<_> = ctx.all_files.iter()
61            .filter(|f| {
62                let name = f.path.file_stem().and_then(|n| n.to_str()).unwrap_or("").to_lowercase();
63                handler_patterns.iter().any(|p| name.contains(p)) && f.line_count > 100
64            })
65            .collect();
66
67        if large_handlers.is_empty() {
68            self.pass()
69        } else {
70            let examples: Vec<String> = large_handlers.iter().take(3)
71                .map(|f| format!("{} ({} lines)", f.relative_path.display(), f.line_count))
72                .collect();
73            self.warn(
74                &format!("Large handler files: {}", examples.join(", ")),
75                Suggestion {
76                    priority: SuggestionPriority::HighImpact,
77                    title: "Keep handlers under 100 lines".into(),
78                    description: "Serverless functions should be small by design. Extract business logic into separate modules and keep handlers thin.".into(),
79                    effort: Effort::Hour,
80                },
81            )
82        }
83    }
84}
85
86// ── Rule SLS3: Test events exist ────────────────────────────────────
87
88pub struct TestEventsExist;
89
90impl Rule for TestEventsExist {
91    fn id(&self) -> &str { "SLS3" }
92    fn name(&self) -> &str { "Test event files exist" }
93    fn dimension(&self) -> Dimension { Dimension::CodeQuality }
94    fn severity(&self) -> Severity { Severity::Medium }
95
96    fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
97
98    fn check(&self, ctx: &ProjectContext) -> RuleResult {
99        let has_events = ctx.root.join("events").is_dir()
100            || ctx.root.join("test-events").is_dir()
101            || ctx.all_files.iter().any(|f| {
102                f.relative_path.to_string_lossy().contains("event") && f.path.extension().map(|e| e == "json").unwrap_or(false)
103            });
104
105        if has_events {
106            self.pass()
107        } else {
108            self.warn(
109                "No test event files found (events/ or test-events/)",
110                Suggestion {
111                    priority: SuggestionPriority::NiceToHave,
112                    title: "Add test event files".into(),
113                    description: "Create events/ directory with sample event JSON files. Claude needs these to test functions locally with `sam local invoke`.".into(),
114                    effort: Effort::Minutes,
115                },
116            )
117        }
118    }
119}
120
121// ── Rule SLS4: Cloud credentials not in repo ────────────────────────
122
123pub struct NoCloudCreds;
124
125impl Rule for NoCloudCreds {
126    fn id(&self) -> &str { "SLS4" }
127    fn name(&self) -> &str { "Cloud credentials not in repo" }
128    fn dimension(&self) -> Dimension { Dimension::ContextEfficiency }
129    fn severity(&self) -> Severity { Severity::Critical }
130
131    fn applies_to(&self, pt: &ProjectType) -> bool { is_serverless(pt) }
132
133    fn check(&self, ctx: &ProjectContext) -> RuleResult {
134        let cred_dirs = [".aws", "credentials"];
135        let found: Vec<&&str> = cred_dirs.iter().filter(|d| ctx.root.join(d).exists()).collect();
136
137        if found.is_empty() {
138            self.pass()
139        } else {
140            self.fail(
141                &format!("Cloud credential files/directories found: {}", found.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(", ")),
142                Suggestion {
143                    priority: SuggestionPriority::QuickWin,
144                    title: "Remove cloud credentials from repo".into(),
145                    description: "Serverless projects have direct cloud access. Leaked credentials = full cloud compromise. Add to .gitignore immediately.".into(),
146                    effort: Effort::Minutes,
147                },
148            )
149        }
150    }
151}