1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12pub enum RmaError {
13 #[error("IO error: {0}")]
14 Io(#[from] std::io::Error),
15
16 #[error("Parse error in {file}: {message}")]
17 Parse { file: PathBuf, message: String },
18
19 #[error("Analysis error: {0}")]
20 Analysis(String),
21
22 #[error("Index error: {0}")]
23 Index(String),
24
25 #[error("Unsupported language: {0}")]
26 UnsupportedLanguage(String),
27
28 #[error("Configuration error: {0}")]
29 Config(String),
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum Language {
36 Rust,
37 JavaScript,
38 TypeScript,
39 Python,
40 Go,
41 Java,
42 Unknown,
43}
44
45impl Language {
46 pub fn from_extension(ext: &str) -> Self {
48 match ext.to_lowercase().as_str() {
49 "rs" => Language::Rust,
50 "js" | "mjs" | "cjs" => Language::JavaScript,
51 "ts" | "tsx" => Language::TypeScript,
52 "py" | "pyi" => Language::Python,
53 "go" => Language::Go,
54 "java" => Language::Java,
55 _ => Language::Unknown,
56 }
57 }
58
59 pub fn extensions(&self) -> &'static [&'static str] {
61 match self {
62 Language::Rust => &["rs"],
63 Language::JavaScript => &["js", "mjs", "cjs"],
64 Language::TypeScript => &["ts", "tsx"],
65 Language::Python => &["py", "pyi"],
66 Language::Go => &["go"],
67 Language::Java => &["java"],
68 Language::Unknown => &[],
69 }
70 }
71}
72
73impl std::fmt::Display for Language {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Language::Rust => write!(f, "rust"),
77 Language::JavaScript => write!(f, "javascript"),
78 Language::TypeScript => write!(f, "typescript"),
79 Language::Python => write!(f, "python"),
80 Language::Go => write!(f, "go"),
81 Language::Java => write!(f, "java"),
82 Language::Unknown => write!(f, "unknown"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum Severity {
91 Info,
92 Warning,
93 Error,
94 Critical,
95}
96
97impl std::fmt::Display for Severity {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Severity::Info => write!(f, "info"),
101 Severity::Warning => write!(f, "warning"),
102 Severity::Error => write!(f, "error"),
103 Severity::Critical => write!(f, "critical"),
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
110pub struct SourceLocation {
111 pub file: PathBuf,
112 pub start_line: usize,
113 pub start_column: usize,
114 pub end_line: usize,
115 pub end_column: usize,
116}
117
118impl SourceLocation {
119 pub fn new(
120 file: PathBuf,
121 start_line: usize,
122 start_column: usize,
123 end_line: usize,
124 end_column: usize,
125 ) -> Self {
126 Self {
127 file,
128 start_line,
129 start_column,
130 end_line,
131 end_column,
132 }
133 }
134}
135
136impl std::fmt::Display for SourceLocation {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 write!(
139 f,
140 "{}:{}:{}-{}:{}",
141 self.file.display(),
142 self.start_line,
143 self.start_column,
144 self.end_line,
145 self.end_column
146 )
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct Finding {
153 pub id: String,
154 pub rule_id: String,
155 pub message: String,
156 pub severity: Severity,
157 pub location: SourceLocation,
158 pub language: Language,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub snippet: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub suggestion: Option<String>,
163}
164
165#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct CodeMetrics {
168 pub lines_of_code: usize,
169 pub lines_of_comments: usize,
170 pub blank_lines: usize,
171 pub cyclomatic_complexity: usize,
172 pub cognitive_complexity: usize,
173 pub function_count: usize,
174 pub class_count: usize,
175 pub import_count: usize,
176}
177
178#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180pub struct ScanSummary {
181 pub files_scanned: usize,
182 pub files_skipped: usize,
183 pub total_lines: usize,
184 pub findings_by_severity: std::collections::HashMap<String, usize>,
185 pub languages: std::collections::HashMap<String, usize>,
186 pub duration_ms: u64,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct RmaConfig {
192 #[serde(default)]
194 pub exclude_patterns: Vec<String>,
195
196 #[serde(default)]
198 pub languages: Vec<Language>,
199
200 #[serde(default = "default_min_severity")]
202 pub min_severity: Severity,
203
204 #[serde(default = "default_max_file_size")]
206 pub max_file_size: usize,
207
208 #[serde(default)]
210 pub parallelism: usize,
211
212 #[serde(default)]
214 pub incremental: bool,
215}
216
217fn default_min_severity() -> Severity {
218 Severity::Warning
219}
220
221fn default_max_file_size() -> usize {
222 10 * 1024 * 1024 }
224
225impl Default for RmaConfig {
226 fn default() -> Self {
227 Self {
228 exclude_patterns: vec![
229 "**/node_modules/**".into(),
230 "**/target/**".into(),
231 "**/vendor/**".into(),
232 "**/.git/**".into(),
233 "**/dist/**".into(),
234 "**/build/**".into(),
235 ],
236 languages: vec![],
237 min_severity: default_min_severity(),
238 max_file_size: default_max_file_size(),
239 parallelism: 0,
240 incremental: false,
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_language_from_extension() {
251 assert_eq!(Language::from_extension("rs"), Language::Rust);
252 assert_eq!(Language::from_extension("js"), Language::JavaScript);
253 assert_eq!(Language::from_extension("py"), Language::Python);
254 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
255 }
256
257 #[test]
258 fn test_severity_ordering() {
259 assert!(Severity::Info < Severity::Warning);
260 assert!(Severity::Warning < Severity::Error);
261 assert!(Severity::Error < Severity::Critical);
262 }
263
264 #[test]
265 fn test_source_location_display() {
266 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
267 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
268 }
269}