Skip to content

Commit 2c3bc1b

Browse files
committed
feat(langauge_server): add typeAware option
1 parent 30c54a3 commit 2c3bc1b

File tree

9 files changed

+393
-12
lines changed

9 files changed

+393
-12
lines changed

crates/oxc_language_server/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ These options can be passed with [initialize](#initialize), [workspace/didChange
2525
| `configPath` | `<string>` \| `null` | `null` | Path to a oxlint configuration file, passing a string will disable nested configuration |
2626
| `tsConfigPath` | `<string>` \| `null` | `null` | Path to a TypeScript configuration file. If your `tsconfig.json` is not at the root, alias paths will not be resolve correctly for the `import` plugin |
2727
| `unusedDisableDirectives` | `"allow" \| "warn"` \| "deny"` | `"allow"` | Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway |
28+
| `typeAware` | `true` \| `false` | `false` | Enables type-aware linting |
2829
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
2930

3031
## Supported LSP Specifications from Server
@@ -43,6 +44,7 @@ The client can pass the workspace options like following:
4344
"configPath": null,
4445
"tsConfigPath": null,
4546
"unusedDisableDirectives": "allow",
47+
"typeAware": false,
4648
"flags": {}
4749
}
4850
}]
@@ -78,6 +80,7 @@ The client can pass the workspace options like following:
7880
"configPath": null,
7981
"tsConfigPath": null,
8082
"unusedDisableDirectives": "allow",
83+
"typeAware": false,
8184
"flags": {}
8285
}
8386
}]
@@ -166,6 +169,7 @@ The client can return a response like:
166169
"configPath": null,
167170
"tsConfigPath": null,
168171
"unusedDisableDirectives": "allow",
172+
"typeAware": false,
169173
"flags": {}
170174
}]
171175
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const promise = new Promise((resolve, _reject) => resolve("value"));
2+
promise;
3+
4+
async function returnsPromise() {
5+
return "value";
6+
}
7+
8+
returnsPromise().then(() => {});
9+
10+
Promise.reject("value").catch();
11+
12+
Promise.reject("value").finally();
13+
14+
[1, 2, 3].map(async (x) => x + 1);

crates/oxc_language_server/src/linter/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pub mod config_walker;
22
pub mod error_with_position;
33
pub mod isolated_lint_handler;
44
pub mod server_linter;
5+
pub mod tsgo_linter;

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use tower_lsp_server::UriExt;
1616
use crate::linter::{
1717
error_with_position::DiagnosticReport,
1818
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
19+
tsgo_linter::TsgoLinter,
1920
};
2021
use crate::options::UnusedDisableDirectives;
2122
use crate::{ConcurrentHashMap, OXC_CONFIG_FILE, Options};
@@ -24,6 +25,7 @@ use super::config_walker::ConfigWalker;
2425

2526
pub struct ServerLinter {
2627
isolated_linter: Arc<Mutex<IsolatedLintHandler>>,
28+
tsgo_linter: Arc<Option<TsgoLinter>>,
2729
gitignore_glob: Vec<Gitignore>,
2830
pub extended_paths: Vec<PathBuf>,
2931
}
@@ -97,7 +99,7 @@ impl ServerLinter {
9799

98100
let isolated_linter = IsolatedLintHandler::new(
99101
lint_options,
100-
config_store,
102+
config_store.clone(), // clone because tsgo linter needs it
101103
&IsolatedLintHandlerOptions {
102104
use_cross_module,
103105
root_path: root_path.to_path_buf(),
@@ -112,6 +114,11 @@ impl ServerLinter {
112114
isolated_linter: Arc::new(Mutex::new(isolated_linter)),
113115
gitignore_glob: Self::create_ignore_glob(&root_path),
114116
extended_paths,
117+
tsgo_linter: if options.type_aware {
118+
Arc::new(Some(TsgoLinter::new(&root_path, config_store)))
119+
} else {
120+
Arc::new(None)
121+
},
115122
}
116123
}
117124

@@ -220,7 +227,19 @@ impl ServerLinter {
220227
return None;
221228
}
222229

223-
self.isolated_linter.lock().await.run_single(uri, content)
230+
// when `IsolatedLintHandler` returns `None`, it means it does not want to lint.
231+
// Do not try `tsgolint` because it could be ignored or is not supported.
232+
let mut reports = self.isolated_linter.lock().await.run_single(uri, content.clone())?;
233+
234+
let Some(tsgo_linter) = &*self.tsgo_linter else {
235+
return Some(reports);
236+
};
237+
238+
if let Some(tsgo_reports) = tsgo_linter.lint_file(uri, content) {
239+
reports.extend(tsgo_reports);
240+
}
241+
242+
Some(reports)
224243
}
225244
}
226245

@@ -410,4 +429,13 @@ mod test {
410429
)
411430
.test_and_snapshot_single_file("deep/src/dep-a.ts");
412431
}
432+
433+
#[test]
434+
fn test_tsgo_lint() {
435+
let tester = Tester::new(
436+
"fixtures/linter/tsgolint",
437+
Some(Options { type_aware: true, ..Default::default() }),
438+
);
439+
tester.test_and_snapshot_single_file("no-floating-promises/index.ts");
440+
}
413441
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use std::{
2+
path::Path,
3+
sync::{Arc, OnceLock},
4+
};
5+
6+
use oxc_linter::{
7+
ConfigStore, LINTABLE_EXTENSIONS, TsGoLintState, loader::LINT_PARTIAL_LOADER_EXTENSIONS,
8+
read_to_string,
9+
};
10+
use rustc_hash::FxHashSet;
11+
use tower_lsp_server::{UriExt, lsp_types::Uri};
12+
13+
use crate::linter::error_with_position::{
14+
DiagnosticReport, message_with_position_to_lsp_diagnostic_report,
15+
};
16+
17+
pub struct TsgoLinter {
18+
state: TsGoLintState,
19+
}
20+
21+
impl TsgoLinter {
22+
pub fn new(root_uri: &Path, config_store: ConfigStore) -> Self {
23+
let state = TsGoLintState::new(root_uri, config_store);
24+
Self { state }
25+
}
26+
27+
pub fn lint_file(&self, uri: &Uri, content: Option<String>) -> Option<Vec<DiagnosticReport>> {
28+
let path = uri.to_file_path()?;
29+
30+
if !Self::should_lint_path(&path) {
31+
return None;
32+
}
33+
34+
let source_text = content.or_else(|| read_to_string(&path).ok())?;
35+
36+
let messages = self.state.lint_source(&Arc::from(path.as_os_str()), source_text).ok()?;
37+
38+
Some(
39+
messages
40+
.iter()
41+
.map(|e| message_with_position_to_lsp_diagnostic_report(e, uri))
42+
.collect(),
43+
)
44+
}
45+
46+
fn should_lint_path(path: &Path) -> bool {
47+
static WANTED_EXTENSIONS: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
48+
let wanted_exts = WANTED_EXTENSIONS.get_or_init(|| {
49+
LINTABLE_EXTENSIONS
50+
.iter()
51+
.filter(|ext| !LINT_PARTIAL_LOADER_EXTENSIONS.contains(ext))
52+
.copied()
53+
.collect()
54+
});
55+
56+
path.extension()
57+
.and_then(std::ffi::OsStr::to_str)
58+
.is_some_and(|ext| wanted_exts.contains(ext))
59+
}
60+
}

crates/oxc_language_server/src/options.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct Options {
2929
pub config_path: Option<String>,
3030
pub ts_config_path: Option<String>,
3131
pub unused_disable_directives: UnusedDisableDirectives,
32+
pub type_aware: bool,
3233
pub flags: FxHashMap<String, String>,
3334
}
3435

@@ -103,6 +104,9 @@ impl TryFrom<Value> for Options {
103104
ts_config_path: object
104105
.get("tsConfigPath")
105106
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
107+
type_aware: object
108+
.get("typeAware")
109+
.is_some_and(|key| serde_json::from_value::<bool>(key.clone()).unwrap_or_default()),
106110
flags,
107111
})
108112
}
@@ -128,6 +132,7 @@ mod test {
128132
"run": "onSave",
129133
"configPath": "./custom.json",
130134
"unusedDisableDirectives": "warn",
135+
"typeAware": true,
131136
"flags": {
132137
"disable_nested_config": "true",
133138
"fix_kind": "dangerous_fix"
@@ -138,6 +143,7 @@ mod test {
138143
assert_eq!(options.run, Run::OnSave);
139144
assert_eq!(options.config_path, Some("./custom.json".into()));
140145
assert_eq!(options.unused_disable_directives, UnusedDisableDirectives::Warn);
146+
assert!(options.type_aware);
141147
assert_eq!(options.flags.get("disable_nested_config"), Some(&"true".to_string()));
142148
assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string()));
143149
}
@@ -150,6 +156,7 @@ mod test {
150156
assert_eq!(options.run, Run::OnType);
151157
assert_eq!(options.config_path, None);
152158
assert_eq!(options.unused_disable_directives, UnusedDisableDirectives::Allow);
159+
assert!(!options.type_aware);
153160
assert!(options.flags.is_empty());
154161
}
155162

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
source: crates/oxc_language_server/src/tester.rs
3+
input_file: crates/oxc_language_server/fixtures/linter/tsgolint/no-floating-promises/index.ts
4+
---
5+
code: "typescript-eslint(no-confusing-void-expression)"
6+
code_description.href: "None"
7+
message: "Returning a void expression from an arrow function shorthand is forbidden. Please add braces to the arrow function."
8+
range: Range { start: Position { line: 0, character: 50 }, end: Position { line: 0, character: 66 } }
9+
related_information[0].message: ""
10+
related_information[0].location.uri: "file://<variable>/fixtures/linter/tsgolint/no-floating-promises/index.ts"
11+
related_information[0].location.range: Range { start: Position { line: 0, character: 50 }, end: Position { line: 0, character: 66 } }
12+
severity: Some(Warning)
13+
source: Some("oxc")
14+
tags: None
15+
fixed: Single(FixedContent { message: None, code: "{ resolve(\"value\"); }", range: Range { start: Position { line: 0, character: 49 }, end: Position { line: 0, character: 66 } } })
16+
17+
18+
code: "typescript-eslint(no-floating-promises)"
19+
code_description.href: "None"
20+
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."
21+
range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 8 } }
22+
related_information[0].message: ""
23+
related_information[0].location.uri: "file://<variable>/fixtures/linter/tsgolint/no-floating-promises/index.ts"
24+
related_information[0].location.range: Range { start: Position { line: 1, character: 0 }, end: Position { line: 1, character: 8 } }
25+
severity: Some(Warning)
26+
source: Some("oxc")
27+
tags: None
28+
fixed: None
29+
30+
31+
code: "typescript-eslint(no-floating-promises)"
32+
code_description.href: "None"
33+
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."
34+
range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 32 } }
35+
related_information[0].message: ""
36+
related_information[0].location.uri: "file://<variable>/fixtures/linter/tsgolint/no-floating-promises/index.ts"
37+
related_information[0].location.range: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 32 } }
38+
severity: Some(Warning)
39+
source: Some("oxc")
40+
tags: None
41+
fixed: None
42+
43+
44+
code: "typescript-eslint(no-floating-promises)"
45+
code_description.href: "None"
46+
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."
47+
range: Range { start: Position { line: 9, character: 0 }, end: Position { line: 9, character: 32 } }
48+
related_information[0].message: ""
49+
related_information[0].location.uri: "file://<variable>/fixtures/linter/tsgolint/no-floating-promises/index.ts"
50+
related_information[0].location.range: Range { start: Position { line: 9, character: 0 }, end: Position { line: 9, character: 32 } }
51+
severity: Some(Warning)
52+
source: Some("oxc")
53+
tags: None
54+
fixed: None
55+
56+
57+
code: "typescript-eslint(no-floating-promises)"
58+
code_description.href: "None"
59+
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."
60+
range: Range { start: Position { line: 11, character: 0 }, end: Position { line: 11, character: 34 } }
61+
related_information[0].message: ""
62+
related_information[0].location.uri: "file://<variable>/fixtures/linter/tsgolint/no-floating-promises/index.ts"
63+
related_information[0].location.range: Range { start: Position { line: 11, character: 0 }, end: Position { line: 11, character: 34 } }
64+
severity: Some(Warning)
65+
source: Some("oxc")
66+
tags: None
67+
fixed: None
68+
69+
70+
code: "typescript-eslint(no-floating-promises)"
71+
code_description.href: "None"
72+
message: "An array of Promises may be unintentional.\nhelp: Consider handling the promises' fulfillment or rejection with Promise.all or similar, or explicitly marking the expression as ignored with the `void` operator."
73+
range: Range { start: Position { line: 13, character: 0 }, end: Position { line: 13, character: 34 } }
74+
related_information[0].message: ""
75+
related_information[0].location.uri: "file://<variable>/fixtures/linter/tsgolint/no-floating-promises/index.ts"
76+
related_information[0].location.range: Range { start: Position { line: 13, character: 0 }, end: Position { line: 13, character: 34 } }
77+
severity: Some(Warning)
78+
source: Some("oxc")
79+
tags: None
80+
fixed: None

crates/oxc_language_server/src/worker.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{str::FromStr, sync::Arc, vec};
22

3-
use log::debug;
3+
use log::{debug, warn};
44
use rustc_hash::FxBuildHasher;
55
use tokio::sync::{Mutex, RwLock};
66
use tower_lsp_server::{
@@ -129,12 +129,23 @@ impl WorkspaceWorker {
129129
|| old_options.use_nested_configs() != new_options.use_nested_configs()
130130
|| old_options.fix_kind() != new_options.fix_kind()
131131
|| old_options.unused_disable_directives != new_options.unused_disable_directives
132+
// TODO: only the TsgoLinter needs to be dropped or created
133+
|| old_options.type_aware != new_options.type_aware
132134
}
133135

134136
pub async fn should_lint_on_run_type(&self, current_run: Run) -> bool {
135-
let run_level = { self.options.lock().await.run };
137+
let options = self.options.lock().await;
138+
// `tsgolint` only supported the os file system. We can not run it on memory file system.
139+
if options.type_aware {
140+
if options.run == Run::OnType {
141+
warn!(
142+
"Linting with type aware is only supported with the OS file system. Change your settings to use onSave."
143+
);
144+
}
145+
return current_run == Run::OnSave;
146+
}
136147

137-
run_level == current_run
148+
options.run == current_run
138149
}
139150

140151
pub async fn lint_file(

0 commit comments

Comments
 (0)