Skip to main content

rma_common/
lib.rs

1//! Common types and utilities for Rust Monorepo Analyzer (RMA)
2//!
3//! This crate provides shared data structures, error types, and utilities
4//! used across all RMA components.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use thiserror::Error;
9
10/// Core error types for RMA operations
11#[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/// Supported programming languages
33#[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    /// Detect language from file extension
47    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    /// Get file extensions for this language
60    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/// Severity levels for findings
88#[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/// A source code location
109#[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/// A security or code quality finding
151#[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/// Code metrics for a file or function
166#[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/// Summary of a scan operation
179#[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/// Configuration for RMA operations
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct RmaConfig {
192    /// Paths to exclude from scanning
193    #[serde(default)]
194    pub exclude_patterns: Vec<String>,
195
196    /// Languages to scan (empty = all supported)
197    #[serde(default)]
198    pub languages: Vec<Language>,
199
200    /// Minimum severity to report
201    #[serde(default = "default_min_severity")]
202    pub min_severity: Severity,
203
204    /// Maximum file size in bytes
205    #[serde(default = "default_max_file_size")]
206    pub max_file_size: usize,
207
208    /// Number of parallel workers (0 = auto)
209    #[serde(default)]
210    pub parallelism: usize,
211
212    /// Enable incremental mode
213    #[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 // 10MB
223}
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}