debtmap 0.16.4

Code complexity and technical debt analyzer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Environment trait and implementations for debtmap analysis.
//!
//! This module provides the core environment abstraction for the effect system,
//! defining all I/O capabilities that analysis operations may need. The environment
//! pattern enables:
//!
//! - **Dependency injection**: Pass capabilities explicitly rather than using globals
//! - **Testability**: Use mock environments in tests
//! - **Pure core**: Separate I/O from business logic at the type level
//!
//! # The Environment Pattern
//!
//! The environment pattern (also known as Reader pattern) threads shared context
//! through a computation without explicit parameter passing. Effect types are
//! parameterized by the environment, allowing functions to declare what capabilities
//! they need.
//!
//! ```rust,ignore
//! use debtmap::effects::AnalysisEffect;
//!
//! // This function needs file system access, declared via the Effect type
//! fn read_source(path: PathBuf) -> AnalysisEffect<String> {
//!     Effect::new(|env| env.file_system().read_to_string(&path))
//! }
//! ```
//!
//! # Usage
//!
//! ## Production Code
//!
//! ```rust,ignore
//! use debtmap::env::RealEnv;
//! use debtmap::config::DebtmapConfig;
//!
//! let config = DebtmapConfig::default();
//! let env = RealEnv::new(config);
//! let result = my_effect.run(&env)?;
//! ```
//!
//! ## Testing
//!
//! ```rust,ignore
//! use debtmap::env::TestEnv;
//!
//! let mut env = TestEnv::new();
//! env.add_file("test.rs", "fn main() {}");
//! let result = my_effect.run(&env)?;
//! ```

use crate::config::DebtmapConfig;
use crate::io::real::{MemoryCache, NoOpCache, RealCoverageLoader, RealFileSystem};
use crate::io::traits::{Cache, CoverageLoader, FileSystem};
use crate::progress::traits::{HasProgress, ProgressSink};
use crate::progress::SilentProgressSink;
use std::sync::Arc;

/// Environment trait defining all I/O capabilities for analysis operations.
///
/// This trait provides access to all external resources that analysis code
/// might need. By parameterizing Effect types with this trait, we can:
///
/// 1. Make I/O requirements explicit in function signatures
/// 2. Easily swap implementations for testing
/// 3. Add new capabilities without changing existing code
///
/// # Thread Safety
///
/// All environment implementations must be `Clone + Send + Sync` to support
/// parallel analysis. Implementations should use `Arc` for shared resources.
///
/// # Design Notes
///
/// The environment returns trait objects (`&dyn Trait`) rather than concrete
/// types for flexibility. This allows different implementations while keeping
/// the interface stable.
pub trait AnalysisEnv: Clone + Send + Sync {
    /// Access file system operations.
    ///
    /// Use this for reading source files, writing output, etc.
    fn file_system(&self) -> &dyn FileSystem;

    /// Access coverage data loading.
    ///
    /// Use this for loading LCOV, Cobertura, or other coverage formats.
    fn coverage_loader(&self) -> &dyn CoverageLoader;

    /// Access cache operations.
    ///
    /// Use this for caching parsed ASTs, analysis results, etc.
    fn cache(&self) -> &dyn Cache;

    /// Access the debtmap configuration.
    ///
    /// This provides access to thresholds, scoring weights, and other settings.
    fn config(&self) -> &DebtmapConfig;

    /// Create a new environment with the updated config.
    ///
    /// This is used by the Reader pattern's `local` operation to run effects
    /// with temporarily modified configuration. Returns a new environment
    /// instance with the updated config (immutable pattern).
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use debtmap::env::AnalysisEnv;
    ///
    /// fn with_strict_thresholds<E: AnalysisEnv>(env: E) -> E {
    ///     let mut config = env.config().clone();
    ///     // Modify config...
    ///     env.with_config(config)
    /// }
    /// ```
    fn with_config(self, config: DebtmapConfig) -> Self;
}

/// Production environment implementation.
///
/// This is the default environment used in production, providing real
/// implementations of all I/O traits.
///
/// # Example
///
/// ```rust
/// use debtmap::env::RealEnv;
/// use debtmap::config::DebtmapConfig;
///
/// let config = DebtmapConfig::default();
/// let env = RealEnv::new(config);
///
/// // Now use env with Effect types
/// ```
#[derive(Clone)]
pub struct RealEnv {
    file_system: Arc<dyn FileSystem>,
    coverage_loader: Arc<dyn CoverageLoader>,
    cache: Arc<dyn Cache>,
    config: DebtmapConfig,
    progress: Arc<dyn ProgressSink>,
}

impl RealEnv {
    /// Create a new production environment with the given configuration.
    ///
    /// This sets up:
    /// - Real file system access
    /// - Real coverage loader (LCOV, etc.)
    /// - In-memory cache (for analysis results)
    /// - Silent progress sink (default, no output)
    ///
    /// For progress reporting, use [`RealEnv::with_progress`] to provide
    /// a custom progress sink.
    pub fn new(config: DebtmapConfig) -> Self {
        Self {
            file_system: Arc::new(RealFileSystem::new()),
            coverage_loader: Arc::new(RealCoverageLoader::new()),
            cache: Arc::new(MemoryCache::new()),
            config,
            progress: Arc::new(SilentProgressSink),
        }
    }

    /// Create an environment with a custom progress sink.
    ///
    /// Use this when you need progress reporting:
    ///
    /// ```rust,ignore
    /// use debtmap::progress::CliProgressSink;
    ///
    /// let sink = Arc::new(CliProgressSink::new(false));
    /// let env = RealEnv::with_progress(config, sink);
    /// ```
    ///
    /// For testing, use [`RecordingProgressSink`](crate::progress::RecordingProgressSink)
    /// to capture and verify progress events.
    pub fn with_progress(config: DebtmapConfig, progress: Arc<dyn ProgressSink>) -> Self {
        Self {
            file_system: Arc::new(RealFileSystem::new()),
            coverage_loader: Arc::new(RealCoverageLoader::new()),
            cache: Arc::new(MemoryCache::new()),
            config,
            progress,
        }
    }

    /// Create an environment with no caching.
    ///
    /// Useful for one-shot analysis where caching overhead isn't worth it.
    pub fn without_cache(config: DebtmapConfig) -> Self {
        Self {
            file_system: Arc::new(RealFileSystem::new()),
            coverage_loader: Arc::new(RealCoverageLoader::new()),
            cache: Arc::new(NoOpCache::new()),
            config,
            progress: Arc::new(SilentProgressSink),
        }
    }

    /// Create an environment with custom implementations.
    ///
    /// This is useful for advanced use cases where you need to customize
    /// specific components while keeping others at their defaults.
    pub fn custom(
        file_system: Arc<dyn FileSystem>,
        coverage_loader: Arc<dyn CoverageLoader>,
        cache: Arc<dyn Cache>,
        config: DebtmapConfig,
    ) -> Self {
        Self {
            file_system,
            coverage_loader,
            cache,
            config,
            progress: Arc::new(SilentProgressSink),
        }
    }

    /// Create an environment with all custom components including progress.
    ///
    /// This is the most flexible constructor, allowing customization of
    /// all environment components.
    pub fn custom_with_progress(
        file_system: Arc<dyn FileSystem>,
        coverage_loader: Arc<dyn CoverageLoader>,
        cache: Arc<dyn Cache>,
        config: DebtmapConfig,
        progress: Arc<dyn ProgressSink>,
    ) -> Self {
        Self {
            file_system,
            coverage_loader,
            cache,
            config,
            progress,
        }
    }

    /// Update the configuration.
    ///
    /// Returns a new environment with the updated config (immutable pattern).
    pub fn with_config(self, config: DebtmapConfig) -> Self {
        Self { config, ..self }
    }

    /// Set the progress sink.
    ///
    /// Returns a new environment with the updated progress sink (immutable pattern).
    pub fn set_progress(self, progress: Arc<dyn ProgressSink>) -> Self {
        Self { progress, ..self }
    }
}

impl AnalysisEnv for RealEnv {
    fn file_system(&self) -> &dyn FileSystem {
        &*self.file_system
    }

    fn coverage_loader(&self) -> &dyn CoverageLoader {
        &*self.coverage_loader
    }

    fn cache(&self) -> &dyn Cache {
        &*self.cache
    }

    fn config(&self) -> &DebtmapConfig {
        &self.config
    }

    fn with_config(self, config: DebtmapConfig) -> Self {
        Self { config, ..self }
    }
}

impl HasProgress for RealEnv {
    fn progress(&self) -> &dyn ProgressSink {
        &*self.progress
    }
}

impl Default for RealEnv {
    fn default() -> Self {
        Self::new(DebtmapConfig::default())
    }
}

impl std::fmt::Debug for RealEnv {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RealEnv")
            .field("config", &self.config)
            .finish_non_exhaustive()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;
    use tempfile::TempDir;

    #[test]
    fn test_real_env_creation() {
        let config = DebtmapConfig::default();
        let env = RealEnv::new(config);

        // Config should be accessible (just verify it doesn't panic)
        let _ = env.config();
    }

    #[test]
    fn test_real_env_file_system() {
        let temp_dir = TempDir::new().unwrap();
        let file_path = temp_dir.path().join("test.txt");

        let env = RealEnv::default();

        // File doesn't exist yet
        assert!(!env.file_system().exists(&file_path));

        // Write via std::fs for testing
        std::fs::write(&file_path, "test content").unwrap();

        // Now it exists
        assert!(env.file_system().exists(&file_path));
        assert!(env.file_system().is_file(&file_path));

        // Read via env
        let content = env.file_system().read_to_string(&file_path).unwrap();
        assert_eq!(content, "test content");
    }

    #[test]
    fn test_real_env_cache() {
        let env = RealEnv::default();

        // Cache operations
        env.cache().set("test_key", b"test_value").unwrap();
        assert_eq!(env.cache().get("test_key"), Some(b"test_value".to_vec()));

        env.cache().invalidate("test_key").unwrap();
        assert!(env.cache().get("test_key").is_none());
    }

    #[test]
    fn test_real_env_without_cache() {
        let env = RealEnv::without_cache(DebtmapConfig::default());

        // Cache operations should work but return None
        env.cache().set("key", b"value").unwrap();
        assert!(env.cache().get("key").is_none());
    }

    #[test]
    fn test_real_env_with_config() {
        let config1 = DebtmapConfig::default();
        let config2 = DebtmapConfig {
            ignore: Some(crate::config::IgnoreConfig {
                patterns: vec!["test".to_string()],
            }),
            ..Default::default()
        };

        let env = RealEnv::new(config1);
        assert!(env.config().ignore.is_none());

        let env = env.with_config(config2);
        assert!(env.config().ignore.is_some());
    }

    #[test]
    fn test_real_env_is_send_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<RealEnv>();
    }

    #[test]
    fn test_real_env_is_clone() {
        let env1 = RealEnv::default();
        let env2 = env1.clone();

        // Both should work independently
        assert!(!env1.file_system().exists(Path::new("/nonexistent")));
        assert!(!env2.file_system().exists(Path::new("/nonexistent")));
    }

    #[test]
    fn test_real_env_default_has_silent_progress() {
        let env = RealEnv::default();

        // Silent progress should not panic on any operation
        env.progress().start_stage("Test");
        env.progress().report("Test", 0, 10);
        env.progress().complete_stage("Test");
        env.progress().warn("Warning");
    }

    #[test]
    fn test_real_env_with_progress() {
        use crate::progress::RecordingProgressSink;

        let recorder = Arc::new(RecordingProgressSink::new());
        let env = RealEnv::with_progress(DebtmapConfig::default(), recorder.clone());

        env.progress().start_stage("Analysis");
        env.progress().report("Analysis", 5, 10);
        env.progress().complete_stage("Analysis");

        assert_eq!(recorder.stages(), vec!["Analysis"]);
        assert_eq!(recorder.completed_stages(), vec!["Analysis"]);
        assert_eq!(recorder.event_count(), 3);
    }

    #[test]
    fn test_real_env_set_progress() {
        use crate::progress::RecordingProgressSink;

        let env = RealEnv::default();
        let recorder = Arc::new(RecordingProgressSink::new());

        let env = env.set_progress(recorder.clone());

        env.progress().start_stage("Test");
        assert_eq!(recorder.stages(), vec!["Test"]);
    }
}