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