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, Profile, ProfileThresholds, ProfilesConfig, RmaTomlConfig,
12    RulesConfig, RulesetsConfig, ScanConfig, SuppressionType, ThresholdOverride, WarningLevel,
13    parse_inline_suppressions,
14};
15
16use serde::{Deserialize, Serialize};
17use std::path::PathBuf;
18use thiserror::Error;
19
20/// Core error types for RMA operations
21#[derive(Error, Debug)]
22pub enum RmaError {
23    #[error("IO error: {0}")]
24    Io(#[from] std::io::Error),
25
26    #[error("Parse error in {file}: {message}")]
27    Parse { file: PathBuf, message: String },
28
29    #[error("Analysis error: {0}")]
30    Analysis(String),
31
32    #[error("Index error: {0}")]
33    Index(String),
34
35    #[error("Unsupported language: {0}")]
36    UnsupportedLanguage(String),
37
38    #[error("Configuration error: {0}")]
39    Config(String),
40}
41
42/// Supported programming languages
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum Language {
46    Rust,
47    JavaScript,
48    TypeScript,
49    Python,
50    Go,
51    Java,
52    Unknown,
53}
54
55impl Language {
56    /// Detect language from file extension
57    pub fn from_extension(ext: &str) -> Self {
58        match ext.to_lowercase().as_str() {
59            "rs" => Language::Rust,
60            "js" | "mjs" | "cjs" => Language::JavaScript,
61            "ts" | "tsx" => Language::TypeScript,
62            "py" | "pyi" => Language::Python,
63            "go" => Language::Go,
64            "java" => Language::Java,
65            _ => Language::Unknown,
66        }
67    }
68
69    /// Get file extensions for this language
70    pub fn extensions(&self) -> &'static [&'static str] {
71        match self {
72            Language::Rust => &["rs"],
73            Language::JavaScript => &["js", "mjs", "cjs"],
74            Language::TypeScript => &["ts", "tsx"],
75            Language::Python => &["py", "pyi"],
76            Language::Go => &["go"],
77            Language::Java => &["java"],
78            Language::Unknown => &[],
79        }
80    }
81}
82
83impl std::fmt::Display for Language {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            Language::Rust => write!(f, "rust"),
87            Language::JavaScript => write!(f, "javascript"),
88            Language::TypeScript => write!(f, "typescript"),
89            Language::Python => write!(f, "python"),
90            Language::Go => write!(f, "go"),
91            Language::Java => write!(f, "java"),
92            Language::Unknown => write!(f, "unknown"),
93        }
94    }
95}
96
97/// Severity levels for findings
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum Severity {
101    Info,
102    Warning,
103    Error,
104    Critical,
105}
106
107impl std::fmt::Display for Severity {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Severity::Info => write!(f, "info"),
111            Severity::Warning => write!(f, "warning"),
112            Severity::Error => write!(f, "error"),
113            Severity::Critical => write!(f, "critical"),
114        }
115    }
116}
117
118/// Confidence level for findings (how certain we are this is a real issue)
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
120#[serde(rename_all = "lowercase")]
121pub enum Confidence {
122    /// Low confidence - may be a false positive, requires manual review
123    Low,
124    /// Medium confidence - likely an issue but context-dependent
125    #[default]
126    Medium,
127    /// High confidence - almost certainly a real issue
128    High,
129}
130
131impl std::fmt::Display for Confidence {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        match self {
134            Confidence::Low => write!(f, "low"),
135            Confidence::Medium => write!(f, "medium"),
136            Confidence::High => write!(f, "high"),
137        }
138    }
139}
140
141/// Category of finding
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
143#[serde(rename_all = "lowercase")]
144pub enum FindingCategory {
145    /// Security vulnerabilities
146    #[default]
147    Security,
148    /// Code quality and maintainability
149    Quality,
150    /// Performance issues
151    Performance,
152    /// Style and formatting
153    Style,
154}
155
156impl std::fmt::Display for FindingCategory {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            FindingCategory::Security => write!(f, "security"),
160            FindingCategory::Quality => write!(f, "quality"),
161            FindingCategory::Performance => write!(f, "performance"),
162            FindingCategory::Style => write!(f, "style"),
163        }
164    }
165}
166
167/// A source code location
168#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
169pub struct SourceLocation {
170    pub file: PathBuf,
171    pub start_line: usize,
172    pub start_column: usize,
173    pub end_line: usize,
174    pub end_column: usize,
175}
176
177impl SourceLocation {
178    pub fn new(
179        file: PathBuf,
180        start_line: usize,
181        start_column: usize,
182        end_line: usize,
183        end_column: usize,
184    ) -> Self {
185        Self {
186            file,
187            start_line,
188            start_column,
189            end_line,
190            end_column,
191        }
192    }
193}
194
195impl std::fmt::Display for SourceLocation {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        write!(
198            f,
199            "{}:{}:{}-{}:{}",
200            self.file.display(),
201            self.start_line,
202            self.start_column,
203            self.end_line,
204            self.end_column
205        )
206    }
207}
208
209/// A security or code quality finding
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct Finding {
212    pub id: String,
213    pub rule_id: String,
214    pub message: String,
215    pub severity: Severity,
216    pub location: SourceLocation,
217    pub language: Language,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub snippet: Option<String>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub suggestion: Option<String>,
222    /// Confidence level (how certain we are this is a real issue)
223    #[serde(default)]
224    pub confidence: Confidence,
225    /// Category of finding (security, quality, performance, style)
226    #[serde(default)]
227    pub category: FindingCategory,
228    /// Stable fingerprint for baseline comparison (sha256 hash)
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub fingerprint: Option<String>,
231}
232
233impl Finding {
234    /// Compute a stable fingerprint for this finding
235    /// Based on: rule_id + relative path + normalized snippet
236    pub fn compute_fingerprint(&mut self) {
237        use sha2::{Digest, Sha256};
238
239        let mut hasher = Sha256::new();
240        hasher.update(self.rule_id.as_bytes());
241        hasher.update(self.location.file.to_string_lossy().as_bytes());
242
243        // Normalize snippet by removing whitespace
244        if let Some(snippet) = &self.snippet {
245            let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
246            hasher.update(normalized.as_bytes());
247        }
248
249        let hash = hasher.finalize();
250        self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
251    }
252}
253
254/// Code metrics for a file or function
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct CodeMetrics {
257    pub lines_of_code: usize,
258    pub lines_of_comments: usize,
259    pub blank_lines: usize,
260    pub cyclomatic_complexity: usize,
261    pub cognitive_complexity: usize,
262    pub function_count: usize,
263    pub class_count: usize,
264    pub import_count: usize,
265}
266
267/// Summary of a scan operation
268#[derive(Debug, Clone, Default, Serialize, Deserialize)]
269pub struct ScanSummary {
270    pub files_scanned: usize,
271    pub files_skipped: usize,
272    pub total_lines: usize,
273    pub findings_by_severity: std::collections::HashMap<String, usize>,
274    pub languages: std::collections::HashMap<String, usize>,
275    pub duration_ms: u64,
276}
277
278/// Configuration for RMA operations
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct RmaConfig {
281    /// Paths to exclude from scanning
282    #[serde(default)]
283    pub exclude_patterns: Vec<String>,
284
285    /// Languages to scan (empty = all supported)
286    #[serde(default)]
287    pub languages: Vec<Language>,
288
289    /// Minimum severity to report
290    #[serde(default = "default_min_severity")]
291    pub min_severity: Severity,
292
293    /// Maximum file size in bytes
294    #[serde(default = "default_max_file_size")]
295    pub max_file_size: usize,
296
297    /// Number of parallel workers (0 = auto)
298    #[serde(default)]
299    pub parallelism: usize,
300
301    /// Enable incremental mode
302    #[serde(default)]
303    pub incremental: bool,
304}
305
306fn default_min_severity() -> Severity {
307    Severity::Warning
308}
309
310fn default_max_file_size() -> usize {
311    10 * 1024 * 1024 // 10MB
312}
313
314impl Default for RmaConfig {
315    fn default() -> Self {
316        Self {
317            exclude_patterns: vec![
318                "**/node_modules/**".into(),
319                "**/target/**".into(),
320                "**/vendor/**".into(),
321                "**/.git/**".into(),
322                "**/dist/**".into(),
323                "**/build/**".into(),
324            ],
325            languages: vec![],
326            min_severity: default_min_severity(),
327            max_file_size: default_max_file_size(),
328            parallelism: 0,
329            incremental: false,
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_language_from_extension() {
340        assert_eq!(Language::from_extension("rs"), Language::Rust);
341        assert_eq!(Language::from_extension("js"), Language::JavaScript);
342        assert_eq!(Language::from_extension("py"), Language::Python);
343        assert_eq!(Language::from_extension("unknown"), Language::Unknown);
344    }
345
346    #[test]
347    fn test_severity_ordering() {
348        assert!(Severity::Info < Severity::Warning);
349        assert!(Severity::Warning < Severity::Error);
350        assert!(Severity::Error < Severity::Critical);
351    }
352
353    #[test]
354    fn test_source_location_display() {
355        let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
356        assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
357    }
358}