diff --git a/scripts/release-server.sh b/scripts/release-server.sh index 3cb7e1b16..1acbd49a3 100755 --- a/scripts/release-server.sh +++ b/scripts/release-server.sh @@ -19,5 +19,6 @@ yarn install yarn run verify:bail cd server -npm publish --tag beta +npm publish +# npm publish --tag beta # for releasing beta versions tagRelease $tag diff --git a/server/CHANGELOG.md b/server/CHANGELOG.md index 641b5bc10..6bbef31b1 100644 --- a/server/CHANGELOG.md +++ b/server/CHANGELOG.md @@ -1,5 +1,12 @@ # Bash Language Server +## 4.0.0 + +- **Breaking**: Drop support for Node 12, which reached its official end of life on April 30th 2022. Doing so enables new features. https://github.com/bash-lsp/bash-language-server/pull/584 +- ShellCheck: support code actions, remove duplicated error codes, add URLs and tags, support parsing dialects (sh, bash, dash, ksh) but still fallback to bash, enable configuring ShellCheck arguments using the `shellcheckArguments` configuration parameter (legacy environment variable: `SHELLCHECK_ARGUMENTS`) +- Support workspace configuration instead of environment variables which enables updating configuration without reloading the server. We still support environment variables, but clients **should migrate to the new workspace configuration**. https://github.com/bash-lsp/bash-language-server/pull/599 +- Allow disabling background analysis by setting `backgroundAnalysisMaxFiles: 0`. + ## 3.3.1 - Fix missing documentation for some help pages https://github.com/bash-lsp/bash-language-server/pull/577 diff --git a/server/package.json b/server/package.json index fe1b4bb5d..adbab75e6 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,7 @@ "description": "A language server for Bash", "author": "Mads Hartmann", "license": "MIT", - "version": "4.0.0-beta.6", + "version": "4.0.0", "publisher": "mads-hartmann", "main": "./out/server.js", "typings": "./out/server.d.ts", diff --git a/server/src/shellcheck/__tests__/config.test.ts b/server/src/shellcheck/__tests__/config.test.ts new file mode 100644 index 000000000..2fbdcd8fd --- /dev/null +++ b/server/src/shellcheck/__tests__/config.test.ts @@ -0,0 +1,63 @@ +import { ShellCheckResultSchema } from '../types' + +describe('shellcheck', () => { + it('asserts one valid shellcheck JSON comment', async () => { + // prettier-ignore + const shellcheckJSON = { + comments: [ + { file: 'testing/fixtures/comment-doc-on-hover.sh', line: 43, endLine: 43, column: 1, endColumn: 7, level: 'warning', code: 2034, message: 'bork bork', fix: null, }, + ], + } + ShellCheckResultSchema.parse(shellcheckJSON) + }) + + it('asserts two valid shellcheck JSON comment', async () => { + // prettier-ignore + const shellcheckJSON = { + comments: [ + { file: 'testing/fixtures/comment-doc-on-hover.sh', line: 43, endLine: 43, column: 1, endColumn: 7, level: 'warning', code: 2034, message: 'bork bork', fix: null, }, + { file: 'testing/fixtures/comment-doc-on-hover.sh', line: 45, endLine: 45, column: 2, endColumn: 8, level: 'warning', code: 2035, message: 'bork bork', fix: null, }, + ], + } + ShellCheckResultSchema.parse(shellcheckJSON) + }) + + it('fails shellcheck JSON with null comments', async () => { + const shellcheckJSON = { comments: null } + expect(() => ShellCheckResultSchema.parse(shellcheckJSON)).toThrow() + }) + + it('fails shellcheck JSON with string commment', async () => { + const shellcheckJSON = { comments: ['foo'] } + expect(() => ShellCheckResultSchema.parse(shellcheckJSON)).toThrow() + }) + + it('fails shellcheck JSON with invalid comment', async () => { + const make = (tweaks = {}) => ({ + comments: [ + { + file: 'testing/fixtures/comment-doc-on-hover.sh', + line: 43, + endLine: 43, + column: 1, + endColumn: 7, + level: 'warning', + code: 2034, + message: 'bork bork', + fix: null, + ...tweaks, + }, + ], + }) + ShellCheckResultSchema.parse(make()) // Defaults should work + + expect(() => ShellCheckResultSchema.parse(make({ file: 9 }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ line: '9' }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ endLine: '9' }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ column: '9' }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ endColumn: '9' }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ level: 9 }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ code: '9' }))).toThrow() + expect(() => ShellCheckResultSchema.parse(make({ message: 9 }))).toThrow() + }) +}) diff --git a/server/src/shellcheck/__tests__/index.test.ts b/server/src/shellcheck/__tests__/index.test.ts index 40365c012..62f7f10c4 100644 --- a/server/src/shellcheck/__tests__/index.test.ts +++ b/server/src/shellcheck/__tests__/index.test.ts @@ -3,7 +3,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import { FIXTURE_DOCUMENT, FIXTURE_FOLDER } from '../../../../testing/fixtures' import { getMockConnection } from '../../../../testing/mocks' -import { assertShellCheckResult, Linter } from '../index' +import { Linter } from '../index' const mockConsole = getMockConnection().console @@ -235,65 +235,3 @@ describe('linter', () => { }) }) }) - -describe('shellcheck', () => { - it('asserts one valid shellcheck JSON comment', async () => { - // prettier-ignore - const shellcheckJSON = { - comments: [ - { file: 'testing/fixtures/comment-doc-on-hover.sh', line: 43, endLine: 43, column: 1, endColumn: 7, level: 'warning', code: 2034, message: 'bork bork', fix: null, }, - ], - } - assertShellCheckResult(shellcheckJSON) - }) - - it('asserts two valid shellcheck JSON comment', async () => { - // prettier-ignore - const shellcheckJSON = { - comments: [ - { file: 'testing/fixtures/comment-doc-on-hover.sh', line: 43, endLine: 43, column: 1, endColumn: 7, level: 'warning', code: 2034, message: 'bork bork', fix: null, }, - { file: 'testing/fixtures/comment-doc-on-hover.sh', line: 45, endLine: 45, column: 2, endColumn: 8, level: 'warning', code: 2035, message: 'bork bork', fix: null, }, - ], - } - assertShellCheckResult(shellcheckJSON) - }) - - it('fails shellcheck JSON with null comments', async () => { - const shellcheckJSON = { comments: null } - expect(() => assertShellCheckResult(shellcheckJSON)).toThrow() - }) - - it('fails shellcheck JSON with string commment', async () => { - const shellcheckJSON = { comments: ['foo'] } - expect(() => assertShellCheckResult(shellcheckJSON)).toThrow() - }) - - it('fails shellcheck JSON with invalid comment', async () => { - const make = (tweaks = {}) => ({ - comments: [ - { - file: 'testing/fixtures/comment-doc-on-hover.sh', - line: 43, - endLine: 43, - column: 1, - endColumn: 7, - level: 'warning', - code: 2034, - message: 'bork bork', - fix: null, - ...tweaks, - }, - ], - }) - assertShellCheckResult(make()) // Defaults should work - - expect(() => assertShellCheckResult(make({ file: 9 }))).toThrow() - expect(() => assertShellCheckResult(make({ line: '9' }))).toThrow() - expect(() => assertShellCheckResult(make({ endLine: '9' }))).toThrow() - expect(() => assertShellCheckResult(make({ column: '9' }))).toThrow() - expect(() => assertShellCheckResult(make({ endColumn: '9' }))).toThrow() - expect(() => assertShellCheckResult(make({ level: 9 }))).toThrow() - expect(() => assertShellCheckResult(make({ code: '9' }))).toThrow() - expect(() => assertShellCheckResult(make({ message: 9 }))).toThrow() - }) -}) diff --git a/server/src/shellcheck/config.ts b/server/src/shellcheck/config.ts index bb44930bf..e9b70c12a 100644 --- a/server/src/shellcheck/config.ts +++ b/server/src/shellcheck/config.ts @@ -1,5 +1,7 @@ import * as LSP from 'vscode-languageserver/node' +import { ShellCheckCommentLevel } from './types' + export const SUPPORTED_BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh'] // https://github.com/koalaman/shellcheck/wiki @@ -14,7 +16,10 @@ export const CODE_TO_TAGS: Record = { // https://github.com/koalaman/shellcheck/blob/364c33395e2f2d5500307f01989f70241c247d5a/src/ShellCheck/Formatter/Format.hs#L50 -export const LEVEL_TO_SEVERITY: Record = { +export const LEVEL_TO_SEVERITY: Record< + ShellCheckCommentLevel, + LSP.DiagnosticSeverity | undefined +> = { error: LSP.DiagnosticSeverity.Error, warning: LSP.DiagnosticSeverity.Warning, info: LSP.DiagnosticSeverity.Information, diff --git a/server/src/shellcheck/index.ts b/server/src/shellcheck/index.ts index f3f418113..9d8605505 100644 --- a/server/src/shellcheck/index.ts +++ b/server/src/shellcheck/index.ts @@ -4,7 +4,12 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import { analyzeShebang } from '../util/shebang' import { CODE_TO_TAGS, LEVEL_TO_SEVERITY } from './config' -import { ShellCheckComment, ShellCheckReplacement, ShellCheckResult } from './types' +import { + ShellCheckComment, + ShellCheckReplacement, + ShellCheckResult, + ShellCheckResultSchema, +} from './types' const SUPPORTED_BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh'] @@ -60,6 +65,9 @@ export class Linter { const documentText = document.getText() const { shellDialect } = analyzeShebang(documentText) + // NOTE: that ShellCheck actually does shebang parsing, but we manually + // do it here in order to fallback to bash. This enables parsing files + // with a bash syntax. const shellName = shellDialect && SUPPORTED_BASH_DIALECTS.includes(shellDialect) ? shellDialect @@ -128,64 +136,10 @@ export class Linter { `ShellCheck: json parse failed with error ${e}\nout:\n${out}\nerr:\n${err}`, ) } - assertShellCheckResult(raw) - return raw - } -} -export function assertShellCheckResult(val: any): asserts val is ShellCheckResult { - // TODO: use zod - if (val !== null && typeof val !== 'object') { - throw new Error(`shellcheck: unexpected json output ${typeof val}`) - } - if (!Array.isArray(val.comments)) { - throw new Error( - `shellcheck: unexpected json output: expected 'comments' array ${typeof val.comments}`, - ) - } - - for (const idx in val.comments) { - const comment = val.comments[idx] - if (comment !== null && typeof comment != 'object') { - throw new Error( - `shellcheck: expected comment at index ${idx} to be object, found ${typeof comment}`, - ) - } - if (typeof comment.file !== 'string') - throw new Error( - `shellcheck: expected comment file at index ${idx} to be string, found ${typeof comment.file}`, - ) - if (typeof comment.level !== 'string') - throw new Error( - `shellcheck: expected comment level at index ${idx} to be string, found ${typeof comment.level}`, - ) - if (typeof comment.message !== 'string') - throw new Error( - `shellcheck: expected comment message at index ${idx} to be string, found ${typeof comment.level}`, - ) - if (typeof comment.line !== 'number') - throw new Error( - `shellcheck: expected comment line at index ${idx} to be number, found ${typeof comment.line}`, - ) - if (typeof comment.endLine !== 'number') - throw new Error( - `shellcheck: expected comment endLine at index ${idx} to be number, found ${typeof comment.endLine}`, - ) - if (typeof comment.column !== 'number') - throw new Error( - `shellcheck: expected comment column at index ${idx} to be number, found ${typeof comment.column}`, - ) - if (typeof comment.endColumn !== 'number') - throw new Error( - `shellcheck: expected comment endColumn at index ${idx} to be number, found ${typeof comment.endColumn}`, - ) - if (typeof comment.code !== 'number') - throw new Error( - `shellcheck: expected comment code at index ${idx} to be number, found ${typeof comment.code}`, - ) + return ShellCheckResultSchema.parse(raw) } } - function mapShellCheckResult({ document, result, diff --git a/server/src/shellcheck/types.ts b/server/src/shellcheck/types.ts index eb7b511d6..f027d7f6c 100644 --- a/server/src/shellcheck/types.ts +++ b/server/src/shellcheck/types.ts @@ -1,29 +1,41 @@ -export type ShellCheckResult = Readonly<{ - comments: ReadonlyArray -}> +import { z } from 'zod' + +const ReplacementSchema = z.object({ + precedence: z.number(), + line: z.number(), + endLine: z.number(), + column: z.number(), + endColumn: z.number(), + insertionPoint: z.string(), + replacement: z.string(), +}) + +// https://github.com/koalaman/shellcheck/blob/364c33395e2f2d5500307f01989f70241c247d5a/src/ShellCheck/Formatter/Format.hs#L50 +const LevelSchema = z.enum(['error', 'warning', 'info', 'style']) // Constituent structures defined here: // https://github.com/koalaman/shellcheck/blob/master/src/ShellCheck/Interface.hs -export type ShellCheckComment = Readonly<{ - file: string - line: number // 1-based - endLine: number // 1-based - column: number // 1-based - endColumn: number // 1-based - level: string // See LEVEL_TO_SEVERITY - code: number - message: string - fix?: { - replacements: ReadonlyArray - } -}> -export type ShellCheckReplacement = { - precedence: number - line: number - endLine: number - column: number - endColumn: number - insertionPoint: string - replacement: string -} +export const ShellCheckResultSchema = z.object({ + comments: z.array( + z.object({ + file: z.string(), + line: z.number(), // 1-based + endLine: z.number(), // 1-based + column: z.number(), // 1-based + endColumn: z.number(), // 1-based + level: LevelSchema, + code: z.number(), + message: z.string(), + fix: z + .object({ + replacements: z.array(ReplacementSchema), + }) + .nullable(), + }), + ), +}) +export type ShellCheckResult = z.infer +export type ShellCheckComment = ShellCheckResult['comments'][number] +export type ShellCheckCommentLevel = z.infer +export type ShellCheckReplacement = z.infer