Skip to content

Commit 094b81c

Browse files
committed
feat(language_server): add unusedDisableDirectives option (#11645)
closes #11618 VSCode users are now able to set the severity of unused disabled directives via `oxc.unusedDisableDirectives`. Other Clients can use this options too, by passing `unusedDisableDirectives` as a new config key. ![screenshot of vscode editor showing the new reporting error](https://github.com/user-attachments/assets/7581db72-7756-4f55-9df1-e873b3ca0f59)
1 parent 9fefb46 commit 094b81c

File tree

10 files changed

+150
-22
lines changed

10 files changed

+150
-22
lines changed

crates/oxc_language_server/README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco
1919

2020
These options can be passed with [initialize](#initialize), [workspace/didChangeConfiguration](#workspace/didChangeConfiguration) and [workspace/configuration](#workspace/configuration).
2121

22-
| Option Key | Value(s) | Default | Description |
23-
| ------------ | ---------------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
24-
| `run` | `"onSave" \| "onType"` | `"onType"` | Should the server lint the files when the user is typing or saving |
25-
| `configPath` | `<string>` \| `null` | `null` | Path to a oxlint configuration file, passing a string will disable nested configuration |
26-
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
22+
| Option Key | Value(s) | Default | Description |
23+
| ------------------------- | ------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
24+
| `run` | `"onSave" \| "onType"` | `"onType"` | Should the server lint the files when the user is typing or saving |
25+
| `configPath` | `<string>` \| `null` | `null` | Path to a oxlint configuration file, passing a string will disable nested configuration |
26+
| `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 |
27+
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
2728

2829
## Supported LSP Specifications from Server
2930

@@ -39,6 +40,7 @@ The client can pass the workspace options like following:
3940
"options": {
4041
"run": "onType",
4142
"configPath": null,
43+
"unusedDisableDirectives": "allow",
4244
"flags": {}
4345
}
4446
}]
@@ -72,6 +74,7 @@ The client can pass the workspace options like following:
7274
"options": {
7375
"run": "onType",
7476
"configPath": null,
77+
"unusedDisableDirectives": "allow",
7578
"flags": {}
7679
}
7780
}]
@@ -155,11 +158,10 @@ Only will be requested when the `ClientCapabilities` has `workspace.configuratio
155158
The client can return a response like:
156159

157160
```json
158-
{
159-
[{
160-
"run": "onType",
161-
"configPath": null,
162-
"flags": {}
163-
}]
164-
}
161+
[{
162+
"run": "onType",
163+
"configPath": null,
164+
"unusedDisableDirectives": "allow",
165+
"flags": {}
166+
}]
165167
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// oxlint-disable-next-line no-debugger -- on wrong line
2+
(() => {
3+
debugger;
4+
})();

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ use log::{debug, warn};
77
use rustc_hash::{FxBuildHasher, FxHashMap};
88
use tower_lsp_server::lsp_types::Uri;
99

10-
use oxc_linter::{Config, ConfigStore, ConfigStoreBuilder, LintOptions, Linter, Oxlintrc};
10+
use oxc_linter::{
11+
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, LintOptions, Linter, Oxlintrc,
12+
};
1113
use tower_lsp_server::UriExt;
1214

1315
use crate::linter::{
1416
error_with_position::DiagnosticReport,
1517
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
1618
};
19+
use crate::options::UnusedDisableDirectives;
1720
use crate::{ConcurrentHashMap, Options};
1821

1922
use super::config_walker::ConfigWalker;
@@ -65,7 +68,15 @@ impl ServerLinter {
6568
extended_paths.extend(config_builder.extended_paths.clone());
6669
let base_config = config_builder.build();
6770

68-
let lint_options = LintOptions { fix: options.fix_kind(), ..Default::default() };
71+
let lint_options = LintOptions {
72+
fix: options.fix_kind(),
73+
report_unused_directive: match options.unused_disable_directives {
74+
UnusedDisableDirectives::Allow => None, // or AllowWarnDeny::Allow, should be the same?
75+
UnusedDisableDirectives::Warn => Some(AllowWarnDeny::Warn),
76+
UnusedDisableDirectives::Deny => Some(AllowWarnDeny::Deny),
77+
},
78+
..Default::default()
79+
};
6980

7081
let config_store = ConfigStore::new(
7182
base_config,
@@ -358,4 +369,18 @@ mod test {
358369
)
359370
.test_and_snapshot_single_file("forward_ref.ts");
360371
}
372+
373+
#[test]
374+
fn test_report_unused_directives() {
375+
use crate::options::UnusedDisableDirectives;
376+
Tester::new(
377+
"fixtures/linter/unused_disabled_directives",
378+
Some(Options {
379+
unused_disable_directives: UnusedDisableDirectives::Deny,
380+
..Default::default()
381+
}),
382+
)
383+
// ToDo: this should be fixable
384+
.test_and_snapshot_single_file("test.js");
385+
}
361386
}

crates/oxc_language_server/src/options.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,21 @@ pub enum Run {
1313
OnType,
1414
}
1515

16+
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
17+
#[serde(rename_all = "camelCase")]
18+
pub enum UnusedDisableDirectives {
19+
#[default]
20+
Allow,
21+
Warn,
22+
Deny,
23+
}
24+
1625
#[derive(Debug, Default, Serialize, Clone)]
1726
#[serde(rename_all = "camelCase")]
1827
pub struct Options {
1928
pub run: Run,
2029
pub config_path: Option<String>,
30+
pub unused_disable_directives: UnusedDisableDirectives,
2131
pub flags: FxHashMap<String, String>,
2232
}
2333

@@ -79,6 +89,13 @@ impl TryFrom<Value> for Options {
7989
.get("run")
8090
.map(|run| serde_json::from_value::<Run>(run.clone()).unwrap_or_default())
8191
.unwrap_or_default(),
92+
unused_disable_directives: object
93+
.get("unusedDisableDirectives")
94+
.map(|key| {
95+
serde_json::from_value::<UnusedDisableDirectives>(key.clone())
96+
.unwrap_or_default()
97+
})
98+
.unwrap_or_default(),
8299
config_path: object
83100
.get("configPath")
84101
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
@@ -99,13 +116,14 @@ mod test {
99116
use rustc_hash::FxHashMap;
100117
use serde_json::json;
101118

102-
use super::{Options, Run, WorkspaceOption};
119+
use super::{Options, Run, UnusedDisableDirectives, WorkspaceOption};
103120

104121
#[test]
105122
fn test_valid_options_json() {
106123
let json = json!({
107124
"run": "onSave",
108125
"configPath": "./custom.json",
126+
"unusedDisableDirectives": "warn",
109127
"flags": {
110128
"disable_nested_config": "true",
111129
"fix_kind": "dangerous_fix"
@@ -115,6 +133,7 @@ mod test {
115133
let options = Options::try_from(json).unwrap();
116134
assert_eq!(options.run, Run::OnSave);
117135
assert_eq!(options.config_path, Some("./custom.json".into()));
136+
assert_eq!(options.unused_disable_directives, UnusedDisableDirectives::Warn);
118137
assert_eq!(options.flags.get("disable_nested_config"), Some(&"true".to_string()));
119138
assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string()));
120139
}
@@ -126,6 +145,7 @@ mod test {
126145
let options = Options::try_from(json).unwrap();
127146
assert_eq!(options.run, Run::OnType);
128147
assert_eq!(options.config_path, None);
148+
assert_eq!(options.unused_disable_directives, UnusedDisableDirectives::Allow);
129149
assert!(options.flags.is_empty());
130150
}
131151

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
source: crates/oxc_language_server/src/tester.rs
3+
input_file: crates/oxc_language_server/fixtures/linter/unused_disabled_directives/test.js
4+
---
5+
code: "eslint(no-debugger)"
6+
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-debugger.html"
7+
message: "`debugger` statement is not allowed\nhelp: Remove the debugger statement"
8+
range: Range { start: Position { line: 2, character: 2 }, end: Position { line: 2, character: 11 } }
9+
related_information[0].message: ""
10+
related_information[0].location.uri: "file://<variable>/fixtures/linter/unused_disabled_directives/test.js"
11+
related_information[0].location.range: Range { start: Position { line: 2, character: 2 }, end: Position { line: 2, character: 11 } }
12+
severity: Some(Warning)
13+
source: Some("oxc")
14+
tags: None
15+
fixed: Single(FixedContent { message: Some("Remove the debugger statement"), code: "", range: Range { start: Position { line: 2, character: 2 }, end: Position { line: 2, character: 11 } } })
16+
17+
18+
code: ""
19+
code_description.href: "None"
20+
message: "Unused eslint-disable directive (no problems were reported from no-debugger)."
21+
range: Range { start: Position { line: 0, character: 2 }, end: Position { line: 0, character: 56 } }
22+
related_information[0].message: ""
23+
related_information[0].location.uri: "file://<variable>/fixtures/linter/unused_disabled_directives/test.js"
24+
related_information[0].location.range: Range { start: Position { line: 0, character: 2 }, end: Position { line: 0, character: 56 } }
25+
severity: Some(Error)
26+
source: Some("oxc")
27+
tags: None
28+
fixed: None

crates/oxc_language_server/src/worker.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ impl WorkspaceWorker {
127127
old_options.config_path != new_options.config_path
128128
|| old_options.use_nested_configs() != new_options.use_nested_configs()
129129
|| old_options.fix_kind() != new_options.fix_kind()
130+
|| old_options.unused_disable_directives != new_options.unused_disable_directives
130131
}
131132

132133
pub async fn should_lint_on_run_type(&self, current_run: Run) -> bool {

editors/vscode/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ Following configuration are supported via `settings.json` and effect the window
3737

3838
Following configuration are supported via `settings.json` and can be changed for each workspace:
3939

40-
| Key | Default Value | Possible Values | Description |
41-
| ---------------- | ------------- | ------------------------ | --------------------------------------------------------------------------- |
42-
| `oxc.lint.run` | `onType` | `onSave` \| `onType` | Run the linter on save (onSave) or on type (onType) |
43-
| `oxc.configPath` | `null` | `null`\| `<string>` | Path to ESlint configuration. Keep it empty to enable nested configuration. |
44-
| `oxc.flags` | - | `Record<string, string>` | Custom flags passed to the language server. |
40+
| Key | Default Value | Possible Values | Description |
41+
| ----------------------------- | ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
42+
| `oxc.lint.run` | `onType` | `onSave` \| `onType` | Run the linter on save (onSave) or on type (onType) |
43+
| `oxc.configPath` | `null` | `null` \| `<string>` | Path to ESlint configuration. Keep it empty to enable nested configuration. |
44+
| `oxc.unusedDisableDirectives` | `allow` | `allow` \| `warn` \| `deny` | Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway. |
45+
| `oxc.flags` | - | `Record<string, string>` | Custom flags passed to the language server. |
4546

4647
#### Flags
4748

editors/vscode/client/WorkspaceConfig.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export const oxlintConfigFileName = '.oxlintrc.json';
55

66
export type Trigger = 'onSave' | 'onType';
77

8+
type UnusedDisableDirectives = 'allow' | 'warn' | 'deny';
9+
810
/**
911
* See `"contributes.configuration"` in `package.json`
1012
*/
@@ -24,6 +26,16 @@ export interface WorkspaceConfigInterface {
2426
* @default 'onType'
2527
*/
2628
run: Trigger;
29+
30+
/**
31+
* Define how directive comments like `// oxlint-disable-line` should be reported,
32+
* when no errors would have been reported on that line anyway
33+
*
34+
* `oxc.unusedDisableDirectives`
35+
*
36+
* @default 'allow'
37+
*/
38+
unusedDisableDirectives: UnusedDisableDirectives;
2739
/**
2840
* Additional flags to pass to the LSP binary
2941
* `oxc.flags`
@@ -36,6 +48,7 @@ export interface WorkspaceConfigInterface {
3648
export class WorkspaceConfig {
3749
private _configPath: string | null = null;
3850
private _runTrigger: Trigger = 'onType';
51+
private _unusedDisableDirectives: UnusedDisableDirectives = 'allow';
3952
private _flags: Record<string, string> = {};
4053

4154
constructor(private readonly workspace: WorkspaceFolder) {
@@ -53,6 +66,8 @@ export class WorkspaceConfig {
5366
this._runTrigger = this.configuration.get<Trigger>('lint.run') || 'onType';
5467
this._configPath = this.configuration.get<string | null>('configPath') ||
5568
(useNestedConfigs ? null : oxlintConfigFileName);
69+
this._unusedDisableDirectives = this.configuration.get<UnusedDisableDirectives>('unusedDisableDirectives') ??
70+
'allow';
5671
this._flags = flags;
5772
}
5873

@@ -63,6 +78,9 @@ export class WorkspaceConfig {
6378
if (event.affectsConfiguration(`${ConfigService.namespace}.lint.run`, this.workspace)) {
6479
return true;
6580
}
81+
if (event.affectsConfiguration(`${ConfigService.namespace}.unusedDisableDirectives`, this.workspace)) {
82+
return true;
83+
}
6684
if (event.affectsConfiguration(`${ConfigService.namespace}.flags`, this.workspace)) {
6785
return true;
6886
}
@@ -91,6 +109,15 @@ export class WorkspaceConfig {
91109
return this.configuration.update('configPath', value, ConfigurationTarget.WorkspaceFolder);
92110
}
93111

112+
get unusedDisableDirectives(): UnusedDisableDirectives {
113+
return this._unusedDisableDirectives;
114+
}
115+
116+
updateUnusedDisableDirectives(value: UnusedDisableDirectives): PromiseLike<void> {
117+
this._unusedDisableDirectives = value;
118+
return this.configuration.update('unusedDisableDirectives', value, ConfigurationTarget.WorkspaceFolder);
119+
}
120+
94121
get flags(): Record<string, string> {
95122
return this._flags;
96123
}
@@ -104,6 +131,7 @@ export class WorkspaceConfig {
104131
return {
105132
run: this.runTrigger,
106133
configPath: this.configPath ?? null,
134+
unusedDisableDirectives: this.unusedDisableDirectives,
107135
flags: this.flags,
108136
};
109137
}

editors/vscode/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@
110110
"default": null,
111111
"description": "Path to ESlint configuration. Keep it empty to enable nested configuration."
112112
},
113+
"oxc.unusedDisableDirectives": {
114+
"type": "string",
115+
"scope": "resource",
116+
"enum": [
117+
"allow",
118+
"warn",
119+
"deny"
120+
],
121+
"enumDescriptions": [
122+
"Allow",
123+
"Warn",
124+
"Deny"
125+
],
126+
"default": "allow",
127+
"description": "Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway."
128+
},
113129
"oxc.flags": {
114130
"type": "object",
115131
"scope": "resource",

editors/vscode/tests/WorkspaceConfig.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ suite('WorkspaceConfig', () => {
77
setup(async () => {
88
const workspaceConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER);
99
const globalConfig = workspace.getConfiguration('oxc');
10-
const keys = ['lint.run', 'configPath', 'flags'];
10+
const keys = ['lint.run', 'configPath', 'flags', 'unusedDisableDirectives'];
1111

1212
await Promise.all(keys.map(key => workspaceConfig.update(key, undefined, ConfigurationTarget.WorkspaceFolder)));
1313
// VSCode will not save different workspace configuration inside a `.code-workspace` file.
@@ -18,7 +18,7 @@ suite('WorkspaceConfig', () => {
1818
teardown(async () => {
1919
const workspaceConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER);
2020
const globalConfig = workspace.getConfiguration('oxc');
21-
const keys = ['lint.run', 'configPath', 'flags'];
21+
const keys = ['lint.run', 'configPath', 'flags', 'unusedDisableDirectives'];
2222

2323
await Promise.all(keys.map(key => workspaceConfig.update(key, undefined, ConfigurationTarget.WorkspaceFolder)));
2424
// VSCode will not save different workspace configuration inside a `.code-workspace` file.
@@ -30,6 +30,7 @@ suite('WorkspaceConfig', () => {
3030
const config = new WorkspaceConfig(WORKSPACE_FOLDER);
3131
strictEqual(config.runTrigger, 'onType');
3232
strictEqual(config.configPath, null);
33+
strictEqual(config.unusedDisableDirectives, 'allow');
3334
deepStrictEqual(config.flags, {});
3435
});
3536

@@ -61,13 +62,15 @@ suite('WorkspaceConfig', () => {
6162
await Promise.all([
6263
config.updateRunTrigger('onSave'),
6364
config.updateConfigPath('./somewhere'),
65+
config.updateUnusedDisableDirectives('deny'),
6466
config.updateFlags({ test: 'value' }),
6567
]);
6668

6769
const wsConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER);
6870

6971
strictEqual(wsConfig.get('lint.run'), 'onSave');
7072
strictEqual(wsConfig.get('configPath'), './somewhere');
73+
strictEqual(wsConfig.get('unusedDisableDirectives'), 'deny');
7174
deepStrictEqual(wsConfig.get('flags'), { test: 'value' });
7275
});
7376
});

0 commit comments

Comments
 (0)