Skip to content

Commit

Permalink
fix: quickfix code action regression + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
spence-s committed Jan 24, 2024
1 parent 6a7dab0 commit 8b331fb
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 26 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -43,7 +43,7 @@
"lint": "xo && npm run lint:md",
"lint:md": "prettier --parser markdown '**/*.md' --check",
"prepare": "husky install",
"test": "NODE_NO_WARNINGS=1 node --trace-warnings --require tsx/cjs --test test/index.ts | tap-mocha-reporter spec",
"test": "NODE_NO_WARNINGS=1 node --require tsx/cjs --test test/index.ts | tap-mocha-reporter spec",
"test:coverage": "NODE_NO_WARNINGS=1 c8 node --require tsx/cjs --test test/index.ts",
"test:watch": "NODE_NO_WARNINGS=1 node --require tsx/cjs --watch --test test/index.ts",
"vscode:prepublish": "npm run check && npm run build"
Expand Down
45 changes: 24 additions & 21 deletions server/code-actions-builder.ts
@@ -1,5 +1,6 @@
import {
TextEdit,
uinteger,
Range,
Position,
CodeActionKind,
Expand All @@ -22,36 +23,38 @@ export class QuickFixCodeActionsBuilder {
}

build(): CodeAction[] {
return this.diagnostics.flatMap<CodeAction>((diagnostic) => {
const diagnosticCodeActions: CodeAction[] = [];
return this.diagnostics
.filter((diagnostic) => diagnostic.source === 'XO')
.flatMap<CodeAction>((diagnostic) => {
const diagnosticCodeActions: CodeAction[] = [];

const disableSameLineCodeAction = this.getDisableSameLine(diagnostic);
if (disableSameLineCodeAction) diagnosticCodeActions.push(disableSameLineCodeAction);
const disableSameLineCodeAction = this.getDisableSameLine(diagnostic);
if (disableSameLineCodeAction) diagnosticCodeActions.push(disableSameLineCodeAction);

const disableNextLineCodeAction = this.getDisableNextLine(diagnostic);
if (disableNextLineCodeAction) diagnosticCodeActions.push(disableNextLineCodeAction);
const disableNextLineCodeAction = this.getDisableNextLine(diagnostic);
if (disableNextLineCodeAction) diagnosticCodeActions.push(disableNextLineCodeAction);

const disableFileCodeAction = this.getDisableEntireFile(diagnostic);
if (disableFileCodeAction) diagnosticCodeActions.push(disableFileCodeAction);
const disableFileCodeAction = this.getDisableEntireFile(diagnostic);
if (disableFileCodeAction) diagnosticCodeActions.push(disableFileCodeAction);

const fix = this.getFix(diagnostic, CodeActionKind.QuickFix);
if (fix) diagnosticCodeActions.push(fix);
const fix = this.getFix(diagnostic, CodeActionKind.QuickFix);
if (fix) diagnosticCodeActions.push(fix);

return diagnosticCodeActions;
});
return diagnosticCodeActions;
});
}

getDisableSameLine(diagnostic: Diagnostic) {
let changes = [];

const startPosition: Position = {
line: diagnostic.range.start.line,
character: Number.MAX_SAFE_INTEGER
character: uinteger.MAX_VALUE
};

const lineText = this.textDocument.getText({
start: Position.create(diagnostic.range.start.line, 0),
end: Position.create(diagnostic.range.start.line, Number.MAX_SAFE_INTEGER)
end: Position.create(diagnostic.range.start.line, uinteger.MAX_VALUE)
});

const matchedForIgnoreComment = lineText && /\/\/ eslint-disable-line/.exec(lineText);
Expand All @@ -75,7 +78,7 @@ export class QuickFixCodeActionsBuilder {
}

const ignoreAction: CodeAction = {
title: `Add Ignore Rule Same Line ${diagnostic.code}`,
title: `Add Ignore Rule ${diagnostic.code}: Same Line`,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
edit: {
Expand All @@ -98,20 +101,20 @@ export class QuickFixCodeActionsBuilder {

const lineText = this.textDocument.getText({
start: Position.create(diagnostic.range.start.line, 0),
end: Position.create(diagnostic.range.start.line, Number.MAX_SAFE_INTEGER)
end: Position.create(diagnostic.range.start.line, uinteger.MAX_VALUE)
});

const lineAboveText = this.textDocument.getText({
start: Position.create(diagnostic.range.start.line - 1, 0),
end: Position.create(diagnostic.range.start.line - 1, Number.MAX_SAFE_INTEGER)
end: Position.create(diagnostic.range.start.line - 1, uinteger.MAX_VALUE)
});

const matchedForIgnoreComment =
lineAboveText && /\/\/ eslint-disable-next-line/.exec(lineAboveText);

if (matchedForIgnoreComment && matchedForIgnoreComment.length > 0) {
const textEdit = TextEdit.insert(
Position.create(diagnostic.range.start.line - 1, Number.MAX_SAFE_INTEGER),
Position.create(diagnostic.range.start.line - 1, uinteger.MAX_VALUE),
`, ${diagnostic.code}`
);

Expand All @@ -135,7 +138,7 @@ export class QuickFixCodeActionsBuilder {
}

const ignoreAction: CodeAction = {
title: `Add Ignore Rule Line Above ${diagnostic.code}`,
title: `Add Ignore Rule ${diagnostic.code}: Next Line`,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
edit: {
Expand All @@ -156,7 +159,7 @@ export class QuickFixCodeActionsBuilder {
const line = shebang === '#!' ? 1 : 0;

const ignoreFileAction = {
title: `Add Ignore Rule ${diagnostic.code} for entire file`,
title: `Add Ignore Rule ${diagnostic.code}: Entire File`,
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
edit: {
Expand All @@ -177,7 +180,7 @@ export class QuickFixCodeActionsBuilder {
if (!edit) return;

return {
title: 'Fix with XO',
title: `Fix ${diagnostic.code} with XO`,
kind: codeActionKind,
diagnostics: [diagnostic],
edit: {
Expand Down
2 changes: 1 addition & 1 deletion server/server.ts
Expand Up @@ -360,7 +360,7 @@ class LintServer {
codeActions.push(codeAction);
}

if (context.only?.includes(CodeActionKind.QuickFix)) {
if (!context.only || context.only?.includes(CodeActionKind.QuickFix)) {
const codeActionBuilder = new QuickFixCodeActionsBuilder(
document,
context.diagnostics,
Expand Down
69 changes: 66 additions & 3 deletions test/lsp/code-actions.ts
@@ -1,15 +1,23 @@
import {test, describe, mock, type Mock} from 'node:test';
import {setTimeout} from 'node:timers/promises';
import assert from 'node:assert';
import {setTimeout} from 'node:timers';
import {TextDocument} from 'vscode-languageserver-textdocument';
import {
Position,
Diagnostic,
CodeActionKind,
type CodeActionParams,
type Range,
type TextDocumentIdentifier
} from 'vscode-languageserver';
import Server from '../../server/server.js';
import {
getCodeActionParams,
getIgnoreSameLineCodeAction,
getIgnoreNextLineCodeAction,
getIgnoreFileCodeAction,
getTextDocument
} from '../stubs.js';

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
Expand Down Expand Up @@ -47,7 +55,7 @@ describe('Server code actions', async () => {
assert.equal(typeof server.handleCodeActionRequest, 'function');
});

test('Server.handleCodeActionRequest returns an empty array if no code actions are available', async (t) => {
await test('Server.handleCodeActionRequest returns an empty array if no code actions are available', async (t) => {
const textDocument: TextDocumentIdentifier = {uri: 'uri'};
const range: Range = {start: Position.create(0, 0), end: Position.create(0, 0)};
const mockCodeActionParams: CodeActionParams = {
Expand All @@ -59,7 +67,7 @@ describe('Server code actions', async () => {
assert.deepEqual(codeActions, []);
});

test('codeActionKind source.fixAll calls getDocumentFormatting for the document', async (t) => {
await test('codeActionKind source.fixAll calls getDocumentFormatting for the document', async (t) => {
const textDocument: TextDocumentIdentifier = {uri: 'uri'};
const range: Range = {start: Position.create(0, 0), end: Position.create(0, 0)};
const mockCodeActionParams: CodeActionParams = {
Expand All @@ -78,4 +86,59 @@ describe('Server code actions', async () => {
assert.equal(server.getDocumentFormatting.mock.callCount(), 1);
assert.deepEqual(server.getDocumentFormatting.mock.calls[0].arguments, ['uri']);
});

await test('codeActionKind only source.quickfix does not call getDocumentFormatting for the document', async (t) => {
const textDocument: TextDocumentIdentifier = {uri: 'uri'};
const range: Range = {start: Position.create(0, 0), end: Position.create(0, 0)};
const mockCodeActionParams: CodeActionParams = {
textDocument,
range,
context: {
diagnostics: [Diagnostic.create(range, 'test message', 1, 'test', 'test')],
only: ['source.quickfix']
}
};
const codeActions = await server.handleCodeActionRequest(mockCodeActionParams);

assert.deepEqual(codeActions, []);
assert.equal(server.getDocumentFormatting.mock.callCount(), 0);
});

await test('codeAction without "only" does not call getDocumentFormatting for the document', async (t) => {
const textDocument: TextDocumentIdentifier = {uri: 'uri'};
const range: Range = {start: Position.create(0, 0), end: Position.create(0, 0)};
const mockCodeActionParams: CodeActionParams = {
textDocument,
range,
context: {
diagnostics: [Diagnostic.create(range, 'test message', 1, 'test', 'test')]
}
};
const codeActions = await server.handleCodeActionRequest(mockCodeActionParams);

assert.deepEqual(codeActions, []);
assert.equal(server.getDocumentFormatting.mock.callCount(), 0);
});

await test('codeAction without "only" produces quickfix code actions', async (t) => {
const codeActions = await server.handleCodeActionRequest(getCodeActionParams());

assert.deepStrictEqual(codeActions, [
getIgnoreSameLineCodeAction(),
getIgnoreNextLineCodeAction(),
getIgnoreFileCodeAction()
]);
});

await test('codeAction with only quickfix produces quickfix code actions', async (t) => {
const params = getCodeActionParams();
params.context.only = [CodeActionKind.QuickFix];
const codeActions = await server.handleCodeActionRequest(params);

assert.deepStrictEqual(codeActions, [
getIgnoreSameLineCodeAction(),
getIgnoreNextLineCodeAction(),
getIgnoreFileCodeAction()
]);
});
});
78 changes: 78 additions & 0 deletions test/stubs.ts
@@ -0,0 +1,78 @@
import {
CodeAction,
Diagnostic,
Range,
Position,
DiagnosticSeverity,
CodeActionKind,
uinteger,
type CodeActionParams
} from 'vscode-languageserver';

export const getTextDocument = () => ({uri: 'uri'});
export const getZeroPosition = () => Position.create(0, 0);
export const getZeroRange = () => Range.create(getZeroPosition(), getZeroPosition());
export const getXoDiagnostic = () =>
Diagnostic.create(getZeroRange(), 'test message', DiagnosticSeverity.Error, 'test', 'XO');
export const getCodeActionParams = (): CodeActionParams => ({
textDocument: getTextDocument(),
range: getZeroRange(),
context: {diagnostics: [getXoDiagnostic()]}
});

export const getIgnoreSameLineCodeAction = () => ({
...CodeAction.create(
'Add Ignore Rule test: Same Line',
{
changes: {
uri: [
{
range: Range.create(
Position.create(0, uinteger.MAX_VALUE),
Position.create(0, uinteger.MAX_VALUE)
),
newText: ' // eslint-disable-line test'
}
]
}
},
CodeActionKind.QuickFix
),
diagnostics: [getXoDiagnostic()]
});

export const getIgnoreNextLineCodeAction = () => ({
...CodeAction.create(
'Add Ignore Rule test: Next Line',
{
changes: {
uri: [
{
range: getZeroRange(),
newText: '// eslint-disable-next-line test\n'
}
]
}
},
CodeActionKind.QuickFix
),
diagnostics: [getXoDiagnostic()]
});

export const getIgnoreFileCodeAction = () => ({
...CodeAction.create(
'Add Ignore Rule test: Entire File',
{
changes: {
uri: [
{
range: getZeroRange(),
newText: '/* eslint-disable test */\n'
}
]
}
},
CodeActionKind.QuickFix
),
diagnostics: [getXoDiagnostic()]
});

0 comments on commit 8b331fb

Please sign in to comment.