Skip to content

Commit 9dffe6e

Browse files
committed
fix(linter/plugins): handle non-UTF8 file paths
1 parent 8f4137d commit 9dffe6e

File tree

8 files changed

+35
-30
lines changed

8 files changed

+35
-30
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ toml = { version = "0.9.8" }
230230
tower-lsp-server = "0.22.1" # LSP server framework
231231
tracing-subscriber = "0.3.20" # Tracing implementation
232232
ureq = { version = "3.1.4", default-features = false } # HTTP client
233+
url = { version = "2.5.7" } # URL parsing
233234
walkdir = "2.5.0" # Directory traversal
234235

235236
[workspace.metadata.cargo-shear]

apps/oxlint/src-js/plugins/load.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { pathToFileURL } from "node:url";
2-
31
import { createContext } from "./context.js";
42
import { getErrorMessage } from "../utils/utils.js";
53

@@ -82,7 +80,7 @@ interface CreateOnceRuleDetails extends RuleDetailsBase {
8280
}
8381

8482
// Absolute paths of plugins which have been loaded
85-
const registeredPluginPaths = new Set<string>();
83+
const registeredPluginUrls = new Set<string>();
8684

8785
// Rule objects for loaded rules.
8886
// Indexed by `ruleId`, which is passed to `lintFile`.
@@ -109,13 +107,13 @@ interface PluginDetails {
109107
*
110108
* Main logic is in separate function `loadPluginImpl`, because V8 cannot optimize functions containing try/catch.
111109
*
112-
* @param path - Absolute path of plugin file
110+
* @param url - Absolute path of plugin file as a `file://...` URL
113111
* @param packageName - Optional package name from `package.json` (fallback if `plugin.meta.name` is not defined)
114112
* @returns Plugin details or error serialized to JSON string
115113
*/
116-
export async function loadPlugin(path: string, packageName: string | null): Promise<string> {
114+
export async function loadPlugin(url: string, packageName: string | null): Promise<string> {
117115
try {
118-
const res = await loadPluginImpl(path, packageName);
116+
const res = await loadPluginImpl(url, packageName);
119117
return JSON.stringify({ Success: res });
120118
} catch (err) {
121119
return JSON.stringify({ Failure: getErrorMessage(err) });
@@ -125,7 +123,7 @@ export async function loadPlugin(path: string, packageName: string | null): Prom
125123
/**
126124
* Load a plugin.
127125
*
128-
* @param path - Absolute path of plugin file
126+
* @param url - Absolute path of plugin file as a `file://...` URL
129127
* @param packageName - Optional package name from `package.json` (fallback if `plugin.meta.name` is not defined)
130128
* @returns - Plugin details
131129
* @throws {Error} If plugin has already been registered
@@ -134,13 +132,13 @@ export async function loadPlugin(path: string, packageName: string | null): Prom
134132
* @throws {TypeError} if `plugin.meta.name` is not a string
135133
* @throws {*} If plugin throws an error during import
136134
*/
137-
async function loadPluginImpl(path: string, packageName: string | null): Promise<PluginDetails> {
135+
async function loadPluginImpl(url: string, packageName: string | null): Promise<PluginDetails> {
138136
if (DEBUG) {
139-
if (registeredPluginPaths.has(path)) throw new Error("This plugin has already been registered");
140-
registeredPluginPaths.add(path);
137+
if (registeredPluginUrls.has(url)) throw new Error("This plugin has already been registered");
138+
registeredPluginUrls.add(url);
141139
}
142140

143-
const { default: plugin } = (await import(pathToFileURL(path).href)) as { default: Plugin };
141+
const { default: plugin } = (await import(url)) as { default: Plugin };
144142

145143
// TODO: Use a validation library to assert the shape of the plugin, and of rules
146144

apps/oxlint/src/js_plugins/external_linter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ pub fn create_external_linter(
3636
///
3737
/// The returned function will panic if called outside of a Tokio runtime.
3838
fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb {
39-
Box::new(move |plugin_path, package_name| {
39+
Box::new(move |plugin_url, package_name| {
4040
let cb = &cb;
4141
tokio::task::block_in_place(|| {
4242
tokio::runtime::Handle::current().block_on(async move {
4343
let result = cb
44-
.call_async(FnArgs::from((plugin_path, package_name)))
44+
.call_async(FnArgs::from((plugin_url, package_name)))
4545
.await?
4646
.into_future()
4747
.await?;

crates/oxc_linter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ serde = { workspace = true, features = ["derive"] }
6969
serde_json = { workspace = true, features = ["preserve_order"] } # preserve_order: print config with ordered keys.
7070
simdutf8 = { workspace = true }
7171
smallvec = { workspace = true }
72+
url = { workspace = true }
7273

7374
[dev-dependencies]
7475
insta = { workspace = true }

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
use itertools::Itertools;
77
use oxc_resolver::{ResolveOptions, Resolver};
88
use rustc_hash::{FxHashMap, FxHashSet};
9+
use url::Url;
910

1011
use oxc_span::{CompactStr, format_compact_str};
1112

@@ -530,25 +531,25 @@ impl ConfigStoreBuilder {
530531
error: e.to_string(),
531532
}
532533
})?;
533-
// TODO: We should support paths which are not valid UTF-8. How?
534-
let plugin_path = resolved.full_path().to_str().unwrap().to_string();
534+
let plugin_path = resolved.full_path();
535535

536536
if external_plugin_store.is_plugin_registered(&plugin_path) {
537537
return Ok(());
538538
}
539539

540-
// Extract package name from package.json if available
540+
// Extract package name from `package.json` if available
541541
let package_name = resolved.package_json().and_then(|pkg| pkg.name().map(String::from));
542542

543-
let result = {
544-
let plugin_path = plugin_path.clone();
545-
(external_linter.load_plugin)(plugin_path, package_name).map_err(|e| {
546-
ConfigBuilderError::PluginLoadFailed {
547-
plugin_specifier: plugin_specifier.to_string(),
548-
error: e.to_string(),
549-
}
550-
})
551-
}?;
543+
// Convert path to a `file://...` URL, as required by `import(...)` on JS side.
544+
// Note: `unwrap()` here is infallible as `plugin_path` is an absolute path.
545+
let plugin_url = Url::from_file_path(&plugin_path).unwrap().as_str().to_string();
546+
547+
let result = (external_linter.load_plugin)(plugin_url, package_name).map_err(|e| {
548+
ConfigBuilderError::PluginLoadFailed {
549+
plugin_specifier: plugin_specifier.to_string(),
550+
error: e.to_string(),
551+
}
552+
})?;
552553

553554
match result {
554555
PluginLoadResult::Success { name, offset, rule_names } => {

crates/oxc_linter/src/external_plugin_store.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::fmt;
1+
use std::{
2+
fmt,
3+
path::{Path, PathBuf},
4+
};
25

36
use rustc_hash::{FxHashMap, FxHashSet};
47

@@ -14,7 +17,7 @@ define_index_type! {
1417

1518
#[derive(Debug)]
1619
pub struct ExternalPluginStore {
17-
registered_plugin_paths: FxHashSet<String>,
20+
registered_plugin_paths: FxHashSet<PathBuf>,
1821

1922
plugins: IndexVec<ExternalPluginId, ExternalPlugin>,
2023
plugin_names: FxHashMap<String, ExternalPluginId>,
@@ -51,7 +54,7 @@ impl ExternalPluginStore {
5154
self.plugins.is_empty()
5255
}
5356

54-
pub fn is_plugin_registered(&self, plugin_path: &str) -> bool {
57+
pub fn is_plugin_registered(&self, plugin_path: &Path) -> bool {
5558
self.registered_plugin_paths.contains(plugin_path)
5659
}
5760

@@ -63,7 +66,7 @@ impl ExternalPluginStore {
6366
/// - `offset` does not equal the number of registered rules.
6467
pub fn register_plugin(
6568
&mut self,
66-
plugin_path: String,
69+
plugin_path: PathBuf,
6770
plugin_name: String,
6871
offset: usize,
6972
rule_names: Vec<String>,

crates/oxc_linter/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ impl Linter {
462462
};
463463

464464
let result = (external_linter.lint_file)(
465-
path.to_str().unwrap().to_string(),
465+
path.to_string_lossy().into_owned(),
466466
external_rules.iter().map(|(rule_id, _)| rule_id.raw()).collect(),
467467
settings_json,
468468
allocator,

0 commit comments

Comments
 (0)