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