Skip to content

Commit 85415a9

Browse files
committed
draft(oxlnt/lsp): support jsPlugins
1 parent 0146b4c commit 85415a9

File tree

15 files changed

+165
-57
lines changed

15 files changed

+165
-57
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: 70 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::new(self.external_linter.is_some());
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,
@@ -135,6 +157,7 @@ impl ServerLinterBuilder {
135157
options.run,
136158
root_path.to_path_buf(),
137159
isolated_linter,
160+
external_linter.cloned(),
138161
LintIgnoreMatcher::new(&base_patterns, &root_path, nested_ignore_patterns),
139162
Self::create_ignore_glob(&root_path),
140163
extended_paths,
@@ -150,7 +173,7 @@ impl ToolBuilder for ServerLinterBuilder {
150173
vec![FIX_ALL_COMMAND_ID.to_string()]
151174
}
152175
fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box<dyn Tool> {
153-
Box::new(ServerLinterBuilder::build(root_uri, options))
176+
Box::new(self.build(root_uri, options))
154177
}
155178
}
156179

@@ -160,6 +183,8 @@ impl ServerLinterBuilder {
160183
fn create_nested_configs(
161184
root_path: &Path,
162185
options: &LSPLintOptions,
186+
external_linter: Option<&ExternalLinter>,
187+
external_plugin_store: &mut ExternalPluginStore,
163188
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
164189
) -> (ConcurrentHashMap<PathBuf, Config>, FxHashSet<PathBuf>) {
165190
let mut extended_paths = FxHashSet::default();
@@ -184,20 +209,29 @@ impl ServerLinterBuilder {
184209
};
185210
// Collect ignore patterns and their root
186211
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(
212+
213+
let Ok(config_store_builder) = (match ConfigStoreBuilder::from_oxlintrc(
189214
false,
190215
oxlintrc,
191-
None,
192-
&mut external_plugin_store,
193-
) else {
194-
warn!("Skipping config (builder failed): {}", file_path.display());
216+
external_linter,
217+
external_plugin_store,
218+
) {
219+
Ok(builder) => Ok(builder),
220+
Err(err) => {
221+
warn!(
222+
"Failed to create ConfigStoreBuilder for {}: {:?}",
223+
dir_path.display(),
224+
err
225+
);
226+
Err(err)
227+
}
228+
}) else {
195229
continue;
196230
};
197231
extended_paths.extend(config_store_builder.extended_paths.clone());
198-
let config = config_store_builder.build(&external_plugin_store).unwrap_or_else(|err| {
232+
let config = config_store_builder.build(external_plugin_store).unwrap_or_else(|err| {
199233
warn!("Failed to build nested config for {}: {:?}", dir_path.display(), err);
200-
ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap()
234+
ConfigStoreBuilder::empty().build(&ExternalPluginStore::new(false)).unwrap()
201235
});
202236
nested_configs.pin().insert(dir_path.to_path_buf(), config);
203237
}
@@ -244,6 +278,7 @@ pub struct ServerLinter {
244278
run: Run,
245279
cwd: PathBuf,
246280
isolated_linter: IsolatedLintHandler,
281+
external_linter: Option<ExternalLinter>,
247282
ignore_matcher: LintIgnoreMatcher,
248283
gitignore_glob: Vec<Gitignore>,
249284
extended_paths: FxHashSet<PathBuf>,
@@ -299,7 +334,8 @@ impl Tool for ServerLinter {
299334

300335
// get the cached files before refreshing the linter, and revalidate them after
301336
let cached_files = self.get_cached_files_of_diagnostics();
302-
let new_linter = ServerLinterBuilder::build(root_uri, new_options_json.clone());
337+
let new_linter = ServerLinterBuilder::new(self.external_linter.clone())
338+
.build(root_uri, new_options_json.clone());
303339
let diagnostics = Some(new_linter.revalidate_diagnostics(cached_files));
304340

305341
let patterns = {
@@ -353,7 +389,8 @@ impl Tool for ServerLinter {
353389
options: serde_json::Value,
354390
) -> ToolRestartChanges {
355391
// TODO: Check if the changed file is actually a config file (including extended paths)
356-
let new_linter = ServerLinterBuilder::build(root_uri, options);
392+
let new_linter =
393+
ServerLinterBuilder::new(self.external_linter.clone()).build(root_uri, options);
357394

358395
// get the cached files before refreshing the linter, and revalidate them after
359396
let cached_files = self.get_cached_files_of_diagnostics();
@@ -508,6 +545,7 @@ impl ServerLinter {
508545
run: Run,
509546
cwd: PathBuf,
510547
isolated_linter: IsolatedLintHandler,
548+
external_linter: Option<ExternalLinter>,
511549
ignore_matcher: LintIgnoreMatcher,
512550
gitignore_glob: Vec<Gitignore>,
513551
extended_paths: FxHashSet<PathBuf>,
@@ -516,6 +554,7 @@ impl ServerLinter {
516554
run,
517555
cwd,
518556
isolated_linter,
557+
external_linter,
519558
ignore_matcher,
520559
gitignore_glob,
521560
extended_paths,
@@ -611,6 +650,7 @@ fn range_overlaps(a: Range, b: Range) -> bool {
611650
mod test {
612651
use std::path::{Path, PathBuf};
613652

653+
use oxc_linter::ExternalPluginStore;
614654
use serde_json::json;
615655

616656
use crate::linter::{
@@ -622,9 +662,12 @@ mod test {
622662
#[test]
623663
fn test_create_nested_configs_with_disabled_nested_configs() {
624664
let mut nested_ignore_patterns = Vec::new();
665+
let mut external_plugin_store = ExternalPluginStore::default();
625666
let (configs, _) = ServerLinterBuilder::create_nested_configs(
626667
Path::new("/root/"),
627668
&LintOptions { disable_nested_config: true, ..LintOptions::default() },
669+
None,
670+
&mut external_plugin_store,
628671
&mut nested_ignore_patterns,
629672
);
630673

@@ -634,9 +677,13 @@ mod test {
634677
#[test]
635678
fn test_create_nested_configs() {
636679
let mut nested_ignore_patterns = Vec::new();
680+
let mut external_plugin_store = ExternalPluginStore::default();
681+
637682
let (configs, _) = ServerLinterBuilder::create_nested_configs(
638683
&get_file_path("fixtures/linter/init_nested_configs"),
639684
&LintOptions::default(),
685+
None,
686+
&mut external_plugin_store,
640687
&mut nested_ignore_patterns,
641688
);
642689
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

0 commit comments

Comments
 (0)