(),
+ "Updated warnings from compilation log"
+ );
+ }
+
+ /// Clear warnings for the specified paths.
+ /// This is called when files are removed or when we know they should no longer have warnings.
+ pub fn clear_warnings_for_paths(&mut self, paths: impl IntoIterator- )
+ where
+ P: Borrow,
+ {
+ for path in paths {
+ self.warnings.remove(path.borrow());
+ }
+
+ tracing::debug!(
+ files_with_warnings = self.warnings.len(),
+ total_warnings = self.warnings.values().map(|w| w.len()).sum::(),
+ "Cleared warnings for paths"
+ );
+ }
+
+ /// Get all warnings as a map from file path to warnings.
+ pub fn get_all_warnings(&self) -> &BTreeMap> {
+ &self.warnings
+ }
+
+ /// Get the total number of warnings across all files.
+ pub fn warning_count(&self) -> usize {
+ self.warnings.values().map(|w| w.len()).sum()
+ }
+
+ /// Check if there are any warnings tracked.
+ pub fn has_warnings(&self) -> bool {
+ !self.warnings.is_empty()
+ }
+}
+
+impl Default for WarningTracker {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::ghci::parse::CompilingModule;
+ use crate::ghci::parse::PositionRange;
+ use crate::ghci::parse::{GhcDiagnostic, Severity};
+
+ fn create_test_diagnostic(severity: Severity, path: &str, message: &str) -> GhcDiagnostic {
+ GhcDiagnostic {
+ severity,
+ path: Some(path.into()),
+ span: PositionRange::new(1, 1, 1, 1),
+ message: message.to_string(),
+ }
+ }
+
+ fn create_test_compilation_log(
+ diagnostics: Vec,
+ compiled_modules: Vec,
+ ) -> CompilationLog {
+ CompilationLog {
+ diagnostics,
+ compiled_modules,
+ summary: None,
+ }
+ }
+
+ #[test]
+ fn test_new_tracker_is_empty() {
+ let tracker = WarningTracker::new();
+ assert!(!tracker.has_warnings());
+ assert_eq!(tracker.warning_count(), 0);
+ assert_eq!(tracker.warnings.len(), 0);
+ }
+
+ #[test]
+ fn test_mark_file_changed() {
+ let mut tracker = WarningTracker::new();
+ let base_dir = std::env::current_dir().unwrap();
+ let path = NormalPath::new("src/test.hs", &base_dir).unwrap();
+
+ tracker.mark_file_changed(path.clone());
+ assert!(tracker.current_changed_files.contains(&path));
+
+ tracker.reset_changed_files();
+ assert!(!tracker.current_changed_files.contains(&path));
+ }
+
+ #[test]
+ fn test_clear_warnings_for_paths() {
+ let mut tracker = WarningTracker::new();
+ let base_dir = std::env::current_dir().unwrap();
+ let path1 = NormalPath::new("src/test1.hs", &base_dir).unwrap();
+ let path2 = NormalPath::new("src/test2.hs", &base_dir).unwrap();
+
+ let warnings = vec![create_test_diagnostic(
+ Severity::Warning,
+ "src/test1.hs",
+ "unused import",
+ )];
+
+ tracker.warnings.insert(path1.clone(), warnings.clone());
+ tracker.warnings.insert(path2.clone(), warnings);
+ assert_eq!(tracker.warnings.len(), 2);
+
+ tracker.clear_warnings_for_paths([&path1]);
+ assert_eq!(tracker.warnings.len(), 1);
+ assert!(!tracker.warnings.contains_key(&path1));
+ assert!(tracker.warnings.contains_key(&path2));
+ }
+
+ #[test]
+ fn test_update_warnings_from_log_direct_change() {
+ let mut tracker = WarningTracker::new();
+ let base_dir = std::env::current_dir().unwrap();
+ let path = NormalPath::new("src/test.hs", &base_dir).unwrap();
+
+ // Add initial warnings
+ let old_warning = create_test_diagnostic(Severity::Warning, "src/test.hs", "old warning");
+ tracker.warnings.insert(path.clone(), vec![old_warning]);
+
+ // Mark file as changed
+ tracker.mark_file_changed(path.clone());
+
+ // Create compilation log with no warnings for the changed file
+ let log = create_test_compilation_log(
+ vec![], // No warnings in this compilation
+ vec![CompilingModule {
+ name: "Test".to_string(),
+ path: "src/test.hs".into(),
+ }],
+ );
+
+ tracker.update_warnings_from_log(&log);
+
+ // Warnings should be cleared since file was directly changed and has no new warnings
+ assert!(!tracker.has_warnings());
+ }
+
+ #[test]
+ fn test_update_warnings_from_log_dependency_change() {
+ let mut tracker = WarningTracker::new();
+ let base_dir = std::env::current_dir().unwrap();
+ let path = NormalPath::new("src/test.hs", &base_dir).unwrap();
+
+ // Add initial warnings
+ let old_warning = create_test_diagnostic(Severity::Warning, "src/test.hs", "old warning");
+ tracker
+ .warnings
+ .insert(path.clone(), vec![old_warning.clone()]);
+
+ // Don't mark file as changed (dependency-driven recompilation)
+
+ // Create compilation log with no warnings for the file
+ let log = create_test_compilation_log(
+ vec![], // No warnings in this compilation
+ vec![CompilingModule {
+ name: "Test".to_string(),
+ path: "src/test.hs".into(),
+ }],
+ );
+
+ tracker.update_warnings_from_log(&log);
+
+ // Warnings should be kept since file was recompiled due to dependencies
+ assert!(tracker.has_warnings());
+ assert_eq!(tracker.warning_count(), 1);
+ }
+
+ #[test]
+ fn test_edge_case_empty_file_path() {
+ let mut tracker = WarningTracker::new();
+
+ let diagnostic_no_path = GhcDiagnostic {
+ severity: Severity::Warning,
+ path: None,
+ span: PositionRange::new(1, 1, 1, 1),
+ message: "warning without path".to_string(),
+ };
+
+ let log = create_test_compilation_log(vec![diagnostic_no_path], vec![]);
+
+ tracker.update_warnings_from_log(&log);
+
+ // Should handle diagnostics without paths gracefully
+ assert!(!tracker.has_warnings());
+ }
+
+ #[test]
+ fn test_edge_case_large_number_of_warnings() {
+ let mut tracker = WarningTracker::new();
+ let base_dir = std::env::current_dir().unwrap();
+
+ // Create 1000 warnings across 100 files
+ for i in 0..100 {
+ let path = NormalPath::new(format!("src/test{}.hs", i), &base_dir).unwrap();
+ let mut warnings = Vec::new();
+ for j in 0..10 {
+ warnings.push(create_test_diagnostic(
+ Severity::Warning,
+ &format!("src/test{}.hs", i),
+ &format!("warning {} in file {}", j, i),
+ ));
+ }
+ tracker.warnings.insert(path, warnings);
+ }
+
+ assert_eq!(tracker.warning_count(), 1000);
+ assert_eq!(tracker.warnings.len(), 100);
+ }
+
+ #[test]
+ fn test_edge_case_special_characters_in_paths() {
+ let mut tracker = WarningTracker::new();
+ let base_dir = std::env::current_dir().unwrap();
+
+ // Test with various special characters that might appear in file paths
+ let special_paths = vec![
+ "src/test with spaces.hs",
+ "src/test-with-dashes.hs",
+ "src/test_with_underscores.hs",
+ "src/test.with.dots.hs",
+ ];
+
+ for path_str in special_paths {
+ if let Ok(path) = NormalPath::new(path_str, &base_dir) {
+ let warning = create_test_diagnostic(Severity::Warning, path_str, "test warning");
+ tracker.warnings.insert(path, vec![warning]);
+ }
+ }
+
+ assert!(!tracker.warnings.is_empty());
+ }
+}