diff --git a/editors/vscode/README.md b/editors/vscode/README.md index ac4e80db5e454..e4274ad729894 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -64,11 +64,22 @@ Following configuration are supported via `settings.json` and can be changed for | `oxc.tsConfigPath` | `null` | `null` \| `` | 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.typeAware` | `false` | `false` \| `true` | Enable type aware linting. | -| `oxc.flags` | - | `Record` | Custom flags passed to the language server. | +| `oxc.disableNestedConfig` | `false` | `false` \| `true` | Disable searching for nested configuration files. | +| `oxc.fixKind` | `safe_fix` | [FixKind](#fixkind) | Specify the kind of fixes to suggest/apply. | | `oxc.fmt.experimental` | `false` | `false` \| `true` | Enable experimental formatting support. This feature is experimental and might not work as expected. | | `oxc.fmt.configPath` | `null` | `` \| `null` | Path to an oxfmt configuration file. When `null`, the server will use `.oxfmtrc.json` at the workspace root. | +| `oxc.flags` | - | `Record` | (deprecated) Custom flags passed to the language server. | -#### Flags +#### FixKind + +- `"safe_fix"` (default) +- `"safe_fix_or_suggestion"` +- `"dangerous_fix"` +- `"dangerous_fix_or_suggestion"` +- `"none"` +- `"all"` + +#### Flags (deprecated) - `key: disable_nested_config`: Disabled nested configuration and searches only for `configPath` - `key: fix_kind`: default: `"safe_fix"`, possible values `"safe_fix" | "safe_fix_or_suggestion" | "dangerous_fix" | "dangerous_fix_or_suggestion" | "none" | "all"` diff --git a/editors/vscode/client/WorkspaceConfig.ts b/editors/vscode/client/WorkspaceConfig.ts index ae53842396876..c2e6bf4310c29 100644 --- a/editors/vscode/client/WorkspaceConfig.ts +++ b/editors/vscode/client/WorkspaceConfig.ts @@ -7,6 +7,15 @@ export type Trigger = 'onSave' | 'onType'; type UnusedDisableDirectives = 'allow' | 'warn' | 'deny'; +export enum FixKind { + SafeFix = 'safe_fix', + SafeFixOrSuggestion = 'safe_fix_or_suggestion', + DangerousFix = 'dangerous_fix', + DangerousFixOrSuggestion = 'dangerous_fix_or_suggestion', + None = 'none', + All = 'all', +} + /** * See `"contributes.configuration"` in `package.json` */ @@ -54,6 +63,20 @@ export interface WorkspaceConfigInterface { */ typeAware: boolean; + /** + * Disable nested config files detection + * `oxc.disableNestedConfig` + * @default false + */ + disableNestedConfig: boolean; + + /** + * Fix kind to use when applying fixes + * `oxc.fixKind` + * @default 'safe_fix' + */ + fixKind: FixKind; + /** * Additional flags to pass to the LSP binary * `oxc.flags` @@ -83,7 +106,8 @@ export class WorkspaceConfig { private _runTrigger: Trigger = 'onType'; private _unusedDisableDirectives: UnusedDisableDirectives = 'allow'; private _typeAware: boolean = false; - private _flags: Record = {}; + private _disableNestedConfig: boolean = false; + private _fixKind: FixKind = FixKind.SafeFix; private _formattingExperimental: boolean = false; private _formattingConfigPath: string | null = null; @@ -97,16 +121,28 @@ export class WorkspaceConfig { public refresh(): void { const flags = this.configuration.get>('flags') ?? {}; - const useNestedConfigs = !('disable_nested_config' in flags); + + // `configuration.get` takes the default value from the package.json, which is always `safe_fix`. + // We need to check the deprecated flags.fix_kind for the real default value. + let fixKind = this.configuration.get('fixKind'); + if (fixKind === FixKind.SafeFix && flags.fix_kind !== undefined && flags.fix_kind !== 'safe_fix') { + fixKind = flags.fix_kind as FixKind; + } + + // the same for disabledNestedConfig + let disableNestedConfig = this.configuration.get('disableNestedConfig'); + if (disableNestedConfig === false && flags.disable_nested_config === 'true') { + disableNestedConfig = true; + } this._runTrigger = this.configuration.get('lint.run') || 'onType'; - this._configPath = - this.configuration.get('configPath') || (useNestedConfigs ? null : oxlintConfigFileName); + this._configPath = this.configuration.get('configPath') ?? null; this._tsConfigPath = this.configuration.get('tsConfigPath') ?? null; this._unusedDisableDirectives = this.configuration.get('unusedDisableDirectives') ?? 'allow'; this._typeAware = this.configuration.get('typeAware') ?? false; - this._flags = flags; + this._disableNestedConfig = disableNestedConfig ?? false; + this._fixKind = fixKind ?? FixKind.SafeFix; this._formattingExperimental = this.configuration.get('fmt.experimental') ?? false; this._formattingConfigPath = this.configuration.get('fmt.configPath') ?? null; } @@ -127,7 +163,10 @@ export class WorkspaceConfig { if (event.affectsConfiguration(`${ConfigService.namespace}.typeAware`, this.workspace)) { return true; } - if (event.affectsConfiguration(`${ConfigService.namespace}.flags`, this.workspace)) { + if (event.affectsConfiguration(`${ConfigService.namespace}.disableNestedConfig`, this.workspace)) { + return true; + } + if (event.affectsConfiguration(`${ConfigService.namespace}.fixKind`, this.workspace)) { return true; } if (event.affectsConfiguration(`${ConfigService.namespace}.fmt.experimental`, this.workspace)) { @@ -136,6 +175,10 @@ export class WorkspaceConfig { if (event.affectsConfiguration(`${ConfigService.namespace}.fmt.configPath`, this.workspace)) { return true; } + // deprecated settings in flags + if (event.affectsConfiguration(`${ConfigService.namespace}.flags`, this.workspace)) { + return true; + } return false; } @@ -188,13 +231,22 @@ export class WorkspaceConfig { return this.configuration.update('typeAware', value, ConfigurationTarget.WorkspaceFolder); } - get flags(): Record { - return this._flags; + get disableNestedConfig(): boolean { + return this._disableNestedConfig; + } + + updateDisableNestedConfig(value: boolean): PromiseLike { + this._disableNestedConfig = value; + return this.configuration.update('disableNestedConfig', value, ConfigurationTarget.WorkspaceFolder); + } + + get fixKind(): FixKind { + return this._fixKind; } - updateFlags(value: Record): PromiseLike { - this._flags = value; - return this.configuration.update('flags', value, ConfigurationTarget.WorkspaceFolder); + updateFixKind(value: FixKind): PromiseLike { + this._fixKind = value; + return this.configuration.update('fixKind', value, ConfigurationTarget.WorkspaceFolder); } get formattingExperimental(): boolean { @@ -222,9 +274,15 @@ export class WorkspaceConfig { tsConfigPath: this.tsConfigPath ?? null, unusedDisableDirectives: this.unusedDisableDirectives, typeAware: this.typeAware, - flags: this.flags, + disableNestedConfig: this.disableNestedConfig, + fixKind: this.fixKind, ['fmt.experimental']: this.formattingExperimental, ['fmt.configPath']: this.formattingConfigPath ?? null, + // deprecated, kept for backward compatibility + flags: { + disable_nested_config: this.disableNestedConfig ? 'true' : 'false', + ...(this.fixKind ? { fix_kind: this.fixKind } : {}), + }, }; } } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 83d11118c24bd..4499408f13fe5 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -147,10 +147,39 @@ "default": false, "description": "Enable type-aware linting." }, + "oxc.disableNestedConfig": { + "type": "boolean", + "scope": "resource", + "default": false, + "description": "Disable searching for nested configuration files. When set to true, only the configuration file specified in `oxc.configPath` (if any) will be used." + }, + "oxc.fixKind": { + "type": "string", + "scope": "resource", + "enum": [ + "safe_fix", + "safe_fix_or_suggestion", + "dangerous_fix", + "dangerous_fix_or_suggestion", + "none", + "all" + ], + "enumDescriptions": [ + "Only safe fixes will be applied", + "Safe fixes or suggestions will be applied", + "Safe and dangerous fixes will be applied", + "Safe and dangerous fixes or suggestions will be applied", + "No fixes will be applied", + "All fixes and suggestions will be applied" + ], + "default": "safe_fix", + "description": "Specify the kind of fixes to suggest/apply." + }, "oxc.flags": { "type": "object", "scope": "resource", "default": {}, + "deprecationMessage": "deprecated since 1.25.0, use `oxc.fixKind` or `oxc.disableNestedConfig` instead.", "description": "Specific Oxlint flags to pass to the language server." }, "oxc.fmt.experimental": { diff --git a/editors/vscode/tests/WorkspaceConfig.spec.ts b/editors/vscode/tests/WorkspaceConfig.spec.ts index 359be06462f4d..6919c1a12cd58 100644 --- a/editors/vscode/tests/WorkspaceConfig.spec.ts +++ b/editors/vscode/tests/WorkspaceConfig.spec.ts @@ -1,30 +1,42 @@ -import { deepStrictEqual, strictEqual } from 'assert'; +import { strictEqual } from 'assert'; import { ConfigurationTarget, workspace } from 'vscode'; -import { WorkspaceConfig } from '../client/WorkspaceConfig.js'; +import { FixKind, WorkspaceConfig } from '../client/WorkspaceConfig.js'; import { WORKSPACE_FOLDER } from './test-helpers.js'; -const keys = ['lint.run', 'configPath', 'tsConfigPath', 'flags', 'unusedDisableDirectives', 'typeAware', 'fmt.experimental', 'fmt.configPath']; +const keys = [ + 'lint.run', + 'configPath', + 'tsConfigPath', + 'unusedDisableDirectives', + 'typeAware', + 'disableNestedConfig', + 'fixKind', + 'fmt.experimental', + 'fmt.configPath', + // deprecated + 'flags' +]; suite('WorkspaceConfig', () => { - setup(async () => { + + const updateConfiguration = async (key: string, value: unknown) => { const workspaceConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER); const globalConfig = workspace.getConfiguration('oxc'); + await Promise.all([ + workspaceConfig.update(key, value, ConfigurationTarget.WorkspaceFolder), + // VSCode will not save different workspace configuration inside a `.code-workspace` file. + // Do not fail, we will make sure the global config is empty too. + globalConfig.update(key, value) + ]); + }; - await Promise.all(keys.map(key => workspaceConfig.update(key, undefined, ConfigurationTarget.WorkspaceFolder))); - // VSCode will not save different workspace configuration inside a `.code-workspace` file. - // Do not fail, we will make sure the global config is empty too. - await Promise.all(keys.map(key => globalConfig.update(key, undefined))); + setup(async () => { + await Promise.all(keys.map(key => updateConfiguration(key, undefined))); }); teardown(async () => { - const workspaceConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER); - const globalConfig = workspace.getConfiguration('oxc'); - - await Promise.all(keys.map(key => workspaceConfig.update(key, undefined, ConfigurationTarget.WorkspaceFolder))); - // VSCode will not save different workspace configuration inside a `.code-workspace` file. - // Do not fail, we will make sure the global config is empty too. - await Promise.all(keys.map(key => globalConfig.update(key, undefined))); + await Promise.all(keys.map(key => updateConfiguration(key, undefined))); }); test('default values on initialization', () => { @@ -34,31 +46,21 @@ suite('WorkspaceConfig', () => { strictEqual(config.tsConfigPath, null); strictEqual(config.unusedDisableDirectives, 'allow'); strictEqual(config.typeAware, false); - deepStrictEqual(config.flags, {}); + strictEqual(config.disableNestedConfig, false); + strictEqual(config.fixKind, "safe_fix"); strictEqual(config.formattingExperimental, false); strictEqual(config.formattingConfigPath, null); }); - test('configPath defaults to null when using nested configs and configPath is empty', async () => { - const wsConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER); - await wsConfig.update('configPath', '', ConfigurationTarget.WorkspaceFolder); - await wsConfig.update('flags', {}, ConfigurationTarget.WorkspaceFolder); + test('deprecated values are respected', async () => { + await updateConfiguration('flags', { + disable_nested_config: 'true', + fix_kind: 'dangerous_fix' + }); const config = new WorkspaceConfig(WORKSPACE_FOLDER); - - deepStrictEqual(config.flags, {}); - strictEqual(config.configPath, null); - }); - - test('configPath defaults to .oxlintrc.json when not using nested configs and configPath is empty', async () => { - const wsConfig = workspace.getConfiguration('oxc', WORKSPACE_FOLDER); - await wsConfig.update('configPath', undefined, ConfigurationTarget.WorkspaceFolder); - await wsConfig.update('flags', { disable_nested_config: '' }, ConfigurationTarget.WorkspaceFolder); - - const config = new WorkspaceConfig(WORKSPACE_FOLDER); - - deepStrictEqual(config.flags, { disable_nested_config: '' }); - strictEqual(config.configPath, '.oxlintrc.json'); + strictEqual(config.disableNestedConfig, true); + strictEqual(config.fixKind, "dangerous_fix"); }); test('updating values updates the workspace configuration', async () => { @@ -70,7 +72,8 @@ suite('WorkspaceConfig', () => { config.updateTsConfigPath('./tsconfig.json'), config.updateUnusedDisableDirectives('deny'), config.updateTypeAware(true), - config.updateFlags({ test: 'value' }), + config.updateDisableNestedConfig(true), + config.updateFixKind(FixKind.DangerousFix), config.updateFormattingExperimental(true), config.updateFormattingConfigPath('./oxfmt.json'), ]); @@ -82,7 +85,8 @@ suite('WorkspaceConfig', () => { strictEqual(wsConfig.get('tsConfigPath'), './tsconfig.json'); strictEqual(wsConfig.get('unusedDisableDirectives'), 'deny'); strictEqual(wsConfig.get('typeAware'), true); - deepStrictEqual(wsConfig.get('flags'), { test: 'value' }); + strictEqual(wsConfig.get('disableNestedConfig'), true); + strictEqual(wsConfig.get('fixKind'), 'dangerous_fix'); strictEqual(wsConfig.get('fmt.experimental'), true); strictEqual(wsConfig.get('fmt.configPath'), './oxfmt.json'); }); diff --git a/editors/vscode/tests/code_actions.spec.ts b/editors/vscode/tests/code_actions.spec.ts index c01c806cc92cf..fc6694df401fc 100644 --- a/editors/vscode/tests/code_actions.spec.ts +++ b/editors/vscode/tests/code_actions.spec.ts @@ -29,8 +29,7 @@ teardown(async () => { const vsConfig = workspace.getConfiguration('oxc'); const wsConfig = workspace.getConfiguration('oxc', fixturesWorkspaceUri()); await vsConfig.update('unusedDisableDirectives', undefined); - await wsConfig.update('flags', undefined, ConfigurationTarget.WorkspaceFolder); - + await wsConfig.update('fixKind', undefined, ConfigurationTarget.WorkspaceFolder); await workspace.getConfiguration('editor').update('codeActionsOnSave', undefined); await workspace.saveAll(); }); @@ -154,9 +153,7 @@ suite('code actions', () => { ); strictEqual(quickFixesNoFix.length, 2); - await workspace.getConfiguration('oxc', fixturesWorkspaceUri()).update('flags', { - 'fix_kind': 'dangerous_fix', - }, ConfigurationTarget.WorkspaceFolder); + await workspace.getConfiguration('oxc', fixturesWorkspaceUri()).update('fixKind', 'dangerous_fix', ConfigurationTarget.WorkspaceFolder); await workspace.saveAll(); const codeActionsWithFix: ProviderResult> = await commands.executeCommand( diff --git a/editors/vscode/tests/e2e_server.spec.ts b/editors/vscode/tests/e2e_server.spec.ts index 357e8b71a347d..d155c7143e708 100644 --- a/editors/vscode/tests/e2e_server.spec.ts +++ b/editors/vscode/tests/e2e_server.spec.ts @@ -32,7 +32,7 @@ suiteSetup(async () => { }); teardown(async () => { - await workspace.getConfiguration('oxc').update('flags', undefined); + await workspace.getConfiguration('oxc').update('fixKind', undefined); await workspace.getConfiguration('oxc').update('tsConfigPath', undefined); await workspace.getConfiguration('oxc').update('typeAware', undefined); await workspace.getConfiguration('oxc').update('fmt.experimental', undefined); @@ -137,7 +137,7 @@ suite('E2E Diagnostics', () => { // We can check the changed with kind with `vscode.executeCodeActionProvider` // but to be safe that everything works, we will check the applied changes. // This way we can be sure that everything works as expected. - test('auto detect changing `fix_kind` flag with fixAll command', async () => { + test('auto detect changing `fixKind` with fixAll command', async () => { const originalContent = 'if (foo == null) { bar();}'; await createOxlintConfiguration({ @@ -163,9 +163,7 @@ suite('E2E Diagnostics', () => { const content = await workspace.fs.readFile(fileUri); strictEqual(content.toString(), originalContent); - await workspace.getConfiguration('oxc').update('flags', { - fix_kind: 'all', - }); + await workspace.getConfiguration('oxc').update('fixKind', 'all'); // wait for server to update the internal linter await sleep(500); await workspace.saveAll();