Skip to content

Commit d012e8b

Browse files
committed
Use ShellCheck directives when sourcing files
1 parent 303d480 commit d012e8b

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

server/src/util/__tests__/__snapshots__/sourcing.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`getSourcedUris returns a set of sourced files 2`] = `
3+
exports[`getSourcedUris returns a set of sourced files (but ignores some unhandled cases) 2`] = `
44
[
55
{
66
"error": null,

server/src/util/__tests__/sourcing.test.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as fs from 'fs'
22
import * as os from 'os'
33
import * as Parser from 'web-tree-sitter'
44

5+
import { REPO_ROOT_FOLDER } from '../../../../testing/fixtures'
56
import { initializeParser } from '../../parser'
67
import { getSourceCommands } from '../sourcing'
78

@@ -13,8 +14,6 @@ beforeAll(async () => {
1314
parser = await initializeParser()
1415
})
1516

16-
jest.spyOn(fs, 'existsSync').mockImplementation(() => true)
17-
1817
// mock os.homedir() to return a fixed path
1918
jest.spyOn(os, 'homedir').mockImplementation(() => '/Users/bash-user')
2019

@@ -29,7 +28,9 @@ describe('getSourcedUris', () => {
2928
expect(sourceCommands).toEqual([])
3029
})
3130

32-
it('returns a set of sourced files', () => {
31+
it('returns a set of sourced files (but ignores some unhandled cases)', () => {
32+
jest.spyOn(fs, 'existsSync').mockImplementation(() => true)
33+
3334
const fileContent = `
3435
source file-in-path.sh # does not contain a slash (i.e. is maybe somewhere on the path)
3536
@@ -73,7 +74,7 @@ describe('getSourcedUris', () => {
7374
done
7475
7576
# ======================================
76-
# example of sourcing through a function
77+
# Example of sourcing through a function
7778
# ======================================
7879
7980
loadlib () {
@@ -149,4 +150,67 @@ describe('getSourcedUris', () => {
149150

150151
expect(sourceCommands).toMatchSnapshot()
151152
})
153+
154+
it('returns a set of sourced files and parses ShellCheck directives', () => {
155+
jest.restoreAllMocks()
156+
157+
const fileContent = `
158+
. ./scripts/release-client.sh
159+
160+
source ./testing/fixtures/issue206.sh
161+
162+
# shellcheck source=/dev/null
163+
source ./IM_NOT_THERE.sh
164+
165+
# shellcheck source-path=testing/fixtures
166+
source missing-node.sh # source path by directive
167+
168+
# shellcheck source=./testing/fixtures/install.sh
169+
source "$X" # source by directive
170+
171+
# shellcheck source=./some-file-that-does-not-exist.sh
172+
source "$Y" # not source due to invalid directive
173+
174+
# shellcheck source-path=SCRIPTDIR # note that this is already the behaviour of bash language server
175+
source ./testing/fixtures/issue101.sh
176+
`
177+
178+
const sourceCommands = getSourceCommands({
179+
fileUri,
180+
rootPath: REPO_ROOT_FOLDER,
181+
tree: parser.parse(fileContent),
182+
})
183+
184+
const sourcedUris = new Set(
185+
sourceCommands
186+
.map((sourceCommand) => sourceCommand.uri)
187+
.filter((uri) => uri !== null),
188+
)
189+
190+
expect(sourcedUris).toEqual(
191+
new Set([
192+
`file://${REPO_ROOT_FOLDER}/scripts/release-client.sh`,
193+
`file://${REPO_ROOT_FOLDER}/testing/fixtures/issue206.sh`,
194+
`file://${REPO_ROOT_FOLDER}/testing/fixtures/missing-node.sh`,
195+
`file://${REPO_ROOT_FOLDER}/testing/fixtures/install.sh`,
196+
`file://${REPO_ROOT_FOLDER}/testing/fixtures/issue101.sh`,
197+
]),
198+
)
199+
200+
expect(
201+
sourceCommands
202+
.filter((command) => command.error)
203+
.map(({ error, range }) => ({
204+
error,
205+
line: range.start.line,
206+
})),
207+
).toMatchInlineSnapshot(`
208+
[
209+
{
210+
"error": "failed to resolve path",
211+
"line": 15,
212+
},
213+
]
214+
`)
215+
})
152216
})

server/src/util/sourcing.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as path from 'path'
33
import * as LSP from 'vscode-languageserver'
44
import * as Parser from 'web-tree-sitter'
55

6+
import { parseShellCheckDirective } from '../shellcheck/directive'
7+
import { discriminate } from './discriminate'
68
import { untildify } from './fs'
79
import * as TreeSitterUtil from './tree-sitter'
810

@@ -61,6 +63,40 @@ function getSourcedPathInfoFromNode({
6163
commandNameNode.type === 'command_name' &&
6264
SOURCING_COMMANDS.includes(commandNameNode.text)
6365
) {
66+
const previousCommentNode =
67+
node.previousSibling?.type === 'comment' ? node.previousSibling : null
68+
69+
if (previousCommentNode?.text.includes('shellcheck')) {
70+
const directives = parseShellCheckDirective(previousCommentNode.text)
71+
const sourcedPath = directives.find(discriminate('type', 'source'))?.path
72+
73+
if (sourcedPath === '/dev/null') {
74+
return null
75+
}
76+
77+
if (sourcedPath) {
78+
return {
79+
sourcedPath,
80+
}
81+
}
82+
83+
const isNotFollowErrorDisabled = !!directives
84+
.filter(discriminate('type', 'disable'))
85+
.flatMap(({ rules }) => rules)
86+
.find((rule) => rule === 'SC1091')
87+
88+
if (isNotFollowErrorDisabled) {
89+
return null
90+
}
91+
92+
const rootFolder = directives.find(discriminate('type', 'source-path'))?.path
93+
if (rootFolder && rootFolder !== 'SCRIPTDIR' && argumentNode.type === 'word') {
94+
return {
95+
sourcedPath: path.join(rootFolder, argumentNode.text),
96+
}
97+
}
98+
}
99+
64100
if (argumentNode.type === 'word') {
65101
return {
66102
sourcedPath: argumentNode.text,

0 commit comments

Comments
 (0)