use std::{
borrow::Cow,
io::{ErrorKind, Write},
path::{Path, PathBuf},
sync::{Arc, mpsc},
};
use cow_utils::CowUtils;
use percent_encoding::AsciiSet;
#[cfg(not(windows))]
use std::fs::canonicalize as strict_canonicalize;
use crate::{
Error, NamedSource, OxcDiagnostic, Severity,
reporter::{DiagnosticReporter, DiagnosticResult},
};
pub type DiagnosticSender = mpsc::Sender<Vec<Error>>;
pub type DiagnosticReceiver = mpsc::Receiver<Vec<Error>>;
pub struct DiagnosticService {
reporter: Box<dyn DiagnosticReporter>,
quiet: bool,
silent: bool,
max_warnings: Option<usize>,
receiver: DiagnosticReceiver,
}
impl DiagnosticService {
pub fn new(reporter: Box<dyn DiagnosticReporter>) -> (Self, DiagnosticSender) {
let (sender, receiver) = mpsc::channel();
(Self { reporter, quiet: false, silent: false, max_warnings: None, receiver }, sender)
}
#[must_use]
pub fn with_quiet(mut self, yes: bool) -> Self {
self.quiet = yes;
self
}
#[must_use]
pub fn with_silent(mut self, yes: bool) -> Self {
self.silent = yes;
self
}
#[must_use]
pub fn with_max_warnings(mut self, max_warnings: Option<usize>) -> Self {
self.max_warnings = max_warnings;
self
}
fn max_warnings_exceeded(&self, warnings_count: usize) -> bool {
self.max_warnings.is_some_and(|max_warnings| warnings_count > max_warnings)
}
pub fn wrap_diagnostics<C: AsRef<Path>, P: AsRef<Path>>(
cwd: C,
path: P,
source_text: &str,
diagnostics: Vec<OxcDiagnostic>,
) -> Vec<Error> {
let is_jetbrains =
std::env::var("TERMINAL_EMULATOR").is_ok_and(|x| x.eq("JetBrains-JediTerm"));
let path_ref = path.as_ref();
let path_display = if is_jetbrains { from_file_path(path_ref) } else { None }
.unwrap_or_else(|| {
let relative_path =
path_ref.strip_prefix(cwd).unwrap_or(path_ref).to_string_lossy();
let normalized_path = relative_path.cow_replace('\\', "/");
normalized_path.to_string()
});
let source = Arc::new(NamedSource::new(path_display, source_text.to_owned()));
diagnostics
.into_iter()
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
.collect()
}
pub fn run(&mut self, writer: &mut dyn Write) -> DiagnosticResult {
let mut warnings_count: usize = 0;
let mut errors_count: usize = 0;
while let Ok(diagnostics) = self.receiver.recv() {
let mut is_minified = false;
for diagnostic in diagnostics {
let severity = diagnostic.severity();
let is_warning = severity == Some(Severity::Warning);
let is_error = severity == Some(Severity::Error) || severity.is_none();
if is_warning || is_error {
if is_warning {
warnings_count += 1;
}
if is_error {
errors_count += 1;
}
else if self.quiet {
continue;
}
}
if self.silent || is_minified {
continue;
}
let path = diagnostic
.source_code()
.and_then(|source| source.name())
.map(ToString::to_string);
if let Some(err_str) = self.reporter.render_error(diagnostic) {
if err_str.lines().any(|line| line.len() >= 1200) {
let mut diagnostic =
OxcDiagnostic::warn("File is too long to fit on the screen");
if let Some(path) = path {
diagnostic =
diagnostic.with_help(format!("{path} seems like a minified file"));
}
let minified_diagnostic = Error::new(diagnostic);
if let Some(err_str) = self.reporter.render_error(minified_diagnostic) {
writer
.write_all(err_str.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
}
is_minified = true;
continue;
}
writer
.write_all(err_str.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
}
}
}
let result = DiagnosticResult::new(
warnings_count,
errors_count,
self.max_warnings_exceeded(warnings_count),
);
if let Some(finish_output) = self.reporter.finish(&result) {
writer
.write_all(finish_output.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
}
writer.flush().or_else(Self::check_for_writer_error).unwrap();
result
}
fn check_for_writer_error(error: std::io::Error) -> Result<(), std::io::Error> {
if matches!(
error.kind(),
ErrorKind::Interrupted | ErrorKind::BrokenPipe | ErrorKind::WouldBlock
) {
Ok(())
} else {
Err(error)
}
}
}
const ASCII_SET: AsciiSet =
percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~')
.remove(b'/');
fn from_file_path<A: AsRef<Path>>(path: A) -> Option<String> {
let path = path.as_ref();
let fragment = if path.is_absolute() {
Cow::Borrowed(path)
} else {
match strict_canonicalize(path) {
Ok(path) => Cow::Owned(path),
Err(_) => return None,
}
};
if cfg!(windows) {
let mut components = fragment.components();
let drive = components.next();
if let Some(drive) = drive {
Some(format!(
"file:///{}{}",
drive.as_os_str().to_string_lossy().cow_replace('\\', "/"),
percent_encoding::utf8_percent_encode(
&components.collect::<PathBuf>().to_string_lossy().cow_replace('\\', "/"),
&ASCII_SET
)
))
} else {
Some(format!(
"file:///{}",
percent_encoding::utf8_percent_encode(
&components.collect::<PathBuf>().to_string_lossy().cow_replace('\\', "/"),
&ASCII_SET
)
))
}
} else {
Some(format!(
"file://{}",
percent_encoding::utf8_percent_encode(&fragment.to_string_lossy(), &ASCII_SET)
))
}
}
#[inline]
#[cfg(windows)]
fn strict_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
use std::io;
fn impl_(path: &Path) -> std::io::Result<PathBuf> {
let head = path.components().next().ok_or(io::Error::other("empty path"))?;
let disk_;
let head = if let std::path::Component::Prefix(prefix) = head {
if let std::path::Prefix::VerbatimDisk(disk) = prefix.kind() {
disk_ = format!("{}:", disk as char);
Path::new(&disk_)
.components()
.next()
.ok_or(io::Error::other("failed to parse disk component"))?
} else {
head
}
} else {
head
};
Ok(std::iter::once(head).chain(path.components().skip(1)).collect())
}
let canon = std::fs::canonicalize(path)?;
impl_(&canon)
}
#[cfg(test)]
mod tests {
use crate::service::from_file_path;
use std::path::PathBuf;
fn with_schema(path: &str) -> String {
const EXPECTED_SCHEMA: &str = if cfg!(windows) { "file:///" } else { "file://" };
format!("{EXPECTED_SCHEMA}{path}")
}
#[test]
#[cfg(windows)]
fn test_idempotent_canonicalization() {
use crate::service::strict_canonicalize;
use std::path::Path;
let lhs = strict_canonicalize(Path::new(".")).unwrap();
let rhs = strict_canonicalize(&lhs).unwrap();
assert_eq!(lhs, rhs);
}
#[test]
#[cfg(unix)]
fn test_path_to_uri() {
let paths = [
PathBuf::from("/some/path/to/file.txt"),
PathBuf::from("/some/path/to/file with spaces.txt"),
PathBuf::from("/some/path/[[...rest]]/file.txt"),
PathBuf::from("/some/path/to/файл.txt"),
PathBuf::from("/some/path/to/文件.txt"),
];
let expected = [
with_schema("/some/path/to/file.txt"),
with_schema("/some/path/to/file%20with%20spaces.txt"),
with_schema("/some/path/%5B%5B...rest%5D%5D/file.txt"),
with_schema("/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt"),
with_schema("/some/path/to/%E6%96%87%E4%BB%B6.txt"),
];
for (path, expected) in paths.iter().zip(expected) {
let uri = from_file_path(path).unwrap();
assert_eq!(uri.clone(), expected);
}
}
#[test]
#[cfg(windows)]
fn test_path_to_uri_windows() {
let paths = [
PathBuf::from("C:\\some\\path\\to\\file.txt"),
PathBuf::from("C:\\some\\path\\to\\file with spaces.txt"),
PathBuf::from("C:\\some\\path\\[[...rest]]\\file.txt"),
PathBuf::from("C:\\some\\path\\to\\файл.txt"),
PathBuf::from("C:\\some\\path\\to\\文件.txt"),
];
let expected = [
with_schema("C:/some/path/to/file.txt"),
with_schema("C:/some/path/to/file%20with%20spaces.txt"),
with_schema("C:/some/path/%5B%5B...rest%5D%5D/file.txt"),
with_schema("C:/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt"),
with_schema("C:/some/path/to/%E6%96%87%E4%BB%B6.txt"),
];
for (path, expected) in paths.iter().zip(expected) {
let uri = from_file_path(path).unwrap();
assert_eq!(uri, expected);
}
}
}