Skip to main content

tldr_core/diagnostics/
mod.rs

1//! Diagnostics module - Unified type checking and linting across languages
2//!
3//! This module provides a unified interface for running diagnostic tools
4//! (type checkers and linters) and parsing their output into a common format.
5//!
6//! # Supported Tools by Language
7//!
8//! - **Python**: pyright (type checking), ruff (linting)
9//! - **TypeScript/JavaScript**: tsc (type checking), eslint (linting)
10//! - **Go**: go vet, golangci-lint
11//! - **Rust**: cargo check, clippy
12//! - **Java**: javac (type checking), checkstyle (linting)
13//! - **C/C++**: clang (syntax checking), clang-tidy (static analysis)
14//! - **Ruby**: rubocop (linting)
15//! - **PHP**: php -l (syntax checking), phpstan (static analysis)
16//! - **Kotlin**: kotlinc, detekt
17//! - **Swift**: swiftc, swiftlint
18//! - **C#**: dotnet build
19//! - **Scala**: scalac
20//! - **Elixir**: mix compile, credo
21//! - **Lua**: luacheck
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use tldr_core::diagnostics::{run_diagnostics, Severity};
27//!
28//! let report = run_diagnostics(
29//!     Path::new("src/"),
30//!     Language::Python,
31//!     DiagnosticsOptions::default(),
32//! )?;
33//!
34//! for diag in &report.diagnostics {
35//!     if diag.severity == Severity::Error {
36//!         println!("{}:{}:{}: {}", diag.file, diag.line, diag.column, diag.message);
37//!     }
38//! }
39//! ```
40
41#[cfg(test)]
42mod tests;
43
44use serde::{Deserialize, Serialize};
45use std::path::PathBuf;
46
47/// Diagnostic severity (LSP-compatible)
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum Severity {
51    /// Error severity - must be fixed
52    Error = 1,
53    /// Warning severity - should be addressed
54    Warning = 2,
55    /// Information severity - informational message
56    Information = 3,
57    /// Hint severity - suggestion for improvement
58    #[default]
59    Hint = 4,
60}
61
62impl std::fmt::Display for Severity {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Severity::Error => write!(f, "error"),
66            Severity::Warning => write!(f, "warning"),
67            Severity::Information => write!(f, "info"),
68            Severity::Hint => write!(f, "hint"),
69        }
70    }
71}
72
73/// A single diagnostic message
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Diagnostic {
76    /// Source file path
77    pub file: PathBuf,
78
79    /// Start line (1-indexed)
80    pub line: u32,
81
82    /// Start column (1-indexed)
83    pub column: u32,
84
85    /// End line (optional)
86    pub end_line: Option<u32>,
87
88    /// End column (optional)
89    pub end_column: Option<u32>,
90
91    /// Diagnostic severity
92    pub severity: Severity,
93
94    /// Human-readable message
95    pub message: String,
96
97    /// Error/rule code (e.g., "E501", "TS2339", "reportUnusedVariable")
98    pub code: Option<String>,
99
100    /// Tool that generated this diagnostic
101    pub source: String,
102
103    /// URL for more information
104    pub url: Option<String>,
105}
106
107impl Diagnostic {
108    /// Generate a deduplication key for this diagnostic.
109    /// Two diagnostics with the same key are considered duplicates.
110    /// Key is based on file, line, column, and a hash of the message.
111    pub fn dedupe_key(&self) -> String {
112        use std::collections::hash_map::DefaultHasher;
113        use std::hash::{Hash, Hasher};
114
115        let mut hasher = DefaultHasher::new();
116        self.message.hash(&mut hasher);
117        let msg_hash = hasher.finish();
118
119        format!(
120            "{}:{}:{}:{:x}",
121            self.file.display(),
122            self.line,
123            self.column,
124            msg_hash
125        )
126    }
127}
128
129/// Complete diagnostics report
130#[derive(Debug, Clone, Serialize, Deserialize, Default)]
131pub struct DiagnosticsReport {
132    /// All diagnostics found
133    pub diagnostics: Vec<Diagnostic>,
134
135    /// Summary counts by severity
136    pub summary: DiagnosticsSummary,
137
138    /// Tools that were run
139    pub tools_run: Vec<ToolResult>,
140
141    /// Files analyzed
142    pub files_analyzed: usize,
143}
144
145/// Summary of diagnostic counts by severity
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
147pub struct DiagnosticsSummary {
148    /// Number of errors
149    pub errors: usize,
150    /// Number of warnings
151    pub warnings: usize,
152    /// Number of informational messages
153    pub info: usize,
154    /// Number of hints
155    pub hints: usize,
156    /// Total number of diagnostics
157    pub total: usize,
158}
159
160/// Result from running a diagnostic tool
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ToolResult {
163    /// Tool name
164    pub name: String,
165    /// Tool version (if available)
166    pub version: Option<String>,
167    /// Whether the tool ran successfully
168    pub success: bool,
169    /// Duration in milliseconds
170    pub duration_ms: u64,
171    /// Number of diagnostics produced
172    pub diagnostic_count: usize,
173    /// Error message if tool failed
174    pub error: Option<String>,
175}
176
177/// Tool configuration for running diagnostics
178#[derive(Debug, Clone)]
179pub struct ToolConfig {
180    /// Tool name for display
181    pub name: &'static str,
182    /// Binary name to execute
183    pub binary: &'static str,
184    /// Command line arguments
185    pub args: Vec<String>,
186    /// Whether this is a type checker
187    pub is_type_checker: bool,
188    /// Whether this is a linter
189    pub is_linter: bool,
190}
191
192/// Options for running diagnostics
193#[derive(Debug, Clone, Default)]
194pub struct DiagnosticsOptions {
195    /// Minimum severity to report
196    pub min_severity: Severity,
197    /// Skip type checkers
198    pub no_typecheck: bool,
199    /// Skip linters
200    pub no_lint: bool,
201    /// Specific tools to run (empty = auto-detect)
202    pub tools: Vec<String>,
203    /// Timeout per tool in seconds
204    pub timeout_secs: u64,
205    /// Analyze entire project
206    pub project_mode: bool,
207    /// File patterns to filter
208    pub filter_files: Vec<String>,
209    /// Error codes to ignore
210    pub ignore_codes: Vec<String>,
211}
212
213// =============================================================================
214// Phase 6: Filtering and Summary Functions
215// =============================================================================
216
217/// Filter diagnostics by minimum severity level.
218/// Only diagnostics with severity <= min_severity are returned.
219/// (Error=1 < Warning=2 < Information=3 < Hint=4)
220pub fn filter_diagnostics_by_severity(
221    diagnostics: &[Diagnostic],
222    min_severity: Severity,
223) -> Vec<Diagnostic> {
224    diagnostics
225        .iter()
226        .filter(|d| d.severity <= min_severity)
227        .cloned()
228        .collect()
229}
230
231/// Compute summary statistics from a list of diagnostics.
232pub fn compute_summary(diagnostics: &[Diagnostic]) -> DiagnosticsSummary {
233    let mut summary = DiagnosticsSummary::default();
234
235    for diag in diagnostics {
236        match diag.severity {
237            Severity::Error => summary.errors += 1,
238            Severity::Warning => summary.warnings += 1,
239            Severity::Information => summary.info += 1,
240            Severity::Hint => summary.hints += 1,
241        }
242    }
243
244    summary.total = diagnostics.len();
245    summary
246}
247
248/// Deduplicate diagnostics based on their dedupe_key.
249/// Returns diagnostics with duplicates removed (keeps first occurrence).
250pub fn dedupe_diagnostics(diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
251    use std::collections::HashSet;
252
253    let mut seen = HashSet::new();
254    diagnostics
255        .into_iter()
256        .filter(|d| seen.insert(d.dedupe_key()))
257        .collect()
258}
259
260/// Compute exit code based on diagnostics summary.
261/// - 0 if no errors (or only warnings without strict mode)
262/// - 1 if errors found (or warnings with strict mode)
263pub fn compute_exit_code(summary: &DiagnosticsSummary, strict: bool) -> i32 {
264    if summary.errors > 0 || (strict && summary.warnings > 0) {
265        1
266    } else {
267        0
268    }
269}
270
271// =============================================================================
272// Submodules (Phases 7-9)
273// =============================================================================
274
275pub mod parsers;
276pub mod runner;
277
278// Re-exports
279pub use parsers::*;
280pub use runner::*;