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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

debugger;

async function returnsPromise() {
return "value";
}

returnsPromise().then(() => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

debugger;

async function returnsPromise() {
return "value";
}

returnsPromise().then(() => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

debugger;

async function returnsPromise() {
return "value";
}

returnsPromise().then(() => {});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

debugger;

async function returnsPromise() {
return "value";
}

returnsPromise().then(() => {});
84 changes: 79 additions & 5 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,24 @@ use crate::linter::{
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
tsgo_linter::TsgoLinter,
};
use crate::options::UnusedDisableDirectives;
use crate::options::{Run, UnusedDisableDirectives};
use crate::{ConcurrentHashMap, OXC_CONFIG_FILE, Options};

use super::config_walker::ConfigWalker;

#[derive(Debug, PartialEq, Eq)]
pub enum ServerLinterRun {
OnType,
OnSave,
Always,
}

pub struct ServerLinter {
isolated_linter: Arc<Mutex<IsolatedLintHandler>>,
tsgo_linter: Arc<Option<TsgoLinter>>,
ignore_matcher: LintIgnoreMatcher,
gitignore_glob: Vec<Gitignore>,
lint_on_run: Run,
pub extended_paths: Vec<PathBuf>,
}

Expand Down Expand Up @@ -125,6 +133,7 @@ impl ServerLinter {
),
gitignore_glob: Self::create_ignore_glob(&root_path),
extended_paths,
lint_on_run: options.run,
tsgo_linter: if options.type_aware {
Arc::new(Some(TsgoLinter::new(&root_path, config_store)))
} else {
Expand Down Expand Up @@ -243,14 +252,42 @@ impl ServerLinter {
&self,
uri: &Uri,
content: Option<String>,
run_type: ServerLinterRun,
) -> Option<Vec<DiagnosticReport>> {
let (oxlint, tsgolint) = match (run_type, self.lint_on_run) {
// run everything on save, or when it is forced
(ServerLinterRun::Always, _) | (ServerLinterRun::OnSave, Run::OnSave) => (true, true),
// run only oxlint on type
// tsgolint does not support memory source_text
(ServerLinterRun::OnType, Run::OnType) => (true, false),
// it does not match, run nothing
(ServerLinterRun::OnType, Run::OnSave) => (false, false),
// run only tsglint on save, even if the user wants it with type
// tsgolint only supports the OS file system.
(ServerLinterRun::OnSave, Run::OnType) => (false, true),
};

// return `None` when both tools do not want to be used
if !oxlint && !tsgolint {
return None;
}

if self.is_ignored(uri) {
return None;
}

// when `IsolatedLintHandler` returns `None`, it means it does not want to lint.
// Do not try `tsgolint` because it could be ignored or is not supported.
let mut reports = self.isolated_linter.lock().await.run_single(uri, content.clone())?;
let mut reports = Vec::with_capacity(0);

if oxlint
&& let Some(oxlint_reports) =
self.isolated_linter.lock().await.run_single(uri, content.clone())
{
reports.extend(oxlint_reports);
}

if !tsgolint {
return Some(reports);
}

let Some(tsgo_linter) = &*self.tsgo_linter else {
return Some(reports);
Expand Down Expand Up @@ -296,6 +333,7 @@ mod test {
use crate::{
Options,
linter::server_linter::{ServerLinter, normalize_path},
options::Run,
tester::{Tester, get_file_path},
};
use rustc_hash::FxHashMap;
Expand Down Expand Up @@ -342,6 +380,42 @@ mod test {
assert!(configs_dirs[0].ends_with("init_nested_configs"));
}

#[test]
fn test_lint_on_run_on_type_on_type() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
Some(Options { type_aware: true, run: Run::OnType, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
}

#[test]
fn test_lint_on_run_on_type_on_save() {
Tester::new(
"fixtures/linter/lint_on_run/on_save",
Some(Options { type_aware: true, run: Run::OnType, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-save.ts", Run::OnSave);
}

#[test]
fn test_lint_on_run_on_save_on_type() {
Tester::new(
"fixtures/linter/lint_on_run/on_save",
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
}

#[test]
fn test_lint_on_run_on_save_on_save() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-save.ts", Run::OnSave);
}

#[test]
fn test_no_errors() {
Tester::new("fixtures/linter/no_errors", None)
Expand Down Expand Up @@ -460,7 +534,7 @@ mod test {
fn test_tsgo_lint() {
let tester = Tester::new(
"fixtures/linter/tsgolint",
Some(Options { type_aware: true, ..Default::default() }),
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
);
tester.test_and_snapshot_single_file("no-floating-promises/index.ts");
}
Expand Down
17 changes: 7 additions & 10 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ mod worker;
use capabilities::Capabilities;
use code_actions::CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC;
use commands::{FIX_ALL_COMMAND_ID, FixAllCommandArgs};
use options::{Options, Run, WorkspaceOption};
use linter::server_linter::ServerLinterRun;
use options::{Options, WorkspaceOption};
use worker::WorkspaceWorker;

type ConcurrentHashMap<K, V> = papaya::HashMap<K, V, FxBuildHasher>;
Expand Down Expand Up @@ -441,10 +442,7 @@ impl LanguageServer for Backend {
let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else {
return;
};
if !worker.should_lint_on_run_type(Run::OnSave).await {
return;
}
if let Some(diagnostics) = worker.lint_file(uri, None).await {
if let Some(diagnostics) = worker.lint_file(uri, None, ServerLinterRun::OnSave).await {
self.client
.publish_diagnostics(
uri.clone(),
Expand All @@ -463,11 +461,8 @@ impl LanguageServer for Backend {
let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else {
return;
};
if !worker.should_lint_on_run_type(Run::OnType).await {
return;
}
let content = params.content_changes.first().map(|c| c.text.clone());
if let Some(diagnostics) = worker.lint_file(uri, content).await {
if let Some(diagnostics) = worker.lint_file(uri, content, ServerLinterRun::OnType).await {
self.client
.publish_diagnostics(
uri.clone(),
Expand All @@ -486,7 +481,9 @@ impl LanguageServer for Backend {
};

let content = params.text_document.text;
if let Some(diagnostics) = worker.lint_file(uri, Some(content)).await {
if let Some(diagnostics) =
worker.lint_file(uri, Some(content), ServerLinterRun::Always).await
{
self.client
.publish_diagnostics(
uri.clone(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: crates/oxc_language_server/src/tester.rs
input_file: crates/oxc_language_server/fixtures/linter/lint_on_run/on_save/on-save.ts
---
code: "typescript-eslint(no-floating-promises)"
code_description.href: "None"
message: "Promises must be awaited.\nhelp: The promise must end with a call to .catch, or end with a call to .then with a rejection handler, or be explicitly marked as ignored with the `void` operator."
range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 32 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/lint_on_run/on_save/on-save.ts"
related_information[0].location.range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 32 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
fixed: Multiple([FixedContent { message: Some("Promises must be awaited."), code: "void ", range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 0 } } }, FixedContent { message: Some("Promises must be awaited."), code: "await ", range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 0 } } }])
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: crates/oxc_language_server/src/tester.rs
input_file: crates/oxc_language_server/fixtures/linter/lint_on_run/on_save/on-type.ts
---
File is ignored
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: crates/oxc_language_server/src/tester.rs
input_file: crates/oxc_language_server/fixtures/linter/lint_on_run/on_type/on-save.ts
---
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html"
message: "`debugger` statement is not allowed\nhelp: Remove the debugger statement"
range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 9 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/lint_on_run/on_type/on-save.ts"
related_information[0].location.range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 9 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
fixed: Single(FixedContent { message: Some("Remove the debugger statement"), code: "", range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 9 } } })


code: "typescript-eslint(no-floating-promises)"
code_description.href: "None"
message: "Promises must be awaited.\nhelp: The promise must end with a call to .catch, or end with a call to .then with a rejection handler, or be explicitly marked as ignored with the `void` operator."
range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 32 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/lint_on_run/on_type/on-save.ts"
related_information[0].location.range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 32 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
fixed: Multiple([FixedContent { message: Some("Promises must be awaited."), code: "void ", range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 0 } } }, FixedContent { message: Some("Promises must be awaited."), code: "await ", range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 0 } } }])
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: crates/oxc_language_server/src/tester.rs
input_file: crates/oxc_language_server/fixtures/linter/lint_on_run/on_type/on-type.ts
---
code: "eslint(no-debugger)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html"
message: "`debugger` statement is not allowed\nhelp: Remove the debugger statement"
range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 9 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/lint_on_run/on_type/on-type.ts"
related_information[0].location.range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 9 } }
severity: Some(Warning)
source: Some("oxc")
tags: None
fixed: Single(FixedContent { message: Some("Remove the debugger statement"), code: "", range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 9 } } })
33 changes: 28 additions & 5 deletions crates/oxc_language_server/src/tester.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use tower_lsp_server::{
lsp_types::{CodeDescription, NumberOrString, Uri},
};

use crate::{Options, worker::WorkspaceWorker};
use crate::{
Options, linter::server_linter::ServerLinterRun, options::Run, worker::WorkspaceWorker,
};

use super::linter::error_with_position::DiagnosticReport;

Expand Down Expand Up @@ -113,12 +115,33 @@ impl Tester<'_> {

/// Given a relative file path (relative to `oxc_language_server` crate root), run the linter
/// and return the resulting diagnostics in a custom snapshot format.
#[expect(clippy::disallowed_methods)]
pub fn test_and_snapshot_single_file(&self, relative_file_path: &str) {
self.test_and_snapshot_single_file_with_run_type(
relative_file_path,
self.options.as_ref().map_or(Run::default(), |o| o.run),
);
}

#[expect(clippy::disallowed_methods)]
pub fn test_and_snapshot_single_file_with_run_type(
&self,
relative_file_path: &str,
run_type: Run,
) {
let uri = get_file_uri(&format!("{}/{}", self.relative_root_dir, relative_file_path));
let reports = tokio::runtime::Runtime::new()
.unwrap()
.block_on(async { self.create_workspace_worker().await.lint_file(&uri, None).await });
let reports = tokio::runtime::Runtime::new().unwrap().block_on(async {
self.create_workspace_worker()
.await
.lint_file(
&uri,
None,
match run_type {
Run::OnSave => ServerLinterRun::OnSave,
Run::OnType => ServerLinterRun::OnType,
},
)
.await
});
let snapshot = if let Some(reports) = reports {
if reports.is_empty() {
"No diagnostic reports".to_string()
Expand Down
Loading
Loading