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,
18    parse_expiration_days, 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(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
104#[serde(rename_all = "lowercase")]
105pub enum Severity {
106    Info,
107    #[default]
108    Warning,
109    Error,
110    Critical,
111}
112
113impl std::fmt::Display for Severity {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            Severity::Info => write!(f, "info"),
117            Severity::Warning => write!(f, "warning"),
118            Severity::Error => write!(f, "error"),
119            Severity::Critical => write!(f, "critical"),
120        }
121    }
122}
123
124/// Confidence level for findings (how certain we are this is a real issue)
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
126#[serde(rename_all = "lowercase")]
127pub enum Confidence {
128    /// Low confidence - may be a false positive, requires manual review
129    Low,
130    /// Medium confidence - likely an issue but context-dependent
131    #[default]
132    Medium,
133    /// High confidence - almost certainly a real issue
134    High,
135}
136
137impl std::fmt::Display for Confidence {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        match self {
140            Confidence::Low => write!(f, "low"),
141            Confidence::Medium => write!(f, "medium"),
142            Confidence::High => write!(f, "high"),
143        }
144    }
145}
146
147/// Category of finding
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
149#[serde(rename_all = "lowercase")]
150pub enum FindingCategory {
151    /// Security vulnerabilities
152    #[default]
153    Security,
154    /// Code quality and maintainability
155    Quality,
156    /// Performance issues
157    Performance,
158    /// Style and formatting
159    Style,
160}
161
162impl std::fmt::Display for FindingCategory {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            FindingCategory::Security => write!(f, "security"),
166            FindingCategory::Quality => write!(f, "quality"),
167            FindingCategory::Performance => write!(f, "performance"),
168            FindingCategory::Style => write!(f, "style"),
169        }
170    }
171}
172
173/// A source code location
174#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
175pub struct SourceLocation {
176    pub file: PathBuf,
177    pub start_line: usize,
178    pub start_column: usize,
179    pub end_line: usize,
180    pub end_column: usize,
181}
182
183impl SourceLocation {
184    pub fn new(
185        file: PathBuf,
186        start_line: usize,
187        start_column: usize,
188        end_line: usize,
189        end_column: usize,
190    ) -> Self {
191        Self {
192            file,
193            start_line,
194            start_column,
195            end_line,
196            end_column,
197        }
198    }
199}
200
201impl std::fmt::Display for SourceLocation {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(
204            f,
205            "{}:{}:{}-{}:{}",
206            self.file.display(),
207            self.start_line,
208            self.start_column,
209            self.end_line,
210            self.end_column
211        )
212    }
213}
214
215/// A security or code quality finding
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Finding {
218    pub id: String,
219    pub rule_id: String,
220    pub message: String,
221    pub severity: Severity,
222    pub location: SourceLocation,
223    pub language: Language,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub snippet: Option<String>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub suggestion: Option<String>,
228    /// Confidence level (how certain we are this is a real issue)
229    #[serde(default)]
230    pub confidence: Confidence,
231    /// Category of finding (security, quality, performance, style)
232    #[serde(default)]
233    pub category: FindingCategory,
234    /// Stable fingerprint for baseline comparison (sha256 hash)
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub fingerprint: Option<String>,
237    /// Additional properties (e.g., import_hits, import_files_sample for OSV findings)
238    #[serde(skip_serializing_if = "Option::is_none", default)]
239    pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
240}
241
242impl Finding {
243    /// Compute a stable fingerprint for this finding
244    /// Based on: rule_id + relative path + normalized snippet
245    pub fn compute_fingerprint(&mut self) {
246        use sha2::{Digest, Sha256};
247
248        let mut hasher = Sha256::new();
249        hasher.update(self.rule_id.as_bytes());
250        hasher.update(self.location.file.to_string_lossy().as_bytes());
251
252        // Normalize snippet by removing whitespace
253        if let Some(snippet) = &self.snippet {
254            let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
255            hasher.update(normalized.as_bytes());
256        }
257
258        let hash = hasher.finalize();
259        self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
260    }
261}
262
263/// Code metrics for a file or function
264#[derive(Debug, Clone, Default, Serialize, Deserialize)]
265pub struct CodeMetrics {
266    pub lines_of_code: usize,
267    pub lines_of_comments: usize,
268    pub blank_lines: usize,
269    pub cyclomatic_complexity: usize,
270    pub cognitive_complexity: usize,
271    pub function_count: usize,
272    pub class_count: usize,
273    pub import_count: usize,
274}
275
276/// Summary of a scan operation
277#[derive(Debug, Clone, Default, Serialize, Deserialize)]
278pub struct ScanSummary {
279    pub files_scanned: usize,
280    pub files_skipped: usize,
281    pub total_lines: usize,
282    pub findings_by_severity: std::collections::HashMap<String, usize>,
283    pub languages: std::collections::HashMap<String, usize>,
284    pub duration_ms: u64,
285}
286
287/// Configuration for RMA operations
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct RmaConfig {
290    /// Paths to exclude from scanning
291    #[serde(default)]
292    pub exclude_patterns: Vec<String>,
293
294    /// Languages to scan (empty = all supported)
295    #[serde(default)]
296    pub languages: Vec<Language>,
297
298    /// Minimum severity to report
299    #[serde(default = "default_min_severity")]
300    pub min_severity: Severity,
301
302    /// Maximum file size in bytes
303    #[serde(default = "default_max_file_size")]
304    pub max_file_size: usize,
305
306    /// Number of parallel workers (0 = auto)
307    #[serde(default)]
308    pub parallelism: usize,
309
310    /// Enable incremental mode
311    #[serde(default)]
312    pub incremental: bool,
313}
314
315fn default_min_severity() -> Severity {
316    Severity::Warning
317}
318
319fn default_max_file_size() -> usize {
320    10 * 1024 * 1024 // 10MB
321}
322
323impl Default for RmaConfig {
324    fn default() -> Self {
325        Self {
326            exclude_patterns: vec![
327                "**/node_modules/**".into(),
328                "**/target/**".into(),
329                "**/vendor/**".into(),
330                "**/.git/**".into(),
331                "**/dist/**".into(),
332                "**/build/**".into(),
333            ],
334            languages: vec![],
335            min_severity: default_min_severity(),
336            max_file_size: default_max_file_size(),
337            parallelism: 0,
338            incremental: false,
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn test_language_from_extension() {
349        assert_eq!(Language::from_extension("rs"), Language::Rust);
350        assert_eq!(Language::from_extension("js"), Language::JavaScript);
351        assert_eq!(Language::from_extension("py"), Language::Python);
352        assert_eq!(Language::from_extension("unknown"), Language::Unknown);
353    }
354
355    #[test]
356    fn test_severity_ordering() {
357        assert!(Severity::Info < Severity::Warning);
358        assert!(Severity::Warning < Severity::Error);
359        assert!(Severity::Error < Severity::Critical);
360    }
361
362    #[test]
363    fn test_source_location_display() {
364        let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
365        assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
366    }
367}