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
16 changes: 10 additions & 6 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco

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

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

## Supported LSP Specifications from Server

Expand All @@ -40,6 +41,7 @@ The client can pass the workspace options like following:
"options": {
"run": "onType",
"configPath": null,
"tsConfigPath": null,
"unusedDisableDirectives": "allow",
"flags": {}
}
Expand Down Expand Up @@ -74,6 +76,7 @@ The client can pass the workspace options like following:
"options": {
"run": "onType",
"configPath": null,
"tsConfigPath": null,
"unusedDisableDirectives": "allow",
"flags": {}
}
Expand Down Expand Up @@ -161,6 +164,7 @@ The client can return a response like:
[{
"run": "onType",
"configPath": null,
"tsConfigPath": null,
"unusedDisableDirectives": "allow",
"flags": {}
}]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins": ["import"],
"rules": {
"import/no-cycle": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// should report cycle detected
import { b } from '@/dep-b';

b();
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// this file is also included in dep-a.ts and dep-a.ts should report a no-cycle diagnostic
import './dep-a.ts';

export function b() { /* ... */ }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"declaration": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use super::error_with_position::{
pub struct IsolatedLintHandlerOptions {
pub use_cross_module: bool,
pub root_path: PathBuf,
pub tsconfig_path: Option<PathBuf>,
}

pub struct IsolatedLintHandler {
Expand Down Expand Up @@ -68,9 +69,13 @@ impl IsolatedLintHandler {
options: &IsolatedLintHandlerOptions,
) -> Self {
let linter = Linter::new(lint_options, config_store, None);
let lint_service_options = LintServiceOptions::new(options.root_path.clone())
let mut lint_service_options = LintServiceOptions::new(options.root_path.clone())
.with_cross_module(options.use_cross_module);

if let Some(tsconfig_path) = &options.tsconfig_path {
lint_service_options = lint_service_options.with_tsconfig(tsconfig_path);
}

let service = LintService::new(linter, AllocatorPool::default(), lint_service_options);

Self { service }
Expand Down
21 changes: 20 additions & 1 deletion crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,14 @@ impl ServerLinter {
let isolated_linter = IsolatedLintHandler::new(
lint_options,
config_store,
&IsolatedLintHandlerOptions { use_cross_module, root_path: root_path.to_path_buf() },
&IsolatedLintHandlerOptions {
use_cross_module,
root_path: root_path.to_path_buf(),
tsconfig_path: options
.ts_config_path
.as_ref()
.map(|path| Path::new(path).to_path_buf()),
},
);

Self {
Expand Down Expand Up @@ -406,4 +413,16 @@ mod test {
Tester::new("fixtures/linter/root_ignore_patterns", None)
.test_and_snapshot_single_file("ignored-file.ts");
}

#[test]
fn test_ts_alias() {
Tester::new(
"fixtures/linter/ts_path_alias",
Some(Options {
ts_config_path: Some("./deep/tsconfig.json".to_string()),
..Default::default()
}),
)
.test_and_snapshot_single_file("deep/src/dep-a.ts");
}
}
4 changes: 4 additions & 0 deletions crates/oxc_language_server/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum UnusedDisableDirectives {
pub struct Options {
pub run: Run,
pub config_path: Option<String>,
pub ts_config_path: Option<String>,
pub unused_disable_directives: UnusedDisableDirectives,
pub flags: FxHashMap<String, String>,
}
Expand Down Expand Up @@ -99,6 +100,9 @@ impl TryFrom<Value> for Options {
config_path: object
.get("configPath")
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
ts_config_path: object
.get("tsConfigPath")
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
flags,
})
}
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/ts_path_alias/deep/src/dep-a.ts
---
code: "eslint-plugin-import(no-cycle)"
code_description.href: "https://oxc.rs/docs/guide/usage/linter/rules/import/no-cycle.html"
message: "Dependency cycle detected\nhelp: These paths form a cycle: \n-> @/dep-b - fixtures/linter/ts_path_alias/deep/src/dep-b.ts\n-> ./dep-a.ts - fixtures/linter/ts_path_alias/deep/src/dep-a.ts"
range: Range { start: Position { line: 1, character: 18 }, end: Position { line: 1, character: 27 } }
related_information[0].message: ""
related_information[0].location.uri: "file://<variable>/fixtures/linter/ts_path_alias/deep/src/dep-a.ts"
related_information[0].location.range: Range { start: Position { line: 1, character: 18 }, end: Position { line: 1, character: 27 } }
severity: Some(Error)
source: Some("oxc")
tags: None
fixed: None
1 change: 1 addition & 0 deletions crates/oxc_language_server/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ impl WorkspaceWorker {

fn needs_linter_restart(old_options: &Options, new_options: &Options) -> bool {
old_options.config_path != new_options.config_path
|| old_options.ts_config_path != new_options.ts_config_path
|| old_options.use_nested_configs() != new_options.use_nested_configs()
|| old_options.fix_kind() != new_options.fix_kind()
|| old_options.unused_disable_directives != new_options.unused_disable_directives
Expand Down
13 changes: 7 additions & 6 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ Following configuration are supported via `settings.json` and effect the window

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

| Key | Default Value | Possible Values | Description |
| ----------------------------- | ------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `oxc.lint.run` | `onType` | `onSave` \| `onType` | Run the linter on save (onSave) or on type (onType) |
| `oxc.configPath` | `null` | `null` \| `<string>` | Path to ESlint configuration. Keep it empty to enable nested configuration. |
| `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. |
| `oxc.flags` | - | `Record<string, string>` | Custom flags passed to the language server. |
| Key | Default Value | Possible Values | Description |
| ----------------------------- | ------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `oxc.lint.run` | `onType` | `onSave` \| `onType` | Run the linter on save (onSave) or on type (onType) |
| `oxc.configPath` | `null` | `null` \| `<string>` | Path to ESlint configuration. Keep it empty to enable nested configuration. |
| `oxc.tsConfigPath` | `null` | `null` \| `<string>` | Path to TypeScript configuration. If your `tsconfig.json` is not at the root, alias paths will not be resolve correctly for the `import` plugin. |
| `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. |
| `oxc.flags` | - | `Record<string, string>` | Custom flags passed to the language server. |

#### Flags

Expand Down
25 changes: 24 additions & 1 deletion editors/vscode/client/WorkspaceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export interface WorkspaceConfigInterface {
* @default null
*/
configPath: string | null;
/**
* typescript config path
*
* `oxc.tsConfigPath`
*
* @default null
*/
tsConfigPath: string | null;
/**
* When to run the linter and generate diagnostics
* `oxc.lint.run`
Expand Down Expand Up @@ -47,6 +55,7 @@ export interface WorkspaceConfigInterface {

export class WorkspaceConfig {
private _configPath: string | null = null;
private _tsConfigPath: string | null = null;
private _runTrigger: Trigger = 'onType';
private _unusedDisableDirectives: UnusedDisableDirectives = 'allow';
private _flags: Record<string, string> = {};
Expand All @@ -66,6 +75,7 @@ export class WorkspaceConfig {
this._runTrigger = this.configuration.get<Trigger>('lint.run') || 'onType';
this._configPath = this.configuration.get<string | null>('configPath') ||
(useNestedConfigs ? null : oxlintConfigFileName);
this._tsConfigPath = this.configuration.get<string | null>('tsConfigPath') ?? null;
this._unusedDisableDirectives = this.configuration.get<UnusedDisableDirectives>('unusedDisableDirectives') ??
'allow';
this._flags = flags;
Expand All @@ -75,6 +85,9 @@ export class WorkspaceConfig {
if (event.affectsConfiguration(`${ConfigService.namespace}.configPath`, this.workspace)) {
return true;
}
if (event.affectsConfiguration(`${ConfigService.namespace}.tsConfigPath`, this.workspace)) {
return true;
}
if (event.affectsConfiguration(`${ConfigService.namespace}.lint.run`, this.workspace)) {
return true;
}
Expand Down Expand Up @@ -104,11 +117,20 @@ export class WorkspaceConfig {
return this._configPath;
}

updateConfigPath(value: string): PromiseLike<void> {
updateConfigPath(value: string | null): PromiseLike<void> {
this._configPath = value;
return this.configuration.update('configPath', value, ConfigurationTarget.WorkspaceFolder);
}

get tsConfigPath(): string | null {
return this._tsConfigPath;
}

updateTsConfigPath(value: string | null): PromiseLike<void> {
this._tsConfigPath = value;
return this.configuration.update('tsConfigPath', value, ConfigurationTarget.WorkspaceFolder);
}

get unusedDisableDirectives(): UnusedDisableDirectives {
return this._unusedDisableDirectives;
}
Expand All @@ -131,6 +153,7 @@ export class WorkspaceConfig {
return {
run: this.runTrigger,
configPath: this.configPath ?? null,
tsConfigPath: this.tsConfigPath ?? null,
unusedDisableDirectives: this.unusedDisableDirectives,
flags: this.flags,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins": ["import"],
"rules": {
"import/no-cycle": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// should report cycle detected
import { b } from '@/dep-b';

b();
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// this file is also included in dep-a.ts and dep-a.ts should report a no-cycle diagnostic
import './dep-a.ts';

export function b() { /* ... */ }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"declaration": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
11 changes: 10 additions & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,16 @@
],
"scope": "resource",
"default": null,
"description": "Path to ESlint configuration. Keep it empty to enable nested configuration."
"description": "Path to oxlint configuration. Keep it empty to enable nested configuration."
},
"oxc.tsConfigPath": {
"type": [
"string",
"null"
],
"scope": "resource",
"default": null,
"description": "Path to TypeScript configuration. If your `tsconfig.json` is not at the root, alias paths will not be resolve correctly for the `import` plugin."
},
"oxc.unusedDisableDirectives": {
"type": "string",
Expand Down
7 changes: 5 additions & 2 deletions editors/vscode/tests/WorkspaceConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ suite('WorkspaceConfig', () => {
setup(async () => {
const workspaceConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER);
const globalConfig = workspace.getConfiguration('oxc');
const keys = ['lint.run', 'configPath', 'flags', 'unusedDisableDirectives'];
const keys = ['lint.run', 'configPath', 'tsConfigPath', 'flags', 'unusedDisableDirectives'];

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

await Promise.all(keys.map(key => workspaceConfig.update(key, undefined, ConfigurationTarget.WorkspaceFolder)));
// VSCode will not save different workspace configuration inside a `.code-workspace` file.
Expand All @@ -30,6 +30,7 @@ suite('WorkspaceConfig', () => {
const config = new WorkspaceConfig(WORKSPACE_FOLDER);
strictEqual(config.runTrigger, 'onType');
strictEqual(config.configPath, null);
strictEqual(config.tsConfigPath, null);
strictEqual(config.unusedDisableDirectives, 'allow');
deepStrictEqual(config.flags, {});
});
Expand Down Expand Up @@ -62,6 +63,7 @@ suite('WorkspaceConfig', () => {
await Promise.all([
config.updateRunTrigger('onSave'),
config.updateConfigPath('./somewhere'),
config.updateTsConfigPath('./tsconfig.json'),
config.updateUnusedDisableDirectives('deny'),
config.updateFlags({ test: 'value' }),
]);
Expand All @@ -70,6 +72,7 @@ suite('WorkspaceConfig', () => {

strictEqual(wsConfig.get('lint.run'), 'onSave');
strictEqual(wsConfig.get('configPath'), './somewhere');
strictEqual(wsConfig.get('tsConfigPath'), './tsconfig.json');
strictEqual(wsConfig.get('unusedDisableDirectives'), 'deny');
deepStrictEqual(wsConfig.get('flags'), { test: 'value' });
});
Expand Down
19 changes: 19 additions & 0 deletions editors/vscode/tests/e2e_server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ suiteSetup(async () => {

teardown(async () => {
await workspace.getConfiguration('oxc').update('flags', undefined);
await workspace.getConfiguration('oxc').update('tsConfigPath', undefined);
await workspace.saveAll();
});

Expand Down Expand Up @@ -219,6 +220,24 @@ suite('E2E Diagnostics', () => {
strictEqual(secondDiagnostics[0].severity, DiagnosticSeverity.Error);
});

testSingleFolderMode('changing oxc.tsConfigPath will revalidate the diagnostics', async () => {
await loadFixture('changing_tsconfig_path');
const firstDiagnostics = await getDiagnostics('deep/src/dep-a.ts');

strictEqual(firstDiagnostics.length, 0);

await workspace.getConfiguration('oxc').update('tsConfigPath', "fixtures/deep/tsconfig.json");
await workspace.saveAll();
await waitForDiagnosticChange();

const secondDiagnostics = await getDiagnostics('deep/src/dep-a.ts');
strictEqual(secondDiagnostics.length, 1);
assert(typeof secondDiagnostics[0].code == 'object');
strictEqual(secondDiagnostics[0].code.target.authority, 'oxc.rs');
assert(secondDiagnostics[0].message.startsWith("Dependency cycle detected"));
strictEqual(secondDiagnostics[0].severity, DiagnosticSeverity.Error);
});

test('cross module', async () => {
await loadFixture('cross_module');
const diagnostics = await getDiagnostics('dep-a.ts');
Expand Down
Loading