Skip to content

Commit e55bac8

Browse files
authoredMay 13, 2022
Merge pull request #342 from shabbyrobe/master
Shellcheck integration
2 parents c6c7c74 + 0f607ba commit e55bac8

File tree

11 files changed

+430
-22
lines changed

11 files changed

+430
-22
lines changed
 

‎.github/workflows/verify.yml

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v3
1616

17+
- name: Install shellcheck (used for testing)
18+
run: sudo apt-get install -y shellcheck
19+
1720
- name: Use Node.js ${{ matrix.node-version }}
1821
uses: actions/setup-node@v3
1922
with:

‎server/src/__tests__/analyzer.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ describe('fromRoot', () => {
275275
expect(connection.window.showWarningMessage).not.toHaveBeenCalled()
276276

277277
// if you add a .sh file to testing/fixtures, update this value
278-
const FIXTURE_FILES_MATCHING_GLOB = 12
278+
const FIXTURE_FILES_MATCHING_GLOB = 14
279279

280280
// Intro, stats on glob, one file skipped due to shebang, and outro
281281
const LOG_LINES = FIXTURE_FILES_MATCHING_GLOB + 4

‎server/src/__tests__/config.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
import * as config from '../config'
22

3+
describe('getShellcheckPath', () => {
4+
it('defaults to shellcheck without path', () => {
5+
process.env = {}
6+
const result = config.getShellcheckPath()
7+
expect(result).toEqual('shellcheck')
8+
})
9+
10+
it('default to null in case of an empty string', () => {
11+
process.env = {
12+
SHELLCHECK_PATH: '',
13+
}
14+
const result = config.getShellcheckPath()
15+
expect(result).toBeNull()
16+
})
17+
18+
it('parses environment variable', () => {
19+
process.env = {
20+
SHELLCHECK_PATH: '/path/to/shellcheck',
21+
}
22+
const result = config.getShellcheckPath()
23+
expect(result).toEqual('/path/to/shellcheck')
24+
})
25+
})
26+
327
describe('getExplainshellEndpoint', () => {
428
it('default to null', () => {
529
process.env = {}

‎server/src/__tests__/linter.test.ts

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import * as path from 'path'
2+
import * as LSP from 'vscode-languageserver'
3+
4+
import { FIXTURE_DOCUMENT, FIXTURE_FOLDER } from '../../../testing/fixtures'
5+
import Linter, { assertShellcheckResult } from '../linter'
6+
7+
function textToDoc(txt: string) {
8+
return LSP.TextDocument.create('foo', 'bar', 0, txt)
9+
}
10+
11+
describe('linter', () => {
12+
it('should set canLint to false if executable empty', () => {
13+
expect(new Linter({ executablePath: null }).canLint).toBe(false)
14+
})
15+
16+
it('should set canLint to true if executable not empty', () => {
17+
expect(new Linter({ executablePath: null }).canLint).toBe(false)
18+
})
19+
20+
it('should set canLint to false when linting fails', async () => {
21+
jest.spyOn(console, 'error').mockImplementation()
22+
const executablePath = '77b4d3f6-c87a-11ec-9b62-a3c90f66d29f'
23+
const linter = new Linter({
24+
executablePath,
25+
})
26+
expect(await linter.lint(textToDoc(''), [])).toEqual([])
27+
expect(linter.canLint).toBe(false)
28+
expect(console.error).toBeCalledWith(
29+
expect.stringContaining('shellcheck not available at path'),
30+
)
31+
})
32+
33+
it('should lint when shellcheck present', async () => {
34+
// prettier-ignore
35+
const shell = [
36+
'#!/bin/bash',
37+
'echo $foo',
38+
].join('\n')
39+
40+
const expected: LSP.Diagnostic[] = [
41+
{
42+
message: 'SC2154: foo is referenced but not assigned.',
43+
severity: 2,
44+
code: 2154,
45+
source: 'shellcheck',
46+
range: { start: { line: 1, character: 5 }, end: { line: 1, character: 9 } },
47+
},
48+
{
49+
message: 'SC2086: Double quote to prevent globbing and word splitting.',
50+
severity: 3,
51+
code: 2086,
52+
source: 'shellcheck',
53+
range: { start: { line: 1, character: 5 }, end: { line: 1, character: 9 } },
54+
},
55+
]
56+
57+
const linter = new Linter({ executablePath: 'shellcheck' })
58+
const result = await linter.lint(textToDoc(shell), [])
59+
expect(result).toEqual(expected)
60+
})
61+
62+
it('should correctly follow sources with correct cwd', async () => {
63+
const linter = new Linter({ executablePath: 'shellcheck', cwd: FIXTURE_FOLDER })
64+
const result = await linter.lint(FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, [])
65+
expect(result).toEqual([])
66+
})
67+
68+
it('should fail to follow sources with incorrect cwd', async () => {
69+
const linter = new Linter({
70+
executablePath: 'shellcheck',
71+
cwd: path.resolve(path.join(FIXTURE_FOLDER, '../')),
72+
})
73+
// prettier-ignore
74+
const expected = [
75+
{ message: 'SC1091: Not following: shellcheck/sourced.sh: openBinaryFile: does not exist (No such file or directory)', severity: 3, code: 1091, source: 'shellcheck', range: { start: { line: 3, character: 7 }, end: { line: 3, character: 19 } }, },
76+
{ message: 'SC2154: foo is referenced but not assigned.', severity: 2, code: 2154, source: 'shellcheck', range: { start: { line: 5, character: 6 }, end: { line: 5, character: 10 } }, },
77+
]
78+
const result = await linter.lint(FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, [])
79+
expect(result).toEqual(expected)
80+
})
81+
82+
it('should follow sources with incorrect cwd if correct path is passed as a workspace path', async () => {
83+
const linter = new Linter({
84+
executablePath: 'shellcheck',
85+
cwd: path.resolve(path.join(FIXTURE_FOLDER, '../')),
86+
})
87+
const result = await linter.lint(FIXTURE_DOCUMENT.SHELLCHECK_SOURCE, [
88+
{ uri: `file://${path.resolve(FIXTURE_FOLDER)}`, name: 'fixtures' },
89+
])
90+
expect(result).toEqual([])
91+
})
92+
})
93+
94+
describe('shellcheck', () => {
95+
it('asserts one valid shellcheck JSON comment', async () => {
96+
// prettier-ignore
97+
const shellcheckJSON = {
98+
comments: [
99+
{ 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, },
100+
],
101+
}
102+
assertShellcheckResult(shellcheckJSON)
103+
})
104+
105+
it('asserts two valid shellcheck JSON comment', async () => {
106+
// prettier-ignore
107+
const shellcheckJSON = {
108+
comments: [
109+
{ 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, },
110+
{ 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, },
111+
],
112+
}
113+
assertShellcheckResult(shellcheckJSON)
114+
})
115+
116+
it('fails shellcheck JSON with null comments', async () => {
117+
const shellcheckJSON = { comments: null }
118+
expect(() => assertShellcheckResult(shellcheckJSON)).toThrow()
119+
})
120+
121+
it('fails shellcheck JSON with string commment', async () => {
122+
const shellcheckJSON = { comments: ['foo'] }
123+
expect(() => assertShellcheckResult(shellcheckJSON)).toThrow()
124+
})
125+
126+
it('fails shellcheck JSON with invalid commment', async () => {
127+
const make = (tweaks = {}) => ({
128+
comments: [
129+
{
130+
file: 'testing/fixtures/comment-doc-on-hover.sh',
131+
line: 43,
132+
endLine: 43,
133+
column: 1,
134+
endColumn: 7,
135+
level: 'warning',
136+
code: 2034,
137+
message: 'bork bork',
138+
fix: null,
139+
...tweaks,
140+
},
141+
],
142+
})
143+
assertShellcheckResult(make()) // Defaults should work
144+
145+
expect(() => assertShellcheckResult(make({ file: 9 }))).toThrow()
146+
expect(() => assertShellcheckResult(make({ line: '9' }))).toThrow()
147+
expect(() => assertShellcheckResult(make({ endLine: '9' }))).toThrow()
148+
expect(() => assertShellcheckResult(make({ column: '9' }))).toThrow()
149+
expect(() => assertShellcheckResult(make({ endColumn: '9' }))).toThrow()
150+
expect(() => assertShellcheckResult(make({ level: 9 }))).toThrow()
151+
expect(() => assertShellcheckResult(make({ code: '9' }))).toThrow()
152+
expect(() => assertShellcheckResult(make({ message: 9 }))).toThrow()
153+
})
154+
})

‎server/src/config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
export const DEFAULT_GLOB_PATTERN = '**/*@(.sh|.inc|.bash|.command)'
22

3+
export function getShellcheckPath(): string | null {
4+
const { SHELLCHECK_PATH } = process.env
5+
// If this is an empty string, this should coalesce to null and disable linting via shellcheck:
6+
return typeof SHELLCHECK_PATH === 'string' ? SHELLCHECK_PATH || null : 'shellcheck'
7+
}
8+
39
export function getExplainshellEndpoint(): string | null {
410
const { EXPLAINSHELL_ENDPOINT } = process.env
511
return typeof EXPLAINSHELL_ENDPOINT === 'string' && EXPLAINSHELL_ENDPOINT.trim() !== ''

0 commit comments

Comments
 (0)
Please sign in to comment.