From dd1f805ecdb8312ab5e0f6c6ff7c8fd0a7c49320 Mon Sep 17 00:00:00 2001 From: Thomas Faivre Date: Sat, 16 Dec 2023 02:02:55 +0100 Subject: [PATCH] feat(shellcheck): add code actions to disable rule Implements #933. Always add a code action to disable a given diagnostic for a given line or for the whole file. This patch handles: * indentation * existing directive. It also sorts the disable directives if any * multi-line command Add corresponding tests. Signed-off-by: Thomas Faivre --- server/src/__tests__/server.test.ts | 2 +- server/src/server.ts | 2 +- server/src/shellcheck/__tests__/index.test.ts | 2063 ++++++++++++++++- server/src/shellcheck/index.ts | 276 ++- 4 files changed, 2274 insertions(+), 69 deletions(-) diff --git a/server/src/__tests__/server.test.ts b/server/src/__tests__/server.test.ts index e6b88d66..b147e06a 100644 --- a/server/src/__tests__/server.test.ts +++ b/server/src/__tests__/server.test.ts @@ -222,7 +222,7 @@ describe('server', () => { {} as any, ) - expect(result).toHaveLength(1) + expect(result).toHaveLength(3) const codeAction = (result as CodeAction[])[0] expect(codeAction.diagnostics).toEqual([fixableDiagnostic]) expect(codeAction.diagnostics).toEqual([fixableDiagnostic]) diff --git a/server/src/server.ts b/server/src/server.ts index 8b70dc25..390b7f02 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -405,7 +405,7 @@ export default class BashServer { const codeActions = params.context.diagnostics .map(({ data }) => codeActionsForUri[data?.id]) - .filter((action): action is LSP.CodeAction => action != null) + .flat() logger.debug(`onCodeAction: found ${codeActions.length} code action(s)`) diff --git a/server/src/shellcheck/__tests__/index.test.ts b/server/src/shellcheck/__tests__/index.test.ts index c2ad2a69..6a625876 100644 --- a/server/src/shellcheck/__tests__/index.test.ts +++ b/server/src/shellcheck/__tests__/index.test.ts @@ -77,67 +77,267 @@ describe('linter', () => { expect(result).toMatchInlineSnapshot(` { "codeActions": { - "shellcheck|2086|1:5-1:9": { - "diagnostics": [ - { - "code": "SC2086", - "codeDescription": { - "href": "https://www.shellcheck.net/wiki/SC2086", + "shellcheck|2086|1:5-1:9": [ + { + "diagnostics": [ + { + "code": "SC2086", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2086", + }, + "data": { + "id": "shellcheck|2086|1:5-1:9", + }, + "message": "Double quote to prevent globbing and word splitting.", + "range": { + "end": { + "character": 9, + "line": 1, + }, + "start": { + "character": 5, + "line": 1, + }, + }, + "severity": 3, + "source": "shellcheck", + "tags": undefined, }, - "data": { - "id": "shellcheck|2086|1:5-1:9", + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": """, + "range": { + "end": { + "character": 9, + "line": 1, + }, + "start": { + "character": 9, + "line": 1, + }, + }, + }, + { + "newText": """, + "range": { + "end": { + "character": 5, + "line": 1, + }, + "start": { + "character": 5, + "line": 1, + }, + }, + }, + ], }, - "message": "Double quote to prevent globbing and word splitting.", - "range": { - "end": { - "character": 9, - "line": 1, + }, + "kind": "quickfix", + "title": "Apply fix for SC2086", + }, + { + "diagnostics": [ + { + "code": "SC2086", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2086", + }, + "data": { + "id": "shellcheck|2086|1:5-1:9", }, - "start": { - "character": 5, - "line": 1, + "message": "Double quote to prevent globbing and word splitting.", + "range": { + "end": { + "character": 9, + "line": 1, + }, + "start": { + "character": 5, + "line": 1, + }, }, + "severity": 3, + "source": "shellcheck", + "tags": undefined, }, - "severity": 3, - "source": "shellcheck", - "tags": undefined, - }, - ], - "edit": { - "changes": { - "${FIXTURE_DOCUMENT_URI}": [ - { - "newText": """, - "range": { - "end": { - "character": 9, - "line": 1, - }, - "start": { - "character": 9, - "line": 1, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2086 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, }, }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2086 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2086", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2086", + }, + "data": { + "id": "shellcheck|2086|1:5-1:9", }, - { - "newText": """, - "range": { - "end": { - "character": 5, - "line": 1, + "message": "Double quote to prevent globbing and word splitting.", + "range": { + "end": { + "character": 9, + "line": 1, + }, + "start": { + "character": 5, + "line": 1, + }, + }, + "severity": 3, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2086 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, }, - "start": { - "character": 5, - "line": 1, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2086 for the entire file", + }, + ], + "shellcheck|2154|1:5-1:9": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:5-1:9", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 1, + }, + "start": { + "character": 5, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, }, }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:5-1:9", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 1, + }, + "start": { + "character": 5, + "line": 1, + }, }, - ], + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", }, - "kind": "quickfix", - "title": "Apply fix for SC2086", - }, + ], }, "diagnostics": [ { @@ -211,27 +411,1766 @@ describe('linter', () => { }) }) - it('should correctly follow sources with correct cwd', async () => { - const [result] = await getLintingResult({ - cwd: FIXTURE_FOLDER, - document: FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, - }) + it('should handle indentation in disable directive', async () => { + // prettier-ignore + const shell = [ + '#!/bin/bash', + '', + 'if true; then', + ' echo "$foo"', + 'fi', + ].join('\n') - expect(result).toEqual({ - codeActions: {}, - diagnostics: [], - }) + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|3:10-3:14": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|3:10-3:14", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 14, + "line": 3, + }, + "start": { + "character": 10, + "line": 3, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": " # shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 3, + }, + "start": { + "character": 0, + "line": 3, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|3:10-3:14", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 14, + "line": 3, + }, + "start": { + "character": 10, + "line": 3, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|3:10-3:14", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 14, + "line": 3, + }, + "start": { + "character": 10, + "line": 3, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) }) - it('should fail to follow sources with incorrect cwd', async () => { - const [result] = await getLintingResult({ - cwd: path.resolve(path.join(FIXTURE_FOLDER, '../')), - document: FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, - }) + it('should add disable directive to existing ones', async () => { + // prettier-ignore + const shell = [ + '#!/bin/bash', + '# shellcheck disable=SC1001', + '', + '# shellcheck disable=SC1002', + 'echo "$foo"', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|4:6-4:10": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:6-4:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 4, + }, + "start": { + "character": 6, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC1002,SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 4, + }, + "start": { + "character": 0, + "line": 3, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:6-4:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 4, + }, + "start": { + "character": 6, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC1001,SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 2, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:6-4:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 4, + }, + "start": { + "character": 6, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should add indented disable directive to existing ones', async () => { + // prettier-ignore + const shell = [ + '#!/bin/bash', + '# shellcheck disable=SC1001', + '', + 'if true; then', + ' # shellcheck disable=SC1002', + ' echo "$foo"', + 'fi', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|5:10-5:14": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:10-5:14", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 14, + "line": 5, + }, + "start": { + "character": 10, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": " # shellcheck disable=SC1002,SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 5, + }, + "start": { + "character": 0, + "line": 4, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:10-5:14", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 14, + "line": 5, + }, + "start": { + "character": 10, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC1001,SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 2, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:10-5:14", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 14, + "line": 5, + }, + "start": { + "character": 10, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + it('should add disable directive to existing ones (sorted)', async () => { + // prettier-ignore + const shell = [ + '#!/bin/bash', + '# shellcheck disable=SC9001,SC1001', + '', + '# shellcheck disable=SC1002,SC9002', + 'echo "$foo"', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) expect(result).toMatchInlineSnapshot(` { - "codeActions": {}, + "codeActions": { + "shellcheck|2154|4:6-4:10": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:6-4:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 4, + }, + "start": { + "character": 6, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC1002,SC2154,SC9002 + ", + "range": { + "end": { + "character": 0, + "line": 4, + }, + "start": { + "character": 0, + "line": 3, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:6-4:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 4, + }, + "start": { + "character": 6, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC1001,SC2154,SC9001 + ", + "range": { + "end": { + "character": 0, + "line": 2, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:6-4:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 4, + }, + "start": { + "character": 6, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should handle multiline in disable directive', async () => { + // prettier-ignore + const shell = [ + '#!/bin/bash', + '', + 'echo \\', + ' "$foo" \\', + ' "bar" \\', + ' "$baz"', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|3:5-3:9": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|3:5-3:9", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 3, + }, + "start": { + "character": 5, + "line": 3, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 2, + }, + "start": { + "character": 0, + "line": 2, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|3:5-3:9", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 3, + }, + "start": { + "character": 5, + "line": 3, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + "shellcheck|2154|5:5-5:9": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:5-5:9", + }, + "message": "baz is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 5, + }, + "start": { + "character": 5, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 2, + }, + "start": { + "character": 0, + "line": 2, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:5-5:9", + }, + "message": "baz is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 5, + }, + "start": { + "character": 5, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|3:5-3:9", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 3, + }, + "start": { + "character": 5, + "line": 3, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:5-5:9", + }, + "message": "baz is referenced but not assigned.", + "range": { + "end": { + "character": 9, + "line": 5, + }, + "start": { + "character": 5, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should append indented multiline in disable directive', async () => { + // prettier-ignore + const shell = [ + '#!/bin/bash', + '', + 'if true; then', + ' echo \\', + ' "$foo" \\', + ' "bar" \\', + ' "$baz"', + 'fi', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|4:9-4:13": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:9-4:13", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 13, + "line": 4, + }, + "start": { + "character": 9, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": " # shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 3, + }, + "start": { + "character": 0, + "line": 3, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:9-4:13", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 13, + "line": 4, + }, + "start": { + "character": 9, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + "shellcheck|2154|6:9-6:13": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|6:9-6:13", + }, + "message": "baz is referenced but not assigned.", + "range": { + "end": { + "character": 13, + "line": 6, + }, + "start": { + "character": 9, + "line": 6, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": " # shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 3, + }, + "start": { + "character": 0, + "line": 3, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|6:9-6:13", + }, + "message": "baz is referenced but not assigned.", + "range": { + "end": { + "character": 13, + "line": 6, + }, + "start": { + "character": 9, + "line": 6, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|4:9-4:13", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 13, + "line": 4, + }, + "start": { + "character": 9, + "line": 4, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|6:9-6:13", + }, + "message": "baz is referenced but not assigned.", + "range": { + "end": { + "character": 13, + "line": 6, + }, + "start": { + "character": 9, + "line": 6, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should handle absence of shebang', async () => { + // prettier-ignore + const shell = [ + '', + 'echo "$foo"', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|1:6-1:10": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:6-1:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 1, + }, + "start": { + "character": 6, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:6-1:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 1, + }, + "start": { + "character": 6, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 0, + }, + "start": { + "character": 0, + "line": 0, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:6-1:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 1, + }, + "start": { + "character": 6, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should handle error on first line', async () => { + // prettier-ignore + const shell = [ + 'echo "$foo"', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|0:6-0:10": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|0:6-0:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 0, + }, + "start": { + "character": 6, + "line": 0, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 0, + }, + "start": { + "character": 0, + "line": 0, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|0:6-0:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 0, + }, + "start": { + "character": 6, + "line": 0, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 0, + }, + "start": { + "character": 0, + "line": 0, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|0:6-0:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 0, + }, + "start": { + "character": 6, + "line": 0, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should handle existing directive', async () => { + // prettier-ignore + const shell = [ + '# shellcheck enable=bar', + 'echo "$foo"', + ].join('\n') + + const [result] = await getLintingResult({ document: textToDoc(shell) }) + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|2154|1:6-1:10": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:6-1:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 1, + }, + "start": { + "character": 6, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck enable=bar disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 0, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:6-1:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 1, + }, + "start": { + "character": 6, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures//foo.sh": [ + { + "newText": "# shellcheck enable=bar disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 0, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|1:6-1:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 1, + }, + "start": { + "character": 6, + "line": 1, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + } + `) + }) + + it('should correctly follow sources with correct cwd', async () => { + const [result] = await getLintingResult({ + cwd: FIXTURE_FOLDER, + document: FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, + }) + + expect(result).toEqual({ + codeActions: {}, + diagnostics: [], + }) + }) + + it('should fail to follow sources with incorrect cwd', async () => { + const [result] = await getLintingResult({ + cwd: path.resolve(path.join(FIXTURE_FOLDER, '../')), + document: FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, + }) + + expect(result).toMatchInlineSnapshot(` + { + "codeActions": { + "shellcheck|1091|3:7-3:19": [ + { + "diagnostics": [ + { + "code": "SC1091", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC1091", + }, + "data": { + "id": "shellcheck|1091|3:7-3:19", + }, + "message": "Not following: shellcheck/sourced.sh: openBinaryFile: does not exist (No such file or directory)", + "range": { + "end": { + "character": 19, + "line": 3, + }, + "start": { + "character": 7, + "line": 3, + }, + }, + "severity": 3, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures/shellcheck/source.sh": [ + { + "newText": "# shellcheck source=shellcheck/sourced.sh disable=SC1091 + ", + "range": { + "end": { + "character": 0, + "line": 3, + }, + "start": { + "character": 0, + "line": 2, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC1091 for this line", + }, + { + "diagnostics": [ + { + "code": "SC1091", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC1091", + }, + "data": { + "id": "shellcheck|1091|3:7-3:19", + }, + "message": "Not following: shellcheck/sourced.sh: openBinaryFile: does not exist (No such file or directory)", + "range": { + "end": { + "character": 19, + "line": 3, + }, + "start": { + "character": 7, + "line": 3, + }, + }, + "severity": 3, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures/shellcheck/source.sh": [ + { + "newText": "# shellcheck disable=SC1091 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC1091 for the entire file", + }, + ], + "shellcheck|2154|5:6-5:10": [ + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:6-5:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 5, + }, + "start": { + "character": 6, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures/shellcheck/source.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 5, + }, + "start": { + "character": 0, + "line": 5, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for this line", + }, + { + "diagnostics": [ + { + "code": "SC2154", + "codeDescription": { + "href": "https://www.shellcheck.net/wiki/SC2154", + }, + "data": { + "id": "shellcheck|2154|5:6-5:10", + }, + "message": "foo is referenced but not assigned.", + "range": { + "end": { + "character": 10, + "line": 5, + }, + "start": { + "character": 6, + "line": 5, + }, + }, + "severity": 2, + "source": "shellcheck", + "tags": undefined, + }, + ], + "edit": { + "changes": { + "file:///home/faivre/dev/pub/lsp/bash-language-server/testing/fixtures/shellcheck/source.sh": [ + { + "newText": "# shellcheck disable=SC2154 + ", + "range": { + "end": { + "character": 0, + "line": 1, + }, + "start": { + "character": 0, + "line": 1, + }, + }, + }, + ], + }, + }, + "kind": "quickfix", + "title": "Disable ShellCheck rule SC2154 for the entire file", + }, + ], + }, "diagnostics": [ { "code": "SC1091", diff --git a/server/src/shellcheck/index.ts b/server/src/shellcheck/index.ts index 5f4f207b..2b795262 100644 --- a/server/src/shellcheck/index.ts +++ b/server/src/shellcheck/index.ts @@ -7,7 +7,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import { debounce } from '../util/async' import { logger } from '../util/logger' -import { analyzeShebang } from '../util/shebang' +import { analyzeShebang, getShebang } from '../util/shebang' import { CODE_TO_TAGS, LEVEL_TO_SEVERITY } from './config' import { ShellCheckComment, @@ -25,7 +25,7 @@ type LinterOptions = { export type LintingResult = { diagnostics: LSP.Diagnostic[] - codeActions: Record + codeActions: Record } export class Linter { @@ -105,7 +105,7 @@ export class Linter { // Clean up the debounced function delete this.uriToDebouncedExecuteLint[document.uri] - return mapShellCheckResult({ uri: document.uri, result }) + return mapShellCheckResult({ document, uri: document.uri, result }) } private async runShellCheck( @@ -180,7 +180,15 @@ export class Linter { } } -function mapShellCheckResult({ uri, result }: { uri: string; result: ShellCheckResult }) { +function mapShellCheckResult({ + document, + uri, + result, +}: { + document: TextDocument + uri: string + result: ShellCheckResult +}) { const diagnostics: LintingResult['diagnostics'] = [] const codeActions: LintingResult['codeActions'] = {} @@ -215,6 +223,8 @@ function mapShellCheckResult({ uri, result }: { uri: string; result: ShellCheckR diagnostics.push(diagnostic) + codeActions[id] = [] + const codeAction = CodeActionProvider.getCodeAction({ comment, diagnostics: [diagnostic], @@ -222,13 +232,33 @@ function mapShellCheckResult({ uri, result }: { uri: string; result: ShellCheckR }) if (codeAction) { - codeActions[id] = codeAction + codeActions[id].push(codeAction) } + + codeActions[id].push( + CodeActionProvider.getDisableLineCodeAction({ + document, + comment, + diagnostics: [diagnostic], + uri, + }), + ) + codeActions[id].push( + CodeActionProvider.getDisableFileCodeAction({ + document, + comment, + diagnostics: [diagnostic], + uri, + }), + ) } return { diagnostics, codeActions } } +const DISABLE_REGEX = /disable=(SC\d{4}(?:,SC\d{4})*)/ +const DIRECTIVE_REGEX = /^\s*# shellcheck(?: [^=]+=[^\s]*)+/ +const MULTILINE_REGEX = /\\\s*(?:#.*)?$/m /** * Code has been adopted from https://github.com/vscode-shellcheck/vscode-shellcheck/ * and modified to fit the needs of this project. @@ -237,6 +267,242 @@ function mapShellCheckResult({ uri, result }: { uri: string; result: ShellCheckR * Copyright (c) Timon Wong */ class CodeActionProvider { + private static getLine({ + document, + lineNb, + }: { + document: TextDocument + lineNb: number + }): string { + return document.getText( + LSP.Range.create( + LSP.Position.create(lineNb, 0), + LSP.Position.create(lineNb + 1, 0), + ), + ) + } + + private static getInsertLineTextEdit({ + lineNb, + newText, + }: { + lineNb: number + newText: string + }): LSP.TextEdit { + return { + range: LSP.Range.create( + LSP.Position.create(lineNb, 0), + LSP.Position.create(lineNb, 0), + ), + newText, + } + } + + private static getReplaceLineTextEdit({ + lineNb, + newText, + }: { + lineNb: number + newText: string + }): LSP.TextEdit { + return { + range: LSP.Range.create( + LSP.Position.create(lineNb, 0), + LSP.Position.create(lineNb + 1, 0), + ), + newText, + } + } + + private static getDisableTextEdit({ + document, + directiveLineNb, + code, + indentation, + insertBefore, + }: { + document: TextDocument + directiveLineNb: number + code: number + indentation: string + insertBefore: boolean + }): LSP.TextEdit { + const directiveLine = this.getLine({ document, lineNb: directiveLineNb }) + + const directives = directiveLine.match(DIRECTIVE_REGEX) + if (!directives) { + // insert new directive line + const offset = insertBefore ? 0 : 1 + return this.getInsertLineTextEdit({ + lineNb: directiveLineNb + offset, + newText: `${indentation}# shellcheck disable=SC${code}\n`, + }) + } + + // TODO: use directives module + const disables = directiveLine.match(DISABLE_REGEX) + if (!disables) { + // No 'disable' directive yet. Append it to the rest + return this.getReplaceLineTextEdit({ + lineNb: directiveLineNb, + newText: `${directives[0]} disable=SC${code}\n`, + }) + } + + // replace existing disables by new list + // extract codes as numbers to be able to sort + const codes = disables[1].split(',').map((c) => Number(c.slice(2))) + codes.push(code) + codes.sort() + return this.getReplaceLineTextEdit({ + lineNb: directiveLineNb, + newText: directiveLine.replace( + DISABLE_REGEX, + `disable=${codes.map((code) => `SC${code}`).join(',')}`, + ), + }) + } + + private static getIndentation(line: string): string { + const match = line.match(/^([\s]*)/) + if (match && match[1]) { + return match[1] + } + return '' + } + + private static getMultiLineNb({ + document, + lineNb, + }: { + document: TextDocument + lineNb: number + }): number { + // Check previous lines for a lack of backslash + while (lineNb > 0) { + const prevLine = this.getLine({ document, lineNb: lineNb - 1 }) + if (!MULTILINE_REGEX.test(prevLine)) { + break + } + lineNb-- + } + return lineNb + } + + private static getDisableLineTextEdit({ + document, + lineNb, + code, + }: { + document: TextDocument + lineNb: number + code: number + }): LSP.TextEdit { + // directive line must be placed above the top of a multiline command + const multiLineNb = this.getMultiLineNb({ + document, + lineNb, + }) + const multiLine = this.getLine({ document, lineNb: multiLineNb }) + const indentation = this.getIndentation(multiLine) + + /* + * When first line: must be inserted in the same line as the error, therefore: + * * ensure directiveLineNb is positive + * * insert before + */ + return this.getDisableTextEdit({ + document, + directiveLineNb: multiLineNb == 0 ? 0 : multiLineNb - 1, + code, + indentation, + insertBefore: multiLineNb == 0, + }) + } + + private static getDisableFileTextEdit({ + document, + lineNb, + code, + }: { + document: TextDocument + lineNb: number + code: number + }): LSP.TextEdit { + // shebang must be on the first line + const firstNonShebangLine = getShebang(this.getLine({ document, lineNb: 0 })) ? 1 : 0 + + /* + * Must be before anything on line after shebang (if any) to avoid breaking + * beginning of the script (multiline, comments, ...) + */ + return this.getDisableTextEdit({ + document, + directiveLineNb: firstNonShebangLine, + code, + indentation: '', + insertBefore: true, + }) + } + + public static getDisableLineCodeAction({ + document, + comment, + diagnostics, + uri, + }: { + document: TextDocument + comment: ShellCheckComment + diagnostics: LSP.Diagnostic[] + uri: string + }): LSP.CodeAction { + return { + title: `Disable ShellCheck rule SC${comment.code} for this line`, + diagnostics, + edit: { + changes: { + [uri]: [ + this.getDisableLineTextEdit({ + document, + lineNb: comment.line - 1, + code: comment.code, + }), + ], + }, + }, + kind: LSP.CodeActionKind.QuickFix, + } + } + + public static getDisableFileCodeAction({ + document, + comment, + diagnostics, + uri, + }: { + document: TextDocument + comment: ShellCheckComment + diagnostics: LSP.Diagnostic[] + uri: string + }): LSP.CodeAction { + return { + title: `Disable ShellCheck rule SC${comment.code} for the entire file`, + diagnostics, + edit: { + changes: { + [uri]: [ + this.getDisableFileTextEdit({ + document, + lineNb: comment.line - 1, + code: comment.code, + }), + ], + }, + }, + kind: LSP.CodeActionKind.QuickFix, + } + } + public static getCodeAction({ comment, diagnostics,