diff --git a/.vscode/settings.json b/.vscode/settings.json index 222f58a0a93b1..26acad080d6f4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { + "oxc.path.oxlint": "./apps/oxlint/dist/cli.js", "oxc.typeAware": true, - "oxc.configPath": "oxlintrc.json", + // "oxc.configPath": "oxlintrc.json", "oxc.unusedDisableDirectives": "deny", "oxc.fmt.experimental": true, "oxc.fmt.configPath": "oxfmtrc.jsonc", diff --git a/apps/oxlint/src/js_plugins/external_linter.rs b/apps/oxlint/src/js_plugins/external_linter.rs index 3cc948e5be052..4a9ee88568619 100644 --- a/apps/oxlint/src/js_plugins/external_linter.rs +++ b/apps/oxlint/src/js_plugins/external_linter.rs @@ -1,4 +1,4 @@ -use std::sync::{atomic::Ordering, mpsc::channel}; +use std::sync::{Arc, atomic::Ordering, mpsc::channel}; use napi::{ Status, @@ -45,7 +45,7 @@ pub fn create_external_linter( /// /// The returned function will panic if called outside of a Tokio runtime. fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb { - Box::new(move |workspace_dir, plugin_url, package_name| { + Arc::new(Box::new(move |workspace_dir, plugin_url, package_name| { let cb = &cb; tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async move { @@ -58,7 +58,7 @@ fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb { Ok(plugin_load_result) }) }) - }) + })) } /// Result returned by `lintFile` JS callback. @@ -75,7 +75,7 @@ pub enum LintFileReturnValue { /// /// The returned function will panic if called outside of a Tokio runtime. fn wrap_create_workspace(cb: JsCreateWorkspaceCb) -> oxc_linter::ExternalLinterCreateWorkspaceCb { - Box::new(move |workspace_dir| { + Arc::new(Box::new(move |workspace_dir| { let cb = &cb; tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async move { @@ -83,16 +83,16 @@ fn wrap_create_workspace(cb: JsCreateWorkspaceCb) -> oxc_linter::ExternalLinterC Ok(()) }) }) - }) + })) } /// Wrap `destroyWorkspace` JS callback as a normal Rust function. fn wrap_destroy_workspace( cb: JsDestroyWorkspaceCb, ) -> oxc_linter::ExternalLinterDestroyWorkspaceCb { - Box::new(move |root_dir: String| { + Arc::new(Box::new(move |root_dir: String| { let _ = cb.call(FnArgs::from((root_dir,)), ThreadsafeFunctionCallMode::Blocking); - }) + })) } /// Wrap `lintFile` JS callback as a normal Rust function. @@ -105,7 +105,7 @@ fn wrap_destroy_workspace( /// Use an `mpsc::channel` to wait for the result from JS side, and block current thread until `lintFile` /// completes execution. fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb { - Box::new( + Arc::new(Box::new( move |workspace_dir: String, file_path: String, rule_ids: Vec, @@ -159,7 +159,7 @@ fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb { Err(err) => panic!("Callback did not respond: {err}"), } }, - ) + )) } /// Get buffer ID of the `Allocator` and, if it hasn't already been sent to JS, diff --git a/apps/oxlint/src/lsp.rs b/apps/oxlint/src/lsp.rs index 0acb3db43aa75..c82dc011f0175 100644 --- a/apps/oxlint/src/lsp.rs +++ b/apps/oxlint/src/lsp.rs @@ -1,4 +1,7 @@ /// Run the language server -pub async fn run_lsp() { - oxc_language_server::run_server(vec![Box::new(oxc_language_server::ServerLinterBuilder)]).await; +pub async fn run_lsp(external_linter: Option) { + oxc_language_server::run_server(vec![Box::new(oxc_language_server::ServerLinterBuilder::new( + external_linter, + ))]) + .await; } diff --git a/apps/oxlint/src/main.rs b/apps/oxlint/src/main.rs index 36926e0da4930..cff18ad42bc63 100644 --- a/apps/oxlint/src/main.rs +++ b/apps/oxlint/src/main.rs @@ -9,7 +9,7 @@ async fn main() -> CliRunResult { // If --lsp flag is set, run the language server if command.lsp { - run_lsp().await; + run_lsp(None).await; return CliRunResult::LintSucceeded; } diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index 47881d406f59c..f8bfa590899a0 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -133,17 +133,6 @@ async fn lint_impl( } }; - // If --lsp flag is set, run the language server - if command.lsp { - crate::lsp::run_lsp().await; - return CliRunResult::LintSucceeded; - } - - init_tracing(); - init_miette(); - - command.handle_threads(); - // JS plugins are only supported on 64-bit little-endian platforms at present #[cfg(all(target_pointer_width = "64", target_endian = "little"))] let external_linter = Some(super::js_plugins::create_external_linter( @@ -158,6 +147,17 @@ async fn lint_impl( None }; + // If --lsp flag is set, run the language server + if command.lsp { + crate::lsp::run_lsp(external_linter).await; + return CliRunResult::LintSucceeded; + } + + init_tracing(); + init_miette(); + + command.handle_threads(); + // stdio is blocked by LineWriter, use a BufWriter to reduce syscalls. // See `https://github.com/rust-lang/rust/issues/60673`. let mut stdout = BufWriter::new(std::io::stdout()); diff --git a/crates/oxc_language_server/src/linter/isolated_lint_handler.rs b/crates/oxc_language_server/src/linter/isolated_lint_handler.rs index 6f7b4a884a17b..ee7b740c06b19 100644 --- a/crates/oxc_language_server/src/linter/isolated_lint_handler.rs +++ b/crates/oxc_language_server/src/linter/isolated_lint_handler.rs @@ -10,9 +10,9 @@ use tower_lsp_server::{UriExt, lsp_types::Uri}; use oxc_allocator::Allocator; use oxc_linter::{ - AllowWarnDeny, ConfigStore, DisableDirectives, Fix, FixKind, LINTABLE_EXTENSIONS, LintOptions, - LintRunner, LintRunnerBuilder, LintServiceOptions, Linter, Message, PossibleFixes, - RuleCommentType, RuntimeFileSystem, read_to_arena_str, read_to_string, + AllowWarnDeny, ConfigStore, DisableDirectives, ExternalLinter, Fix, FixKind, + LINTABLE_EXTENSIONS, LintOptions, LintRunner, LintRunnerBuilder, LintServiceOptions, Linter, + Message, PossibleFixes, RuleCommentType, RuntimeFileSystem, read_to_arena_str, read_to_string, }; use super::error_with_position::{ @@ -68,12 +68,13 @@ impl IsolatedLintHandler { cwd: &Path, lint_options: LintOptions, config_store: ConfigStore, + external_linter: Option, options: &IsolatedLintHandlerOptions, ) -> Self { let config_store_clone = config_store.clone(); let cwd = cwd.to_string_lossy().to_string(); - let linter = Linter::new(cwd.clone(), lint_options, config_store, None); + let linter = Linter::new(cwd.clone(), lint_options, config_store, external_linter); let mut lint_service_options = LintServiceOptions::new(options.root_path.clone()) .with_cross_module(options.use_cross_module); @@ -123,6 +124,7 @@ impl IsolatedLintHandler { debug!("lint {}", path.display()); let rope = &Rope::from_str(source_text); + // ToDO: with external linter, we need a new FS (raw) system let fs = IsolatedLintHandlerFileSystem::new(path.to_path_buf(), Arc::from(source_text)); let mut messages: Vec = self diff --git a/crates/oxc_language_server/src/linter/server_linter.rs b/crates/oxc_language_server/src/linter/server_linter.rs index c32a0f375b710..147ed8fb691d1 100644 --- a/crates/oxc_language_server/src/linter/server_linter.rs +++ b/crates/oxc_language_server/src/linter/server_linter.rs @@ -16,8 +16,8 @@ use tower_lsp_server::{ }; use oxc_linter::{ - AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalPluginStore, FixKind, - LintIgnoreMatcher, LintOptions, Oxlintrc, + AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore, + FixKind, LintIgnoreMatcher, LintOptions, Oxlintrc, }; use crate::{ @@ -38,12 +38,17 @@ use crate::{ utils::normalize_path, }; -pub struct ServerLinterBuilder; +pub struct ServerLinterBuilder { + external_linter: Option, +} impl ServerLinterBuilder { + pub const fn new(external_linter: Option) -> Self { + Self { external_linter } + } /// # Panics /// Panics if the root URI cannot be converted to a file path. - pub fn build(root_uri: &Uri, options: serde_json::Value) -> ServerLinter { + pub fn build(&self, root_uri: &Uri, options: serde_json::Value) -> ServerLinter { let options = match serde_json::from_value::(options) { Ok(opts) => opts, Err(e) => { @@ -54,9 +59,23 @@ impl ServerLinterBuilder { } }; let root_path = root_uri.to_file_path().unwrap(); + let mut external_plugin_store = ExternalPluginStore::new(self.external_linter.is_some()); + + let mut external_linter = self.external_linter.clone(); + + if let Some(extern_linter) = &external_linter { + let _ = (extern_linter.create_workspace)(root_path.to_string_lossy().to_string()); + } + let mut nested_ignore_patterns = Vec::new(); - let (nested_configs, mut extended_paths) = - Self::create_nested_configs(&root_path, &options, &mut nested_ignore_patterns); + let (nested_configs, mut extended_paths) = Self::create_nested_configs( + &root_path, + &options, + external_linter.as_ref(), + &mut external_plugin_store, + &mut nested_ignore_patterns, + ); + let config_path = options.config_path.as_ref().map_or(LINT_CONFIG_FILE, |v| v); let config = normalize_path(root_path.join(config_path)); let oxlintrc = if config.try_exists().is_ok_and(|exists| exists) { @@ -76,12 +95,11 @@ impl ServerLinterBuilder { let base_patterns = oxlintrc.ignore_patterns.clone(); - let mut external_plugin_store = ExternalPluginStore::new(false); let config_builder = ConfigStoreBuilder::from_oxlintrc( false, oxlintrc, &root_path, - None, + external_linter.as_ref(), &mut external_plugin_store, ) .unwrap_or_default(); @@ -97,9 +115,14 @@ impl ServerLinterBuilder { extended_paths.extend(config_builder.extended_paths.clone()); let base_config = config_builder.build(&external_plugin_store).unwrap_or_else(|err| { warn!("Failed to build config: {err}"); - ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap() + ConfigStoreBuilder::empty().build(&ExternalPluginStore::new(false)).unwrap() }); + // If no external rules, discard `ExternalLinter` + if external_plugin_store.is_empty() { + external_linter = None; + } + let lint_options = LintOptions { fix: fix_kind, report_unused_directive: match options.unused_disable_directives { @@ -127,6 +150,7 @@ impl ServerLinterBuilder { &root_path, lint_options, config_store, + external_linter.clone(), &IsolatedLintHandlerOptions { use_cross_module, type_aware: options.type_aware, @@ -143,6 +167,7 @@ impl ServerLinterBuilder { options.run, root_path.to_path_buf(), isolated_linter, + external_linter, LintIgnoreMatcher::new(&base_patterns, &root_path, nested_ignore_patterns), Self::create_ignore_glob(&root_path), extended_paths, @@ -210,7 +235,7 @@ impl ToolBuilder for ServerLinterBuilder { }); } fn build_boxed(&self, root_uri: &Uri, options: serde_json::Value) -> Box { - Box::new(ServerLinterBuilder::build(root_uri, options)) + Box::new(self.build(root_uri, options)) } } @@ -220,6 +245,8 @@ impl ServerLinterBuilder { fn create_nested_configs( root_path: &Path, options: &LSPLintOptions, + external_linter: Option<&ExternalLinter>, + external_plugin_store: &mut ExternalPluginStore, nested_ignore_patterns: &mut Vec<(Vec, PathBuf)>, ) -> (ConcurrentHashMap, FxHashSet) { let mut extended_paths = FxHashSet::default(); @@ -244,21 +271,30 @@ impl ServerLinterBuilder { }; // Collect ignore patterns and their root nested_ignore_patterns.push((oxlintrc.ignore_patterns.clone(), dir_path.to_path_buf())); - let mut external_plugin_store = ExternalPluginStore::new(false); - let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc( + + let Ok(config_store_builder) = (match ConfigStoreBuilder::from_oxlintrc( false, oxlintrc, root_path, - None, - &mut external_plugin_store, - ) else { - warn!("Skipping config (builder failed): {}", file_path.display()); + external_linter, + external_plugin_store, + ) { + Ok(builder) => Ok(builder), + Err(err) => { + warn!( + "Failed to create ConfigStoreBuilder for {}: {:?}", + dir_path.display(), + err + ); + Err(err) + } + }) else { continue; }; extended_paths.extend(config_store_builder.extended_paths.clone()); - let config = config_store_builder.build(&external_plugin_store).unwrap_or_else(|err| { + let config = config_store_builder.build(external_plugin_store).unwrap_or_else(|err| { warn!("Failed to build nested config for {}: {:?}", dir_path.display(), err); - ConfigStoreBuilder::empty().build(&external_plugin_store).unwrap() + ConfigStoreBuilder::empty().build(&ExternalPluginStore::new(false)).unwrap() }); nested_configs.pin().insert(dir_path.to_path_buf(), config); } @@ -305,6 +341,7 @@ pub struct ServerLinter { run: Run, cwd: PathBuf, isolated_linter: IsolatedLintHandler, + external_linter: Option, ignore_matcher: LintIgnoreMatcher, gitignore_glob: Vec, extended_paths: FxHashSet, @@ -317,6 +354,9 @@ impl Tool for ServerLinter { } fn shutdown(&self) -> ToolShutdownChanges { + if let Some(extern_linter) = &self.external_linter { + (extern_linter.destroy_workspace)(self.cwd.to_string_lossy().to_string()); + } ToolShutdownChanges { uris_to_clear_diagnostics: Some(self.get_cached_files_of_diagnostics()), } @@ -360,7 +400,12 @@ impl Tool for ServerLinter { // get the cached files before refreshing the linter, and revalidate them after let cached_files = self.get_cached_files_of_diagnostics(); - let new_linter = ServerLinterBuilder::build(root_uri, new_options_json.clone()); + // destroy the js linter workspace + if let Some(extern_linter) = &self.external_linter { + (extern_linter.destroy_workspace)(self.cwd.to_string_lossy().to_string()); + } + let new_linter = ServerLinterBuilder::new(self.external_linter.clone()) + .build(root_uri, new_options_json.clone()); let diagnostics = Some(new_linter.revalidate_diagnostics(cached_files)); let patterns = { @@ -414,7 +459,14 @@ impl Tool for ServerLinter { options: serde_json::Value, ) -> ToolRestartChanges { // TODO: Check if the changed file is actually a config file (including extended paths) - let new_linter = ServerLinterBuilder::build(root_uri, options); + + // destroy the js linter workspace + if let Some(extern_linter) = &self.external_linter { + (extern_linter.destroy_workspace)(self.cwd.to_string_lossy().to_string()); + } + + let new_linter = + ServerLinterBuilder::new(self.external_linter.clone()).build(root_uri, options); // get the cached files before refreshing the linter, and revalidate them after let cached_files = self.get_cached_files_of_diagnostics(); @@ -569,6 +621,7 @@ impl ServerLinter { run: Run, cwd: PathBuf, isolated_linter: IsolatedLintHandler, + external_linter: Option, ignore_matcher: LintIgnoreMatcher, gitignore_glob: Vec, extended_paths: FxHashSet, @@ -577,6 +630,7 @@ impl ServerLinter { run, cwd, isolated_linter, + external_linter, ignore_matcher, gitignore_glob, extended_paths, @@ -682,7 +736,7 @@ mod tests_builder { #[test] fn test_server_capabilities_empty_capabilities() { - let builder = ServerLinterBuilder; + let builder = ServerLinterBuilder::new(None); let mut capabilities = ServerCapabilities::default(); builder.server_capabilities(&mut capabilities); @@ -706,7 +760,7 @@ mod tests_builder { #[test] fn test_server_capabilities_with_existing_code_action_kinds() { - let builder = ServerLinterBuilder; + let builder = ServerLinterBuilder::new(None); let mut capabilities = ServerCapabilities { code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(vec![CodeActionKind::REFACTOR]), @@ -733,7 +787,7 @@ mod tests_builder { #[test] fn test_server_capabilities_with_existing_quickfix_kind() { - let builder = ServerLinterBuilder; + let builder = ServerLinterBuilder::new(None); let mut capabilities = ServerCapabilities { code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]), @@ -758,7 +812,7 @@ mod tests_builder { #[test] fn test_server_capabilities_with_simple_code_action_provider() { - let builder = ServerLinterBuilder; + let builder = ServerLinterBuilder::new(None); let mut capabilities = ServerCapabilities { code_action_provider: Some(CodeActionProviderCapability::Simple(true)), ..Default::default() @@ -780,7 +834,7 @@ mod tests_builder { #[test] fn test_server_capabilities_with_existing_commands() { - let builder = ServerLinterBuilder; + let builder = ServerLinterBuilder::new(None); let mut capabilities = ServerCapabilities { execute_command_provider: Some(ExecuteCommandOptions { commands: vec!["existing.command".to_string()], @@ -805,7 +859,7 @@ mod tests_builder { #[test] fn test_server_capabilities_with_existing_fix_all_command() { - let builder = ServerLinterBuilder; + let builder = ServerLinterBuilder::new(None); let mut capabilities = ServerCapabilities { execute_command_provider: Some(ExecuteCommandOptions { commands: vec![FIX_ALL_COMMAND_ID.to_string()], @@ -922,6 +976,7 @@ mod test_watchers { mod test { use std::path::{Path, PathBuf}; + use oxc_linter::ExternalPluginStore; use serde_json::json; use crate::linter::{ @@ -933,9 +988,12 @@ mod test { #[test] fn test_create_nested_configs_with_disabled_nested_configs() { let mut nested_ignore_patterns = Vec::new(); + let mut external_plugin_store = ExternalPluginStore::default(); let (configs, _) = ServerLinterBuilder::create_nested_configs( Path::new("/root/"), &LintOptions { disable_nested_config: true, ..LintOptions::default() }, + None, + &mut external_plugin_store, &mut nested_ignore_patterns, ); @@ -945,9 +1003,13 @@ mod test { #[test] fn test_create_nested_configs() { let mut nested_ignore_patterns = Vec::new(); + let mut external_plugin_store = ExternalPluginStore::default(); + let (configs, _) = ServerLinterBuilder::create_nested_configs( &get_file_path("fixtures/linter/init_nested_configs"), &LintOptions::default(), + None, + &mut external_plugin_store, &mut nested_ignore_patterns, ); let configs = configs.pin(); diff --git a/crates/oxc_language_server/src/linter/tester.rs b/crates/oxc_language_server/src/linter/tester.rs index ae296487ec62a..1d2bc3865b622 100644 --- a/crates/oxc_language_server/src/linter/tester.rs +++ b/crates/oxc_language_server/src/linter/tester.rs @@ -167,10 +167,8 @@ impl Tester<'_> { } fn create_linter(&self) -> ServerLinter { - ServerLinterBuilder::build( - &Self::get_root_uri(self.relative_root_dir), - self.options.clone(), - ) + ServerLinterBuilder::new(None) + .build(&Self::get_root_uri(self.relative_root_dir), self.options.clone()) } pub fn get_root_uri(relative_root_dir: &str) -> Uri { diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index 7c33e92bdd3ea..cd78d2b15ca78 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -6,7 +6,7 @@ async fn main() { #[cfg(feature = "formatter")] v.push(Box::new(oxc_language_server::ServerFormatterBuilder)); #[cfg(feature = "linter")] - v.push(Box::new(oxc_language_server::ServerLinterBuilder)); + v.push(Box::new(oxc_language_server::ServerLinterBuilder::new(None))); v }; diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs index 1f4adaad53959..86bf2bec1118f 100644 --- a/crates/oxc_linter/src/external_linter.rs +++ b/crates/oxc_linter/src/external_linter.rs @@ -1,25 +1,33 @@ -use std::{error::Error, fmt::Debug}; +use std::{error::Error, fmt::Debug, sync::Arc}; use serde::Deserialize; use oxc_allocator::Allocator; -pub type ExternalLinterLoadPluginCb = Box< - dyn Fn(String, String, Option) -> Result> - + Send - + Sync, +pub type ExternalLinterLoadPluginCb = Arc< + Box< + dyn Fn( + String, + String, + Option, + ) -> Result> + + Send + + Sync, + >, >; -pub type ExternalLinterLintFileCb = Box< - dyn Fn(String, String, Vec, String, &Allocator) -> Result, String> - + Sync - + Send, +pub type ExternalLinterLintFileCb = Arc< + Box< + dyn Fn(String, String, Vec, String, &Allocator) -> Result, String> + + Sync + + Send, + >, >; pub type ExternalLinterCreateWorkspaceCb = - Box Result<(), Box> + Send + Sync>; + Arc Result<(), Box> + Send + Sync>>; -pub type ExternalLinterDestroyWorkspaceCb = Box; +pub type ExternalLinterDestroyWorkspaceCb = Arc>; #[derive(Clone, Debug, Deserialize)] pub enum PluginLoadResult { @@ -49,12 +57,12 @@ pub struct JsFix { pub text: String, } +#[derive(Clone)] pub struct ExternalLinter { pub(crate) load_plugin: ExternalLinterLoadPluginCb, pub(crate) lint_file: ExternalLinterLintFileCb, pub create_workspace: ExternalLinterCreateWorkspaceCb, - #[expect(dead_code)] - destroy_workspace: ExternalLinterDestroyWorkspaceCb, + pub destroy_workspace: ExternalLinterDestroyWorkspaceCb, } impl ExternalLinter { diff --git a/editors/vscode/fixtures/js_plugins/.oxlintrc.json b/editors/vscode/fixtures/js_plugins/.oxlintrc.json new file mode 100644 index 0000000000000..6a2c6b1f6a1b8 --- /dev/null +++ b/editors/vscode/fixtures/js_plugins/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "jsPlugins": ["./plugin.js"], + "rules": { + "js-plugin/test-rule": "error", + "no-debugger": "off", + } +} diff --git a/editors/vscode/fixtures/js_plugins/index.js b/editors/vscode/fixtures/js_plugins/index.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/editors/vscode/fixtures/js_plugins/index.js @@ -0,0 +1 @@ +debugger; diff --git a/editors/vscode/fixtures/js_plugins/plugin.js b/editors/vscode/fixtures/js_plugins/plugin.js new file mode 100644 index 0000000000000..a99cfb1853227 --- /dev/null +++ b/editors/vscode/fixtures/js_plugins/plugin.js @@ -0,0 +1,22 @@ +const plugin = { + meta: { + name: 'js-plugin', + }, + rules: { + 'test-rule': { + create(context) { + return { + DebuggerStatement(debuggerStatement) { + context.report({ + message: 'Custom name JS Plugin Test Rule.', + node: debuggerStatement, + }); + }, + }; + }, + }, + }, +}; + + +export default plugin; diff --git a/editors/vscode/tests/e2e_server_linter.spec.ts b/editors/vscode/tests/e2e_server_linter.spec.ts index 49125061d62e1..46c736ca54140 100644 --- a/editors/vscode/tests/e2e_server_linter.spec.ts +++ b/editors/vscode/tests/e2e_server_linter.spec.ts @@ -282,4 +282,22 @@ suite('E2E Server Linter', () => { const secondDiagnostics = await getDiagnostics('index.ts'); strictEqual(secondDiagnostics.length, 1); }); + + test('js plugin support', async () => { + // only with `oxlint --lsp` + if (!process.env.SERVER_PATH_DEV?.endsWith('.js')) { + return; + } + + + await loadFixture('js_plugins'); + await sleep(500); + + const diagnostics = await getDiagnosticsWithoutClose('index.js'); + strictEqual(diagnostics.length, 1); + assert(typeof diagnostics[0].code == 'object'); + strictEqual(diagnostics[0].code.target.authority, 'oxc.rs'); + strictEqual(diagnostics[0].message, 'Custom name JS Plugin Test Rule.'); + strictEqual(diagnostics[0].severity, DiagnosticSeverity.Error); + }); });