1pub mod config;
7
8pub use config::{
9 AllowConfig, AllowType, Baseline, BaselineConfig, BaselineEntry, BaselineMode,
10 CURRENT_CONFIG_VERSION, ConfigLoadResult, ConfigSource, ConfigWarning, EffectiveConfig,
11 Fingerprint, InlineSuppression, OxlintProviderConfig, PmdProviderConfig, Profile,
12 ProfileThresholds, ProfilesConfig, ProviderType, ProvidersConfig, RmaTomlConfig, RulesConfig,
13 RulesetsConfig, ScanConfig, SuppressionType, ThresholdOverride, WarningLevel,
14 parse_inline_suppressions,
15};
16
17use serde::{Deserialize, Serialize};
18use std::path::PathBuf;
19use thiserror::Error;
20
21#[derive(Error, Debug)]
23pub enum RmaError {
24 #[error("IO error: {0}")]
25 Io(#[from] std::io::Error),
26
27 #[error("Parse error in {file}: {message}")]
28 Parse { file: PathBuf, message: String },
29
30 #[error("Analysis error: {0}")]
31 Analysis(String),
32
33 #[error("Index error: {0}")]
34 Index(String),
35
36 #[error("Unsupported language: {0}")]
37 UnsupportedLanguage(String),
38
39 #[error("Configuration error: {0}")]
40 Config(String),
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum Language {
47 Rust,
48 JavaScript,
49 TypeScript,
50 Python,
51 Go,
52 Java,
53 Unknown,
54}
55
56impl Language {
57 pub fn from_extension(ext: &str) -> Self {
59 match ext.to_lowercase().as_str() {
60 "rs" => Language::Rust,
61 "js" | "mjs" | "cjs" => Language::JavaScript,
62 "ts" | "tsx" => Language::TypeScript,
63 "py" | "pyi" => Language::Python,
64 "go" => Language::Go,
65 "java" => Language::Java,
66 _ => Language::Unknown,
67 }
68 }
69
70 pub fn extensions(&self) -> &'static [&'static str] {
72 match self {
73 Language::Rust => &["rs"],
74 Language::JavaScript => &["js", "mjs", "cjs"],
75 Language::TypeScript => &["ts", "tsx"],
76 Language::Python => &["py", "pyi"],
77 Language::Go => &["go"],
78 Language::Java => &["java"],
79 Language::Unknown => &[],
80 }
81 }
82}
83
84impl std::fmt::Display for Language {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 Language::Rust => write!(f, "rust"),
88 Language::JavaScript => write!(f, "javascript"),
89 Language::TypeScript => write!(f, "typescript"),
90 Language::Python => write!(f, "python"),
91 Language::Go => write!(f, "go"),
92 Language::Java => write!(f, "java"),
93 Language::Unknown => write!(f, "unknown"),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
100#[serde(rename_all = "lowercase")]
101pub enum Severity {
102 Info,
103 Warning,
104 Error,
105 Critical,
106}
107
108impl std::fmt::Display for Severity {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Severity::Info => write!(f, "info"),
112 Severity::Warning => write!(f, "warning"),
113 Severity::Error => write!(f, "error"),
114 Severity::Critical => write!(f, "critical"),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
121#[serde(rename_all = "lowercase")]
122pub enum Confidence {
123 Low,
125 #[default]
127 Medium,
128 High,
130}
131
132impl std::fmt::Display for Confidence {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 Confidence::Low => write!(f, "low"),
136 Confidence::Medium => write!(f, "medium"),
137 Confidence::High => write!(f, "high"),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
144#[serde(rename_all = "lowercase")]
145pub enum FindingCategory {
146 #[default]
148 Security,
149 Quality,
151 Performance,
153 Style,
155}
156
157impl std::fmt::Display for FindingCategory {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 FindingCategory::Security => write!(f, "security"),
161 FindingCategory::Quality => write!(f, "quality"),
162 FindingCategory::Performance => write!(f, "performance"),
163 FindingCategory::Style => write!(f, "style"),
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
170pub struct SourceLocation {
171 pub file: PathBuf,
172 pub start_line: usize,
173 pub start_column: usize,
174 pub end_line: usize,
175 pub end_column: usize,
176}
177
178impl SourceLocation {
179 pub fn new(
180 file: PathBuf,
181 start_line: usize,
182 start_column: usize,
183 end_line: usize,
184 end_column: usize,
185 ) -> Self {
186 Self {
187 file,
188 start_line,
189 start_column,
190 end_line,
191 end_column,
192 }
193 }
194}
195
196impl std::fmt::Display for SourceLocation {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 write!(
199 f,
200 "{}:{}:{}-{}:{}",
201 self.file.display(),
202 self.start_line,
203 self.start_column,
204 self.end_line,
205 self.end_column
206 )
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Finding {
213 pub id: String,
214 pub rule_id: String,
215 pub message: String,
216 pub severity: Severity,
217 pub location: SourceLocation,
218 pub language: Language,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub snippet: Option<String>,
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub suggestion: Option<String>,
223 #[serde(default)]
225 pub confidence: Confidence,
226 #[serde(default)]
228 pub category: FindingCategory,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub fingerprint: Option<String>,
232}
233
234impl Finding {
235 pub fn compute_fingerprint(&mut self) {
238 use sha2::{Digest, Sha256};
239
240 let mut hasher = Sha256::new();
241 hasher.update(self.rule_id.as_bytes());
242 hasher.update(self.location.file.to_string_lossy().as_bytes());
243
244 if let Some(snippet) = &self.snippet {
246 let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
247 hasher.update(normalized.as_bytes());
248 }
249
250 let hash = hasher.finalize();
251 self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
252 }
253}
254
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
257pub struct CodeMetrics {
258 pub lines_of_code: usize,
259 pub lines_of_comments: usize,
260 pub blank_lines: usize,
261 pub cyclomatic_complexity: usize,
262 pub cognitive_complexity: usize,
263 pub function_count: usize,
264 pub class_count: usize,
265 pub import_count: usize,
266}
267
268#[derive(Debug, Clone, Default, Serialize, Deserialize)]
270pub struct ScanSummary {
271 pub files_scanned: usize,
272 pub files_skipped: usize,
273 pub total_lines: usize,
274 pub findings_by_severity: std::collections::HashMap<String, usize>,
275 pub languages: std::collections::HashMap<String, usize>,
276 pub duration_ms: u64,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct RmaConfig {
282 #[serde(default)]
284 pub exclude_patterns: Vec<String>,
285
286 #[serde(default)]
288 pub languages: Vec<Language>,
289
290 #[serde(default = "default_min_severity")]
292 pub min_severity: Severity,
293
294 #[serde(default = "default_max_file_size")]
296 pub max_file_size: usize,
297
298 #[serde(default)]
300 pub parallelism: usize,
301
302 #[serde(default)]
304 pub incremental: bool,
305}
306
307fn default_min_severity() -> Severity {
308 Severity::Warning
309}
310
311fn default_max_file_size() -> usize {
312 10 * 1024 * 1024 }
314
315impl Default for RmaConfig {
316 fn default() -> Self {
317 Self {
318 exclude_patterns: vec![
319 "**/node_modules/**".into(),
320 "**/target/**".into(),
321 "**/vendor/**".into(),
322 "**/.git/**".into(),
323 "**/dist/**".into(),
324 "**/build/**".into(),
325 ],
326 languages: vec![],
327 min_severity: default_min_severity(),
328 max_file_size: default_max_file_size(),
329 parallelism: 0,
330 incremental: false,
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_language_from_extension() {
341 assert_eq!(Language::from_extension("rs"), Language::Rust);
342 assert_eq!(Language::from_extension("js"), Language::JavaScript);
343 assert_eq!(Language::from_extension("py"), Language::Python);
344 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
345 }
346
347 #[test]
348 fn test_severity_ordering() {
349 assert!(Severity::Info < Severity::Warning);
350 assert!(Severity::Warning < Severity::Error);
351 assert!(Severity::Error < Severity::Critical);
352 }
353
354 #[test]
355 fn test_source_location_display() {
356 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
357 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
358 }
359}