ts_gen/util/
diagnostics.rs1use std::path::{Path, PathBuf};
4
5#[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#[derive(Clone, Debug)]
21pub struct Diagnostic {
22 pub level: DiagnosticLevel,
23 pub message: String,
24 pub location: Option<SourceLocation>,
26 pub source_text: Option<String>,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum DiagnosticLevel {
32 Error,
34 Warning,
36 Info,
38}
39
40#[derive(Clone, Debug, Default)]
45pub struct DiagnosticCollector {
46 pub diagnostics: Vec<Diagnostic>,
47 current_file: Option<PathBuf>,
49 current_source: Option<String>,
50 line_offsets: Vec<u32>,
52}
53
54impl DiagnosticCollector {
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn set_file(&mut self, path: &Path, source: &str) {
62 self.current_file = Some(path.to_path_buf());
63 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 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 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 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 pub fn emit(&self) {
145 let mut seen = std::collections::HashSet::new();
146 for diag in &self.diagnostics {
147 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 fn offset_to_line_col(&self, offset: u32) -> (u32, u32) {
172 if self.line_offsets.is_empty() {
173 return (1, 1);
174 }
175 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}