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
6pub 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/// Core error types for RMA operations
22#[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/// Supported programming languages
44#[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    /// Detect language from file extension
58    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    /// Get file extensions for this language
71    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/// Severity levels for findings
99#[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/// Confidence level for findings (how certain we are this is a real issue)
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
121#[serde(rename_all = "lowercase")]
122pub enum Confidence {
123    /// Low confidence - may be a false positive, requires manual review
124    Low,
125    /// Medium confidence - likely an issue but context-dependent
126    #[default]
127    Medium,
128    /// High confidence - almost certainly a real issue
129    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/// Category of finding
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
144#[serde(rename_all = "lowercase")]
145pub enum FindingCategory {
146    /// Security vulnerabilities
147    #[default]
148    Security,
149    /// Code quality and maintainability
150    Quality,
151    /// Performance issues
152    Performance,
153    /// Style and formatting
154    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/// A source code location
169#[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/// A security or code quality finding
211#[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    /// Confidence level (how certain we are this is a real issue)
224    #[serde(default)]
225    pub confidence: Confidence,
226    /// Category of finding (security, quality, performance, style)
227    #[serde(default)]
228    pub category: FindingCategory,
229    /// Stable fingerprint for baseline comparison (sha256 hash)
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub fingerprint: Option<String>,
232}
233
234impl Finding {
235    /// Compute a stable fingerprint for this finding
236    /// Based on: rule_id + relative path + normalized snippet
237    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        // Normalize snippet by removing whitespace
245        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/// Code metrics for a file or function
256#[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/// Summary of a scan operation
269#[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/// Configuration for RMA operations
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct RmaConfig {
282    /// Paths to exclude from scanning
283    #[serde(default)]
284    pub exclude_patterns: Vec<String>,
285
286    /// Languages to scan (empty = all supported)
287    #[serde(default)]
288    pub languages: Vec<Language>,
289
290    /// Minimum severity to report
291    #[serde(default = "default_min_severity")]
292    pub min_severity: Severity,
293
294    /// Maximum file size in bytes
295    #[serde(default = "default_max_file_size")]
296    pub max_file_size: usize,
297
298    /// Number of parallel workers (0 = auto)
299    #[serde(default)]
300    pub parallelism: usize,
301
302    /// Enable incremental mode
303    #[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 // 10MB
313}
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}