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 security or code quality finding
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Finding {
220    pub id: String,
221    pub rule_id: String,
222    pub message: String,
223    pub severity: Severity,
224    pub location: SourceLocation,
225    pub language: Language,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub snippet: Option<String>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub suggestion: Option<String>,
230    /// Confidence level (how certain we are this is a real issue)
231    #[serde(default)]
232    pub confidence: Confidence,
233    /// Category of finding (security, quality, performance, style)
234    #[serde(default)]
235    pub category: FindingCategory,
236    /// Stable fingerprint for baseline comparison (sha256 hash)
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub fingerprint: Option<String>,
239    /// Additional properties (e.g., import_hits, import_files_sample for OSV findings)
240    #[serde(skip_serializing_if = "Option::is_none", default)]
241    pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
242}
243
244impl Finding {
245    /// Compute a stable fingerprint for this finding
246    /// Based on: rule_id + relative path + normalized snippet
247    pub fn compute_fingerprint(&mut self) {
248        use sha2::{Digest, Sha256};
249
250        let mut hasher = Sha256::new();
251        hasher.update(self.rule_id.as_bytes());
252        hasher.update(self.location.file.to_string_lossy().as_bytes());
253
254        // Normalize snippet by removing whitespace
255        if let Some(snippet) = &self.snippet {
256            let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
257            hasher.update(normalized.as_bytes());
258        }
259
260        let hash = hasher.finalize();
261        self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
262    }
263}
264
265/// Code metrics for a file or function
266#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct CodeMetrics {
268    pub lines_of_code: usize,
269    pub lines_of_comments: usize,
270    pub blank_lines: usize,
271    pub cyclomatic_complexity: usize,
272    pub cognitive_complexity: usize,
273    pub function_count: usize,
274    pub class_count: usize,
275    pub import_count: usize,
276}
277
278/// Summary of a scan operation
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280pub struct ScanSummary {
281    pub files_scanned: usize,
282    pub files_skipped: usize,
283    pub total_lines: usize,
284    pub findings_by_severity: std::collections::HashMap<String, usize>,
285    pub languages: std::collections::HashMap<String, usize>,
286    pub duration_ms: u64,
287}
288
289/// Configuration for RMA operations
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct RmaConfig {
292    /// Paths to exclude from scanning
293    #[serde(default)]
294    pub exclude_patterns: Vec<String>,
295
296    /// Languages to scan (empty = all supported)
297    #[serde(default)]
298    pub languages: Vec<Language>,
299
300    /// Minimum severity to report
301    #[serde(default = "default_min_severity")]
302    pub min_severity: Severity,
303
304    /// Maximum file size in bytes
305    #[serde(default = "default_max_file_size")]
306    pub max_file_size: usize,
307
308    /// Number of parallel workers (0 = auto)
309    #[serde(default)]
310    pub parallelism: usize,
311
312    /// Enable incremental mode
313    #[serde(default)]
314    pub incremental: bool,
315}
316
317fn default_min_severity() -> Severity {
318    Severity::Warning
319}
320
321fn default_max_file_size() -> usize {
322    10 * 1024 * 1024 // 10MB
323}
324
325impl Default for RmaConfig {
326    fn default() -> Self {
327        Self {
328            exclude_patterns: vec![
329                "**/node_modules/**".into(),
330                "**/target/**".into(),
331                "**/vendor/**".into(),
332                "**/.git/**".into(),
333                "**/dist/**".into(),
334                "**/build/**".into(),
335            ],
336            languages: vec![],
337            min_severity: default_min_severity(),
338            max_file_size: default_max_file_size(),
339            parallelism: 0,
340            incremental: false,
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_language_from_extension() {
351        assert_eq!(Language::from_extension("rs"), Language::Rust);
352        assert_eq!(Language::from_extension("js"), Language::JavaScript);
353        assert_eq!(Language::from_extension("py"), Language::Python);
354        assert_eq!(Language::from_extension("unknown"), Language::Unknown);
355    }
356
357    #[test]
358    fn test_severity_ordering() {
359        assert!(Severity::Info < Severity::Warning);
360        assert!(Severity::Warning < Severity::Error);
361        assert!(Severity::Error < Severity::Critical);
362    }
363
364    #[test]
365    fn test_source_location_display() {
366        let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
367        assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
368    }
369}