Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ nonmax = "0.5.5"
num-bigint = "0.4.6"
num-traits = "0.2.19"
papaya = "0.2.1"
percent-encoding = "2.3.1"
petgraph = { version = "0.8.2", default-features = false }
phf = "0.12.0"
phf_codegen = "0.12.0"
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_diagnostics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ doctest = false
[dependencies]
cow-utils = { workspace = true }
miette = { workspace = true }
percent-encoding = { workspace = true }
181 changes: 171 additions & 10 deletions crates/oxc_diagnostics/src/service.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::{
borrow::Cow,
io::{ErrorKind, Write},
path::{Path, PathBuf},
sync::{Arc, mpsc},
};

use cow_utils::CowUtils;
use miette::LabeledSpan;
use percent_encoding::AsciiSet;
#[cfg(not(windows))]
use std::fs::canonicalize as strict_canonicalize;

use crate::{
Error, NamedSource, OxcDiagnostic, Severity,
Expand Down Expand Up @@ -128,20 +132,28 @@ impl DiagnosticService {
/// Wrap [diagnostics] with the source code and path, converting them into [Error]s.
///
/// [diagnostics]: OxcDiagnostic
pub fn wrap_diagnostics<P: AsRef<Path>>(
pub fn wrap_diagnostics<C: AsRef<Path>, P: AsRef<Path>>(
cwd: C,
path: P,
source_text: &str,
source_start: u32,
diagnostics: Vec<OxcDiagnostic>,
) -> (PathBuf, Vec<Error>) {
let path = path.as_ref();
let path_display = path.to_string_lossy();
// replace windows \ path separator with posix style one
// reflects what eslint is outputting
let path_display = path_display.cow_replace('\\', "/");
) -> Vec<Error> {
// TODO: This causes snapshots to fail when running tests through a JetBrains terminal.
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()));
let diagnostics = diagnostics
diagnostics
.into_iter()
.map(|diagnostic| {
if source_start == 0 {
Expand All @@ -166,8 +178,7 @@ impl DiagnosticService {
}
}
})
.collect();
(path.to_path_buf(), diagnostics)
.collect()
}

/// # Panics
Expand Down Expand Up @@ -260,3 +271,153 @@ impl DiagnosticService {
}
}
}

// The following from_file_path and strict_canonicalize implementations are from tower-lsp-community/tower-lsp-server
// available under the MIT License or Apache 2.0 License.
//
// Copyright (c) 2023 Eyal Kalderon
// https://github.com/tower-lsp-community/tower-lsp-server/blob/85506ddcbd108c514438e0b62e0eb858c812adcf/src/uri_ext.rs

const ASCII_SET: AsciiSet =
// RFC3986 allows only alphanumeric characters, `-`, `.`, `_`, and `~` in the path.
percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~')
// we do not want path separators to be percent-encoded
.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) {
// we want to write a triple-slash path for Windows paths
// it's a shorthand for `file://localhost/C:/Windows` with the `localhost` omitted.
let mut components = fragment.components();
let drive = components.next();

Some(format!(
"file:///{:?}:/{}",
drive.unwrap().as_os_str().to_string_lossy(),
percent_encoding::utf8_percent_encode(
// Skip the drive character.
&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)
))
}
}

/// On Windows, rewrites the wide path prefix `\\?\C:` to `C:`
/// Source: https://stackoverflow.com/a/70970317
#[inline]
#[cfg(windows)]
fn strict_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
use std::io;

fn impl_(path: PathBuf) -> 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() {
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.to_string(), 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 = Uri::from_file_path(path).unwrap();
assert_eq!(uri.to_string(), expected);
}
}
}
4 changes: 2 additions & 2 deletions crates/oxc_linter/src/service/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,14 +550,14 @@ impl<'l> Runtime<'l> {

if !messages.is_empty() {
let errors = messages.into_iter().map(Into::into).collect();
let path = path.strip_prefix(&me.cwd).unwrap_or(path);
let diagnostics = DiagnosticService::wrap_diagnostics(
&me.cwd,
path,
dep.source_text,
section.source.start,
errors,
);
tx_error.send(Some(diagnostics)).unwrap();
tx_error.send(Some((path.to_path_buf(), diagnostics))).unwrap();
}
}
// If the new source text is owned, that means it was modified,
Expand Down
Loading