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::*;