Skip to content

Commit c38da48

Browse files
authored
Merge pull request #608 from bash-lsp/shellcheck-zod
Use zod for ShellCheck result parsing
2 parents 2802f0c + 49418df commit c38da48

File tree

8 files changed

+127
-147
lines changed

8 files changed

+127
-147
lines changed

scripts/release-server.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ yarn install
1919
yarn run verify:bail
2020

2121
cd server
22-
npm publish --tag beta
22+
npm publish
23+
# npm publish --tag beta # for releasing beta versions
2324
tagRelease $tag

server/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Bash Language Server
22

3+
## 4.0.0
4+
5+
- **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
6+
- 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`)
7+
- 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
8+
- Allow disabling background analysis by setting `backgroundAnalysisMaxFiles: 0`.
9+
310
## 3.3.1
411

512
- Fix missing documentation for some help pages https://github.com/bash-lsp/bash-language-server/pull/577

server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "A language server for Bash",
44
"author": "Mads Hartmann",
55
"license": "MIT",
6-
"version": "4.0.0-beta.6",
6+
"version": "4.0.0",
77
"publisher": "mads-hartmann",
88
"main": "./out/server.js",
99
"typings": "./out/server.d.ts",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ShellCheckResultSchema } from '../types'
2+
3+
describe('shellcheck', () => {
4+
it('asserts one valid shellcheck JSON comment', async () => {
5+
// prettier-ignore
6+
const shellcheckJSON = {
7+
comments: [
8+
{ 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, },
9+
],
10+
}
11+
ShellCheckResultSchema.parse(shellcheckJSON)
12+
})
13+
14+
it('asserts two valid shellcheck JSON comment', async () => {
15+
// prettier-ignore
16+
const shellcheckJSON = {
17+
comments: [
18+
{ 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, },
19+
{ 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, },
20+
],
21+
}
22+
ShellCheckResultSchema.parse(shellcheckJSON)
23+
})
24+
25+
it('fails shellcheck JSON with null comments', async () => {
26+
const shellcheckJSON = { comments: null }
27+
expect(() => ShellCheckResultSchema.parse(shellcheckJSON)).toThrow()
28+
})
29+
30+
it('fails shellcheck JSON with string commment', async () => {
31+
const shellcheckJSON = { comments: ['foo'] }
32+
expect(() => ShellCheckResultSchema.parse(shellcheckJSON)).toThrow()
33+
})
34+
35+
it('fails shellcheck JSON with invalid comment', async () => {
36+
const make = (tweaks = {}) => ({
37+
comments: [
38+
{
39+
file: 'testing/fixtures/comment-doc-on-hover.sh',
40+
line: 43,
41+
endLine: 43,
42+
column: 1,
43+
endColumn: 7,
44+
level: 'warning',
45+
code: 2034,
46+
message: 'bork bork',
47+
fix: null,
48+
...tweaks,
49+
},
50+
],
51+
})
52+
ShellCheckResultSchema.parse(make()) // Defaults should work
53+
54+
expect(() => ShellCheckResultSchema.parse(make({ file: 9 }))).toThrow()
55+
expect(() => ShellCheckResultSchema.parse(make({ line: '9' }))).toThrow()
56+
expect(() => ShellCheckResultSchema.parse(make({ endLine: '9' }))).toThrow()
57+
expect(() => ShellCheckResultSchema.parse(make({ column: '9' }))).toThrow()
58+
expect(() => ShellCheckResultSchema.parse(make({ endColumn: '9' }))).toThrow()
59+
expect(() => ShellCheckResultSchema.parse(make({ level: 9 }))).toThrow()
60+
expect(() => ShellCheckResultSchema.parse(make({ code: '9' }))).toThrow()
61+
expect(() => ShellCheckResultSchema.parse(make({ message: 9 }))).toThrow()
62+
})
63+
})

server/src/shellcheck/__tests__/index.test.ts

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument'
33

44
import { FIXTURE_DOCUMENT, FIXTURE_FOLDER } from '../../../../testing/fixtures'
55
import { getMockConnection } from '../../../../testing/mocks'
6-
import { assertShellCheckResult, Linter } from '../index'
6+
import { Linter } from '../index'
77

88
const mockConsole = getMockConnection().console
99

@@ -235,65 +235,3 @@ describe('linter', () => {
235235
})
236236
})
237237
})
238-
239-
describe('shellcheck', () => {
240-
it('asserts one valid shellcheck JSON comment', async () => {
241-
// prettier-ignore
242-
const shellcheckJSON = {
243-
comments: [
244-
{ 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, },
245-
],
246-
}
247-
assertShellCheckResult(shellcheckJSON)
248-
})
249-
250-
it('asserts two valid shellcheck JSON comment', async () => {
251-
// prettier-ignore
252-
const shellcheckJSON = {
253-
comments: [
254-
{ 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, },
255-
{ 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, },
256-
],
257-
}
258-
assertShellCheckResult(shellcheckJSON)
259-
})
260-
261-
it('fails shellcheck JSON with null comments', async () => {
262-
const shellcheckJSON = { comments: null }
263-
expect(() => assertShellCheckResult(shellcheckJSON)).toThrow()
264-
})
265-
266-
it('fails shellcheck JSON with string commment', async () => {
267-
const shellcheckJSON = { comments: ['foo'] }
268-
expect(() => assertShellCheckResult(shellcheckJSON)).toThrow()
269-
})
270-
271-
it('fails shellcheck JSON with invalid comment', async () => {
272-
const make = (tweaks = {}) => ({
273-
comments: [
274-
{
275-
file: 'testing/fixtures/comment-doc-on-hover.sh',
276-
line: 43,
277-
endLine: 43,
278-
column: 1,
279-
endColumn: 7,
280-
level: 'warning',
281-
code: 2034,
282-
message: 'bork bork',
283-
fix: null,
284-
...tweaks,
285-
},
286-
],
287-
})
288-
assertShellCheckResult(make()) // Defaults should work
289-
290-
expect(() => assertShellCheckResult(make({ file: 9 }))).toThrow()
291-
expect(() => assertShellCheckResult(make({ line: '9' }))).toThrow()
292-
expect(() => assertShellCheckResult(make({ endLine: '9' }))).toThrow()
293-
expect(() => assertShellCheckResult(make({ column: '9' }))).toThrow()
294-
expect(() => assertShellCheckResult(make({ endColumn: '9' }))).toThrow()
295-
expect(() => assertShellCheckResult(make({ level: 9 }))).toThrow()
296-
expect(() => assertShellCheckResult(make({ code: '9' }))).toThrow()
297-
expect(() => assertShellCheckResult(make({ message: 9 }))).toThrow()
298-
})
299-
})

server/src/shellcheck/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as LSP from 'vscode-languageserver/node'
22

3+
import { ShellCheckCommentLevel } from './types'
4+
35
export const SUPPORTED_BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh']
46

57
// https://github.com/koalaman/shellcheck/wiki
@@ -14,7 +16,10 @@ export const CODE_TO_TAGS: Record<number, LSP.DiagnosticTag[] | undefined> = {
1416

1517
// https://github.com/koalaman/shellcheck/blob/364c33395e2f2d5500307f01989f70241c247d5a/src/ShellCheck/Formatter/Format.hs#L50
1618

17-
export const LEVEL_TO_SEVERITY: Record<string, LSP.DiagnosticSeverity | undefined> = {
19+
export const LEVEL_TO_SEVERITY: Record<
20+
ShellCheckCommentLevel,
21+
LSP.DiagnosticSeverity | undefined
22+
> = {
1823
error: LSP.DiagnosticSeverity.Error,
1924
warning: LSP.DiagnosticSeverity.Warning,
2025
info: LSP.DiagnosticSeverity.Information,

server/src/shellcheck/index.ts

Lines changed: 10 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { TextDocument } from 'vscode-languageserver-textdocument'
44

55
import { analyzeShebang } from '../util/shebang'
66
import { CODE_TO_TAGS, LEVEL_TO_SEVERITY } from './config'
7-
import { ShellCheckComment, ShellCheckReplacement, ShellCheckResult } from './types'
7+
import {
8+
ShellCheckComment,
9+
ShellCheckReplacement,
10+
ShellCheckResult,
11+
ShellCheckResultSchema,
12+
} from './types'
813

914
const SUPPORTED_BASH_DIALECTS = ['sh', 'bash', 'dash', 'ksh']
1015

@@ -60,6 +65,9 @@ export class Linter {
6065
const documentText = document.getText()
6166

6267
const { shellDialect } = analyzeShebang(documentText)
68+
// NOTE: that ShellCheck actually does shebang parsing, but we manually
69+
// do it here in order to fallback to bash. This enables parsing files
70+
// with a bash syntax.
6371
const shellName =
6472
shellDialect && SUPPORTED_BASH_DIALECTS.includes(shellDialect)
6573
? shellDialect
@@ -128,64 +136,10 @@ export class Linter {
128136
`ShellCheck: json parse failed with error ${e}\nout:\n${out}\nerr:\n${err}`,
129137
)
130138
}
131-
assertShellCheckResult(raw)
132-
return raw
133-
}
134-
}
135-
export function assertShellCheckResult(val: any): asserts val is ShellCheckResult {
136-
// TODO: use zod
137-
if (val !== null && typeof val !== 'object') {
138-
throw new Error(`shellcheck: unexpected json output ${typeof val}`)
139-
}
140139

141-
if (!Array.isArray(val.comments)) {
142-
throw new Error(
143-
`shellcheck: unexpected json output: expected 'comments' array ${typeof val.comments}`,
144-
)
145-
}
146-
147-
for (const idx in val.comments) {
148-
const comment = val.comments[idx]
149-
if (comment !== null && typeof comment != 'object') {
150-
throw new Error(
151-
`shellcheck: expected comment at index ${idx} to be object, found ${typeof comment}`,
152-
)
153-
}
154-
if (typeof comment.file !== 'string')
155-
throw new Error(
156-
`shellcheck: expected comment file at index ${idx} to be string, found ${typeof comment.file}`,
157-
)
158-
if (typeof comment.level !== 'string')
159-
throw new Error(
160-
`shellcheck: expected comment level at index ${idx} to be string, found ${typeof comment.level}`,
161-
)
162-
if (typeof comment.message !== 'string')
163-
throw new Error(
164-
`shellcheck: expected comment message at index ${idx} to be string, found ${typeof comment.level}`,
165-
)
166-
if (typeof comment.line !== 'number')
167-
throw new Error(
168-
`shellcheck: expected comment line at index ${idx} to be number, found ${typeof comment.line}`,
169-
)
170-
if (typeof comment.endLine !== 'number')
171-
throw new Error(
172-
`shellcheck: expected comment endLine at index ${idx} to be number, found ${typeof comment.endLine}`,
173-
)
174-
if (typeof comment.column !== 'number')
175-
throw new Error(
176-
`shellcheck: expected comment column at index ${idx} to be number, found ${typeof comment.column}`,
177-
)
178-
if (typeof comment.endColumn !== 'number')
179-
throw new Error(
180-
`shellcheck: expected comment endColumn at index ${idx} to be number, found ${typeof comment.endColumn}`,
181-
)
182-
if (typeof comment.code !== 'number')
183-
throw new Error(
184-
`shellcheck: expected comment code at index ${idx} to be number, found ${typeof comment.code}`,
185-
)
140+
return ShellCheckResultSchema.parse(raw)
186141
}
187142
}
188-
189143
function mapShellCheckResult({
190144
document,
191145
result,

server/src/shellcheck/types.ts

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
1-
export type ShellCheckResult = Readonly<{
2-
comments: ReadonlyArray<ShellCheckComment>
3-
}>
1+
import { z } from 'zod'
2+
3+
const ReplacementSchema = z.object({
4+
precedence: z.number(),
5+
line: z.number(),
6+
endLine: z.number(),
7+
column: z.number(),
8+
endColumn: z.number(),
9+
insertionPoint: z.string(),
10+
replacement: z.string(),
11+
})
12+
13+
// https://github.com/koalaman/shellcheck/blob/364c33395e2f2d5500307f01989f70241c247d5a/src/ShellCheck/Formatter/Format.hs#L50
14+
const LevelSchema = z.enum(['error', 'warning', 'info', 'style'])
415

516
// Constituent structures defined here:
617
// https://github.com/koalaman/shellcheck/blob/master/src/ShellCheck/Interface.hs
7-
export type ShellCheckComment = Readonly<{
8-
file: string
9-
line: number // 1-based
10-
endLine: number // 1-based
11-
column: number // 1-based
12-
endColumn: number // 1-based
13-
level: string // See LEVEL_TO_SEVERITY
14-
code: number
15-
message: string
16-
fix?: {
17-
replacements: ReadonlyArray<ShellCheckReplacement>
18-
}
19-
}>
2018

21-
export type ShellCheckReplacement = {
22-
precedence: number
23-
line: number
24-
endLine: number
25-
column: number
26-
endColumn: number
27-
insertionPoint: string
28-
replacement: string
29-
}
19+
export const ShellCheckResultSchema = z.object({
20+
comments: z.array(
21+
z.object({
22+
file: z.string(),
23+
line: z.number(), // 1-based
24+
endLine: z.number(), // 1-based
25+
column: z.number(), // 1-based
26+
endColumn: z.number(), // 1-based
27+
level: LevelSchema,
28+
code: z.number(),
29+
message: z.string(),
30+
fix: z
31+
.object({
32+
replacements: z.array(ReplacementSchema),
33+
})
34+
.nullable(),
35+
}),
36+
),
37+
})
38+
export type ShellCheckResult = z.infer<typeof ShellCheckResultSchema>
39+
export type ShellCheckComment = ShellCheckResult['comments'][number]
40+
export type ShellCheckCommentLevel = z.infer<typeof LevelSchema>
41+
export type ShellCheckReplacement = z.infer<typeof ReplacementSchema>

0 commit comments

Comments
 (0)