oxc_diagnostics/
service.rs1use std::{
2 borrow::Cow,
3 io::{ErrorKind, Write},
4 path::{Path, PathBuf},
5 sync::{Arc, mpsc},
6};
7
8use cow_utils::CowUtils;
9use percent_encoding::AsciiSet;
10#[cfg(not(windows))]
11use std::fs::canonicalize as strict_canonicalize;
12
13use crate::{
14 Error, NamedSource, OxcDiagnostic, Severity,
15 reporter::{DiagnosticReporter, DiagnosticResult},
16};
17
18pub type DiagnosticTuple = (PathBuf, Vec<Error>);
19pub type DiagnosticSender = mpsc::Sender<DiagnosticTuple>;
20pub type DiagnosticReceiver = mpsc::Receiver<DiagnosticTuple>;
21
22pub struct DiagnosticService {
52 reporter: Box<dyn DiagnosticReporter>,
53
54 quiet: bool,
56
57 silent: bool,
59
60 max_warnings: Option<usize>,
63
64 receiver: DiagnosticReceiver,
65}
66
67impl DiagnosticService {
68 pub fn new(reporter: Box<dyn DiagnosticReporter>) -> (Self, DiagnosticSender) {
71 let (sender, receiver) = mpsc::channel();
72 (Self { reporter, quiet: false, silent: false, max_warnings: None, receiver }, sender)
73 }
74
75 #[must_use]
81 pub fn with_quiet(mut self, yes: bool) -> Self {
82 self.quiet = yes;
83 self
84 }
85
86 #[must_use]
92 pub fn with_silent(mut self, yes: bool) -> Self {
93 self.silent = yes;
94 self
95 }
96
97 #[must_use]
106 pub fn with_max_warnings(mut self, max_warnings: Option<usize>) -> Self {
107 self.max_warnings = max_warnings;
108 self
109 }
110
111 fn max_warnings_exceeded(&self, warnings_count: usize) -> bool {
114 self.max_warnings.is_some_and(|max_warnings| warnings_count > max_warnings)
115 }
116
117 pub fn wrap_diagnostics<C: AsRef<Path>, P: AsRef<Path>>(
121 cwd: C,
122 path: P,
123 source_text: &str,
124 diagnostics: Vec<OxcDiagnostic>,
125 ) -> Vec<Error> {
126 let is_jetbrains =
128 std::env::var("TERMINAL_EMULATOR").is_ok_and(|x| x.eq("JetBrains-JediTerm"));
129
130 let path_ref = path.as_ref();
131 let path_display = if is_jetbrains { from_file_path(path_ref) } else { None }
132 .unwrap_or_else(|| {
133 let relative_path =
134 path_ref.strip_prefix(cwd).unwrap_or(path_ref).to_string_lossy();
135 let normalized_path = relative_path.cow_replace('\\', "/");
136 normalized_path.to_string()
137 });
138
139 let source = Arc::new(NamedSource::new(path_display, source_text.to_owned()));
140 diagnostics
141 .into_iter()
142 .map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
143 .collect()
144 }
145
146 pub fn run(&mut self, writer: &mut dyn Write) -> DiagnosticResult {
156 let mut warnings_count: usize = 0;
157 let mut errors_count: usize = 0;
158
159 while let Ok((path, diagnostics)) = self.receiver.recv() {
160 let mut is_minified = false;
161 for diagnostic in diagnostics {
162 let severity = diagnostic.severity();
163 let is_warning = severity == Some(Severity::Warning);
164 let is_error = severity == Some(Severity::Error) || severity.is_none();
165 if is_warning || is_error {
166 if is_warning {
167 warnings_count += 1;
168 }
169 if is_error {
170 errors_count += 1;
171 }
172 else if self.quiet {
175 continue;
176 }
177 }
178
179 if self.silent || is_minified {
180 continue;
181 }
182
183 if let Some(err_str) = self.reporter.render_error(diagnostic) {
184 if err_str.lines().any(|line| line.len() >= 1200) {
187 let minified_diagnostic = Error::new(
188 OxcDiagnostic::warn("File is too long to fit on the screen").with_help(
189 format!("{} seems like a minified file", path.display()),
190 ),
191 );
192
193 if let Some(err_str) = self.reporter.render_error(minified_diagnostic) {
194 writer
195 .write_all(err_str.as_bytes())
196 .or_else(Self::check_for_writer_error)
197 .unwrap();
198 }
199 is_minified = true;
200 continue;
201 }
202
203 writer
204 .write_all(err_str.as_bytes())
205 .or_else(Self::check_for_writer_error)
206 .unwrap();
207 }
208 }
209 }
210
211 let result = DiagnosticResult::new(
212 warnings_count,
213 errors_count,
214 self.max_warnings_exceeded(warnings_count),
215 );
216
217 if let Some(finish_output) = self.reporter.finish(&result) {
218 writer
219 .write_all(finish_output.as_bytes())
220 .or_else(Self::check_for_writer_error)
221 .unwrap();
222 }
223
224 writer.flush().or_else(Self::check_for_writer_error).unwrap();
225
226 result
227 }
228
229 fn check_for_writer_error(error: std::io::Error) -> Result<(), std::io::Error> {
230 if matches!(error.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) {
232 Ok(())
233 } else {
234 Err(error)
235 }
236 }
237}
238
239const ASCII_SET: AsciiSet =
246 percent_encoding::NON_ALPHANUMERIC
248 .remove(b'-')
249 .remove(b'.')
250 .remove(b'_')
251 .remove(b'~')
252 .remove(b'/');
254
255fn from_file_path<A: AsRef<Path>>(path: A) -> Option<String> {
256 let path = path.as_ref();
257
258 let fragment = if path.is_absolute() {
259 Cow::Borrowed(path)
260 } else {
261 match strict_canonicalize(path) {
262 Ok(path) => Cow::Owned(path),
263 Err(_) => return None,
264 }
265 };
266
267 if cfg!(windows) {
268 let mut components = fragment.components();
271 let drive = components.next();
272
273 if let Some(drive) = drive {
274 Some(format!(
275 "file:///{}{}",
276 drive.as_os_str().to_string_lossy().cow_replace('\\', "/"),
277 percent_encoding::utf8_percent_encode(
279 &components.collect::<PathBuf>().to_string_lossy().cow_replace('\\', "/"),
280 &ASCII_SET
281 )
282 ))
283 } else {
284 Some(format!(
285 "file:///{}",
286 percent_encoding::utf8_percent_encode(
287 &components.collect::<PathBuf>().to_string_lossy().cow_replace('\\', "/"),
288 &ASCII_SET
289 )
290 ))
291 }
292 } else {
293 Some(format!(
294 "file://{}",
295 percent_encoding::utf8_percent_encode(&fragment.to_string_lossy(), &ASCII_SET)
296 ))
297 }
298}
299
300#[inline]
303#[cfg(windows)]
304fn strict_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
305 use std::io;
306
307 fn impl_(path: &Path) -> std::io::Result<PathBuf> {
308 let head = path.components().next().ok_or(io::Error::other("empty path"))?;
309 let disk_;
310 let head = if let std::path::Component::Prefix(prefix) = head {
311 if let std::path::Prefix::VerbatimDisk(disk) = prefix.kind() {
312 disk_ = format!("{}:", disk as char);
313 Path::new(&disk_)
314 .components()
315 .next()
316 .ok_or(io::Error::other("failed to parse disk component"))?
317 } else {
318 head
319 }
320 } else {
321 head
322 };
323 Ok(std::iter::once(head).chain(path.components().skip(1)).collect())
324 }
325
326 let canon = std::fs::canonicalize(path)?;
327 impl_(&canon)
328}
329
330#[cfg(test)]
331mod tests {
332 use crate::service::from_file_path;
333 use std::path::PathBuf;
334
335 fn with_schema(path: &str) -> String {
336 const EXPECTED_SCHEMA: &str = if cfg!(windows) { "file:///" } else { "file://" };
337 format!("{EXPECTED_SCHEMA}{path}")
338 }
339
340 #[test]
341 #[cfg(windows)]
342 fn test_idempotent_canonicalization() {
343 use crate::service::strict_canonicalize;
344 use std::path::Path;
345
346 let lhs = strict_canonicalize(Path::new(".")).unwrap();
347 let rhs = strict_canonicalize(&lhs).unwrap();
348 assert_eq!(lhs, rhs);
349 }
350
351 #[test]
352 #[cfg(unix)]
353 fn test_path_to_uri() {
354 let paths = [
355 PathBuf::from("/some/path/to/file.txt"),
356 PathBuf::from("/some/path/to/file with spaces.txt"),
357 PathBuf::from("/some/path/[[...rest]]/file.txt"),
358 PathBuf::from("/some/path/to/файл.txt"),
359 PathBuf::from("/some/path/to/文件.txt"),
360 ];
361
362 let expected = [
363 with_schema("/some/path/to/file.txt"),
364 with_schema("/some/path/to/file%20with%20spaces.txt"),
365 with_schema("/some/path/%5B%5B...rest%5D%5D/file.txt"),
366 with_schema("/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt"),
367 with_schema("/some/path/to/%E6%96%87%E4%BB%B6.txt"),
368 ];
369
370 for (path, expected) in paths.iter().zip(expected) {
371 let uri = from_file_path(path).unwrap();
372 assert_eq!(uri.to_string(), expected);
373 }
374 }
375
376 #[test]
377 #[cfg(windows)]
378 fn test_path_to_uri_windows() {
379 let paths = [
380 PathBuf::from("C:\\some\\path\\to\\file.txt"),
381 PathBuf::from("C:\\some\\path\\to\\file with spaces.txt"),
382 PathBuf::from("C:\\some\\path\\[[...rest]]\\file.txt"),
383 PathBuf::from("C:\\some\\path\\to\\файл.txt"),
384 PathBuf::from("C:\\some\\path\\to\\文件.txt"),
385 ];
386
387 let expected = [
388 with_schema("C:/some/path/to/file.txt"),
389 with_schema("C:/some/path/to/file%20with%20spaces.txt"),
390 with_schema("C:/some/path/%5B%5B...rest%5D%5D/file.txt"),
391 with_schema("C:/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt"),
392 with_schema("C:/some/path/to/%E6%96%87%E4%BB%B6.txt"),
393 ];
394
395 for (path, expected) in paths.iter().zip(expected) {
396 let uri = from_file_path(path).unwrap();
397 assert_eq!(uri, expected);
398 }
399 }
400}