Skip to main content

ts_gen/util/
diagnostics.rs

1//! Diagnostic messages for unsupported or partially-supported TS constructs.
2
3use std::path::{Path, PathBuf};
4
5/// Source location for a diagnostic.
6#[derive(Clone, Debug)]
7pub struct SourceLocation {
8    pub file: PathBuf,
9    pub line: u32,
10    pub col: u32,
11}
12
13impl std::fmt::Display for SourceLocation {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "{}:{}:{}", self.file.display(), self.line, self.col)
16    }
17}
18
19/// A diagnostic emitted during parsing or code generation.
20#[derive(Clone, Debug)]
21pub struct Diagnostic {
22    pub level: DiagnosticLevel,
23    pub message: String,
24    /// Optional source location.
25    pub location: Option<SourceLocation>,
26    /// Optional: the TS source text that triggered this diagnostic.
27    pub source_text: Option<String>,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum DiagnosticLevel {
32    /// A type could not be resolved — the output may be incorrect.
33    Error,
34    /// Something was skipped or simplified.
35    Warning,
36    /// Informational — the tool made a decision the user should know about.
37    Info,
38}
39
40/// Collects diagnostics during a parse/codegen session.
41///
42/// Call `set_file` before processing each file. `warn_at`/`error_at` accept
43/// byte offsets and automatically compute line:col from the source.
44#[derive(Clone, Debug, Default)]
45pub struct DiagnosticCollector {
46    pub diagnostics: Vec<Diagnostic>,
47    /// The file currently being processed, with its source text.
48    current_file: Option<PathBuf>,
49    current_source: Option<String>,
50    /// Pre-computed byte offsets of each line start (0-indexed) for O(log n) line lookup.
51    line_offsets: Vec<u32>,
52}
53
54impl DiagnosticCollector {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Set the current file and its source text for subsequent diagnostics.
60    /// Builds a line offset table for O(log n) offset-to-line lookup.
61    pub fn set_file(&mut self, path: &Path, source: &str) {
62        self.current_file = Some(path.to_path_buf());
63        // Build line offset table: line_offsets[i] = byte offset of start of line i+1.
64        let mut offsets = vec![0u32];
65        for (i, b) in source.bytes().enumerate() {
66            if b == b'\n' {
67                offsets.push((i + 1) as u32);
68            }
69        }
70        self.line_offsets = offsets;
71        self.current_source = Some(source.to_string());
72    }
73
74    /// Emit a warning with optional line/column.
75    pub fn warn(&mut self, message: impl Into<String>) {
76        self.diagnostics.push(Diagnostic {
77            level: DiagnosticLevel::Warning,
78            message: message.into(),
79            location: None,
80            source_text: None,
81        });
82    }
83
84    /// Emit a warning at a byte offset in the current file.
85    pub fn warn_at(&mut self, message: impl Into<String>, offset: u32) {
86        let location = self.make_location(offset);
87        self.diagnostics.push(Diagnostic {
88            level: DiagnosticLevel::Warning,
89            message: message.into(),
90            location,
91            source_text: None,
92        });
93    }
94
95    pub fn warn_with_source(&mut self, message: impl Into<String>, source: impl Into<String>) {
96        self.diagnostics.push(Diagnostic {
97            level: DiagnosticLevel::Warning,
98            message: message.into(),
99            location: None,
100            source_text: Some(source.into()),
101        });
102    }
103
104    pub fn info(&mut self, message: impl Into<String>) {
105        self.diagnostics.push(Diagnostic {
106            level: DiagnosticLevel::Info,
107            message: message.into(),
108            location: None,
109            source_text: None,
110        });
111    }
112
113    pub fn error(&mut self, message: impl Into<String>) {
114        self.diagnostics.push(Diagnostic {
115            level: DiagnosticLevel::Error,
116            message: message.into(),
117            location: None,
118            source_text: None,
119        });
120    }
121
122    /// Emit an error at a byte offset in the current file.
123    pub fn error_at(&mut self, message: impl Into<String>, offset: u32) {
124        let location = self.make_location(offset);
125        self.diagnostics.push(Diagnostic {
126            level: DiagnosticLevel::Error,
127            message: message.into(),
128            location,
129            source_text: None,
130        });
131    }
132
133    fn make_location(&self, offset: u32) -> Option<SourceLocation> {
134        let file = self.current_file.as_ref()?;
135        let (line, col) = self.offset_to_line_col(offset);
136        Some(SourceLocation {
137            file: file.clone(),
138            line,
139            col,
140        })
141    }
142
143    /// Print all diagnostics to stderr, deduplicating identical messages.
144    pub fn emit(&self) {
145        let mut seen = std::collections::HashSet::new();
146        for diag in &self.diagnostics {
147            // Dedupe key: level + message (ignore location for dedup)
148            let key = format!("{:?}:{}", diag.level, diag.message);
149            if !seen.insert(key) {
150                continue;
151            }
152            let prefix = match diag.level {
153                DiagnosticLevel::Error => "error",
154                DiagnosticLevel::Warning => "warning",
155                DiagnosticLevel::Info => "info",
156            };
157            if let Some(ref loc) = diag.location {
158                eprintln!("[ts-gen {prefix}]: {loc}: {}", diag.message);
159            } else {
160                eprintln!("[ts-gen {prefix}]: {}", diag.message);
161            }
162            if let Some(ref src) = diag.source_text {
163                eprintln!("  source: {src}");
164            }
165        }
166    }
167
168    /// Compute line and column from a byte offset using the pre-built line offset table.
169    /// Returns (line, col) where both are 1-indexed.
170    /// Uses binary search for O(log n) lookup.
171    fn offset_to_line_col(&self, offset: u32) -> (u32, u32) {
172        if self.line_offsets.is_empty() {
173            return (1, 1);
174        }
175        // Binary search: find the last line_offset <= offset
176        let line_idx = match self.line_offsets.binary_search(&offset) {
177            Ok(i) => i,
178            Err(i) => i.saturating_sub(1),
179        };
180        let line = (line_idx as u32) + 1;
181        let col = offset - self.line_offsets[line_idx] + 1;
182        (line, col)
183    }
184
185    pub fn has_warnings(&self) -> bool {
186        self.diagnostics
187            .iter()
188            .any(|d| d.level == DiagnosticLevel::Warning)
189    }
190
191    pub fn has_errors(&self) -> bool {
192        self.diagnostics
193            .iter()
194            .any(|d| d.level == DiagnosticLevel::Error)
195    }
196}