Skip to content

Commit 4813d86

Browse files
committed
draft(oxlnt/lsp): support jsPlugins
1 parent e89c5ba commit 4813d86

File tree

13 files changed

+141
-93
lines changed

13 files changed

+141
-93
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"oxc.path.server": "./apps/oxlint/dist/cli.js"
3+
}

apps/oxlint/src/js_plugins/external_linter.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::{atomic::Ordering, mpsc::channel};
1+
use std::sync::{Arc, atomic::Ordering, mpsc::channel};
22

33
use napi::{
44
Status,
@@ -36,7 +36,7 @@ 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+
Arc::new(Box::new(move |plugin_path, package_name| {
4040
let cb = &cb;
4141
tokio::task::block_in_place(|| {
4242
tokio::runtime::Handle::current().block_on(async move {
@@ -49,7 +49,7 @@ fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb {
4949
Ok(plugin_load_result)
5050
})
5151
})
52-
})
52+
}))
5353
}
5454

5555
/// Result returned by `lintFile` JS callback.
@@ -69,7 +69,7 @@ pub enum LintFileReturnValue {
6969
/// Use an `mpsc::channel` to wait for the result from JS side, and block current thread until `lintFile`
7070
/// completes execution.
7171
fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb {
72-
Box::new(
72+
Arc::new(Box::new(
7373
move |file_path: String,
7474
rule_ids: Vec<u32>,
7575
settings_json: String,
@@ -115,7 +115,7 @@ fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb {
115115
Err(err) => panic!("Callback did not respond: {err}"),
116116
}
117117
},
118-
)
118+
))
119119
}
120120

121121
/// Get buffer ID of the `Allocator` and, if it hasn't already been sent to JS,

apps/oxlint/src/lsp.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/// Run the language server
2-
pub async fn run_lsp() {
3-
oxc_language_server::run_server(vec![Box::new(oxc_language_server::ServerLinterBuilder)]).await;
2+
pub async fn run_lsp(external_linter: Option<oxc_linter::ExternalLinter>) {
3+
oxc_language_server::run_server(vec![Box::new(oxc_language_server::ServerLinterBuilder::new(
4+
external_linter,
5+
))])
6+
.await;
47
}

apps/oxlint/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ async fn main() -> CliRunResult {
99

1010
// If --lsp flag is set, run the language server
1111
if command.lsp {
12-
run_lsp().await;
12+
run_lsp(None).await;
1313
return CliRunResult::LintSucceeded;
1414
}
1515

apps/oxlint/src/run.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,18 @@ async fn lint_impl(
9191
}
9292
};
9393

94+
// JS plugins are only supported on 64-bit little-endian platforms at present
95+
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
96+
let external_linter = Some(super::js_plugins::create_external_linter(load_plugin, lint_file));
97+
#[cfg(not(all(target_pointer_width = "64", target_endian = "little")))]
98+
let external_linter = {
99+
let (_, _) = (load_plugin, lint_file);
100+
None
101+
};
102+
94103
// If --lsp flag is set, run the language server
95104
if command.lsp {
96-
crate::lsp::run_lsp().await;
105+
crate::lsp::run_lsp(external_linter).await;
97106
return CliRunResult::LintSucceeded;
98107
}
99108

@@ -102,15 +111,6 @@ async fn lint_impl(
102111

103112
command.handle_threads();
104113

105-
// JS plugins are only supported on 64-bit little-endian platforms at present
106-
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
107-
let external_linter = Some(super::js_plugins::create_external_linter(load_plugin, lint_file));
108-
#[cfg(not(all(target_pointer_width = "64", target_endian = "little")))]
109-
let external_linter = {
110-
let (_, _) = (load_plugin, lint_file);
111-
None
112-
};
113-
114114
// stdio is blocked by LineWriter, use a BufWriter to reduce syscalls.
115115
// See `https://github.com/rust-lang/rust/issues/60673`.
116116
let mut stdout = BufWriter::new(std::io::stdout());

crates/oxc_language_server/src/linter/isolated_lint_handler.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ use tower_lsp_server::{UriExt, lsp_types::Uri};
1010

1111
use oxc_allocator::Allocator;
1212
use oxc_linter::{
13-
AllowWarnDeny, ConfigStore, DisableDirectives, Fix, FixKind, LINTABLE_EXTENSIONS, LintOptions,
14-
LintRunner, LintRunnerBuilder, LintServiceOptions, Linter, Message, PossibleFixes,
15-
RuleCommentType, RuntimeFileSystem, read_to_arena_str, read_to_string,
13+
AllowWarnDeny, ConfigStore, DisableDirectives, ExternalLinter, Fix, FixKind,
14+
LINTABLE_EXTENSIONS, LintOptions, LintRunner, LintRunnerBuilder, LintServiceOptions, Linter,
15+
Message, PossibleFixes, RuleCommentType, RuntimeFileSystem, read_to_arena_str, read_to_string,
1616
};
1717

1818
use super::error_with_position::{
@@ -67,11 +67,12 @@ impl IsolatedLintHandler {
6767
pub fn new(
6868
lint_options: LintOptions,
6969
config_store: ConfigStore,
70+
external_linter: Option<ExternalLinter>,
7071
options: &IsolatedLintHandlerOptions,
7172
) -> Self {
7273
let config_store_clone = config_store.clone();
7374

74-
let linter = Linter::new(lint_options, config_store, None);
75+
let linter = Linter::new(lint_options, config_store, external_linter);
7576
let mut lint_service_options = LintServiceOptions::new(options.root_path.clone())
7677
.with_cross_module(options.use_cross_module);
7778

@@ -121,6 +122,7 @@ impl IsolatedLintHandler {
121122
debug!("lint {}", path.display());
122123
let rope = &Rope::from_str(source_text);
123124

125+
// ToDO: with external linter, we need a new FS (raw) system
124126
let fs = IsolatedLintHandlerFileSystem::new(path.to_path_buf(), Arc::from(source_text));
125127

126128
let mut messages: Vec<DiagnosticReport> = self

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use tower_lsp_server::{
1414
};
1515

1616
use oxc_linter::{
17-
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalPluginStore, FixKind,
18-
LintIgnoreMatcher, LintOptions, Oxlintrc,
17+
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore,
18+
FixKind, LintIgnoreMatcher, LintOptions, Oxlintrc,
1919
};
2020

2121
use crate::{
@@ -36,12 +36,17 @@ use crate::{
3636
utils::normalize_path,
3737
};
3838

39-
pub struct ServerLinterBuilder;
39+
pub struct ServerLinterBuilder {
40+
external_linter: Option<ExternalLinter>,
41+
}
4042

4143
impl ServerLinterBuilder {
44+
pub const fn new(external_linter: Option<ExternalLinter>) -> Self {
45+
Self { external_linter }
46+
}
4247
/// # Panics
4348
/// Panics if the root URI cannot be converted to a file path.
44-
pub fn build(root_uri: &Uri, options: serde_json::Value) -> ServerLinter {
49+
pub fn build(&self, root_uri: &Uri, options: serde_json::Value) -> ServerLinter {
4550
let options = match serde_json::from_value::<LSPLintOptions>(options) {
4651
Ok(opts) => opts,
4752
Err(e) => {
@@ -52,9 +57,18 @@ impl ServerLinterBuilder {
5257
}
5358
};
5459
let root_path = root_uri.to_file_path().unwrap();
60+
let mut external_plugin_store = ExternalPluginStore::default();
61+
62+
let mut external_linter = self.external_linter.as_ref();
5563
let mut nested_ignore_patterns = Vec::new();
56-
let (nested_configs, mut extended_paths) =
57-
Self::create_nested_configs(&root_path, &options, &mut nested_ignore_patterns);
64+
let (nested_configs, mut extended_paths) = Self::create_nested_configs(
65+
&root_path,
66+
&options,
67+
external_linter,
68+
&mut external_plugin_store,
69+
&mut nested_ignore_patterns,
70+
);
71+
5872
let config_path = options.config_path.as_ref().map_or(LINT_CONFIG_FILE, |v| v);
5973
let config = normalize_path(root_path.join(config_path));
6074
let oxlintrc = if config.try_exists().is_ok_and(|exists| exists) {
@@ -73,11 +87,13 @@ impl ServerLinterBuilder {
7387
};
7488

7589
let base_patterns = oxlintrc.ignore_patterns.clone();
76-
77-
let mut external_plugin_store = ExternalPluginStore::new(false);
78-
let config_builder =
79-
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store)
80-
.unwrap_or_default();
90+
let config_builder = ConfigStoreBuilder::from_oxlintrc(
91+
false,
92+
oxlintrc,
93+
external_linter,
94+
&mut external_plugin_store,
95+
)
96+
.unwrap_or_default();
8197

8298
// TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality.
8399
let use_nested_config = options.use_nested_configs();
@@ -90,9 +106,14 @@ impl ServerLinterBuilder {
90106
extended_paths.extend(config_builder.extended_paths.clone());
91107
let base_config = config_builder.build(&external_plugin_store).unwrap_or_else(|err| {
92108
warn!("Failed to build config: {err}");
93-
ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap()
109+
ConfigStoreBuilder::empty().build(&ExternalPluginStore::new(false)).unwrap()
94110
});
95111

112+
// If no external rules, discard `ExternalLinter`
113+
if external_plugin_store.is_empty() {
114+
external_linter = None;
115+
}
116+
96117
let lint_options = LintOptions {
97118
fix: fix_kind,
98119
report_unused_directive: match options.unused_disable_directives {
@@ -119,6 +140,7 @@ impl ServerLinterBuilder {
119140
let isolated_linter = IsolatedLintHandler::new(
120141
lint_options,
121142
config_store,
143+
external_linter.cloned(),
122144
&IsolatedLintHandlerOptions {
123145
use_cross_module,
124146
type_aware: options.type_aware,
@@ -150,7 +172,7 @@ impl ToolBuilder for ServerLinterBuilder {
150172
vec![FIX_ALL_COMMAND_ID.to_string()]
151173
}
152174
fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box<dyn Tool> {
153-
Box::new(ServerLinterBuilder::build(root_uri, options))
175+
Box::new(self.build(root_uri, options))
154176
}
155177
}
156178

@@ -160,6 +182,8 @@ impl ServerLinterBuilder {
160182
fn create_nested_configs(
161183
root_path: &Path,
162184
options: &LSPLintOptions,
185+
external_linter: Option<&ExternalLinter>,
186+
external_plugin_store: &mut ExternalPluginStore,
163187
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
164188
) -> (ConcurrentHashMap<PathBuf, Config>, FxHashSet<PathBuf>) {
165189
let mut extended_paths = FxHashSet::default();
@@ -184,20 +208,29 @@ impl ServerLinterBuilder {
184208
};
185209
// Collect ignore patterns and their root
186210
nested_ignore_patterns.push((oxlintrc.ignore_patterns.clone(), dir_path.to_path_buf()));
187-
let mut external_plugin_store = ExternalPluginStore::new(false);
188-
let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc(
211+
212+
let Ok(config_store_builder) = (match ConfigStoreBuilder::from_oxlintrc(
189213
false,
190214
oxlintrc,
191-
None,
192-
&mut external_plugin_store,
193-
) else {
194-
warn!("Skipping config (builder failed): {}", file_path.display());
215+
external_linter,
216+
external_plugin_store,
217+
) {
218+
Ok(builder) => Ok(builder),
219+
Err(err) => {
220+
warn!(
221+
"Failed to create ConfigStoreBuilder for {}: {:?}",
222+
dir_path.display(),
223+
err
224+
);
225+
Err(err)
226+
}
227+
}) else {
195228
continue;
196229
};
197230
extended_paths.extend(config_store_builder.extended_paths.clone());
198-
let config = config_store_builder.build(&external_plugin_store).unwrap_or_else(|err| {
231+
let config = config_store_builder.build(external_plugin_store).unwrap_or_else(|err| {
199232
warn!("Failed to build nested config for {}: {:?}", dir_path.display(), err);
200-
ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap()
233+
ConfigStoreBuilder::empty().build(&ExternalPluginStore::new(false)).unwrap()
201234
});
202235
nested_configs.pin().insert(dir_path.to_path_buf(), config);
203236
}
@@ -299,7 +332,8 @@ impl Tool for ServerLinter {
299332

300333
// get the cached files before refreshing the linter, and revalidate them after
301334
let cached_files = self.get_cached_files_of_diagnostics();
302-
let new_linter = ServerLinterBuilder::build(root_uri, new_options_json.clone());
335+
// TODO: external linter should also be passed here
336+
let new_linter = ServerLinterBuilder::new(None).build(root_uri, new_options_json.clone());
303337
let diagnostics = Some(new_linter.revalidate_diagnostics(cached_files));
304338

305339
let patterns = {
@@ -353,7 +387,8 @@ impl Tool for ServerLinter {
353387
options: serde_json::Value,
354388
) -> ToolRestartChanges {
355389
// TODO: Check if the changed file is actually a config file (including extended paths)
356-
let new_linter = ServerLinterBuilder::build(root_uri, options);
390+
// TODO: external linter should also be passed here
391+
let new_linter = ServerLinterBuilder::new(None).build(root_uri, options);
357392

358393
// get the cached files before refreshing the linter, and revalidate them after
359394
let cached_files = self.get_cached_files_of_diagnostics();
@@ -611,6 +646,7 @@ fn range_overlaps(a: Range, b: Range) -> bool {
611646
mod test {
612647
use std::path::{Path, PathBuf};
613648

649+
use oxc_linter::ExternalPluginStore;
614650
use serde_json::json;
615651

616652
use crate::linter::{
@@ -622,9 +658,12 @@ mod test {
622658
#[test]
623659
fn test_create_nested_configs_with_disabled_nested_configs() {
624660
let mut nested_ignore_patterns = Vec::new();
661+
let mut external_plugin_store = ExternalPluginStore::default();
625662
let (configs, _) = ServerLinterBuilder::create_nested_configs(
626663
Path::new("/root/"),
627664
&LintOptions { disable_nested_config: true, ..LintOptions::default() },
665+
None,
666+
&mut external_plugin_store,
628667
&mut nested_ignore_patterns,
629668
);
630669

@@ -634,9 +673,13 @@ mod test {
634673
#[test]
635674
fn test_create_nested_configs() {
636675
let mut nested_ignore_patterns = Vec::new();
676+
let mut external_plugin_store = ExternalPluginStore::default();
677+
637678
let (configs, _) = ServerLinterBuilder::create_nested_configs(
638679
&get_file_path("fixtures/linter/init_nested_configs"),
639680
&LintOptions::default(),
681+
None,
682+
&mut external_plugin_store,
640683
&mut nested_ignore_patterns,
641684
);
642685
let configs = configs.pin();

crates/oxc_language_server/src/linter/tester.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ impl Tester<'_> {
171171
.join(self.relative_root_dir);
172172
let uri = Uri::from_file_path(absolute_path).expect("could not convert current dir to uri");
173173

174-
ServerLinterBuilder::build(&uri, self.options.clone())
174+
ServerLinterBuilder::new(None).build(&uri, self.options.clone())
175175
}
176176

177177
/// Given a relative file path (relative to `oxc_language_server` crate root), run the linter

crates/oxc_language_server/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ async fn main() {
66
#[cfg(feature = "formatter")]
77
v.push(Box::new(oxc_language_server::ServerFormatterBuilder));
88
#[cfg(feature = "linter")]
9-
v.push(Box::new(oxc_language_server::ServerLinterBuilder));
9+
v.push(Box::new(oxc_language_server::ServerLinterBuilder::new(None)));
1010
v
1111
};
1212

crates/oxc_language_server/src/worker.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ mod test_watchers {
441441
) -> WorkspaceWorker {
442442
let worker = WorkspaceWorker::new(absolute_path);
443443
let tools: Vec<Box<dyn ToolBuilder>> =
444-
vec![Box::new(ServerLinterBuilder), Box::new(ServerFormatterBuilder)];
444+
vec![Box::new(ServerLinterBuilder::new(None)), Box::new(ServerFormatterBuilder)];
445445

446446
worker.start_worker(options, &tools).await;
447447

0 commit comments

Comments
 (0)