Skip to content

Commit 12914ea

Browse files
authored
Merge pull request #1392 from kolyshkin/bats-load-sourcing
feat: resolve bats `load` as a sourcing command
2 parents 77294ff + 61b5428 commit 12914ea

7 files changed

Lines changed: 163 additions & 11 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,7 @@ exports[`server onRenameRequest Workspace-wide rename returns correct WorkspaceE
12481248
exports[`server onRenameRequest Workspace-wide rename returns correct WorkspaceEdits for unsourced symbols when includeAllWorkspaceSymbols is true 1`] = `
12491249
{
12501250
"changes": {
1251+
"file://__REPO_ROOT_FOLDER__/testing/fixtures/bats/test_helper.bash": [],
12511252
"file://__REPO_ROOT_FOLDER__/testing/fixtures/comment-doc-on-hover.sh": [],
12521253
"file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc": [],
12531254
"file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh": [
@@ -1355,6 +1356,7 @@ exports[`server onRenameRequest Workspace-wide rename returns correct WorkspaceE
13551356
exports[`server onRenameRequest Workspace-wide rename returns correct WorkspaceEdits for unsourced symbols when includeAllWorkspaceSymbols is true 2`] = `
13561357
{
13571358
"changes": {
1359+
"file://__REPO_ROOT_FOLDER__/testing/fixtures/bats/test_helper.bash": [],
13581360
"file://__REPO_ROOT_FOLDER__/testing/fixtures/comment-doc-on-hover.sh": [],
13591361
"file://__REPO_ROOT_FOLDER__/testing/fixtures/extension.inc": [],
13601362
"file://__REPO_ROOT_FOLDER__/testing/fixtures/install.sh": [],

server/src/__tests__/analyzer.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Logger } from '../util/logger'
1616
const CURRENT_URI = 'dummy-uri.sh'
1717

1818
// if you add a .sh file to testing/fixtures, update this value
19-
const FIXTURE_FILES_MATCHING_GLOB = 20
19+
const FIXTURE_FILES_MATCHING_GLOB = 21
2020

2121
const defaultConfig = getDefaultConfiguration()
2222

@@ -243,6 +243,38 @@ describe('findDeclarationLocations', () => {
243243
`)
244244
})
245245

246+
it('returns a location in a bats helper file pulled in with `load`', async () => {
247+
const analyzer = await getAnalyzer({
248+
runBackgroundAnalysis: true,
249+
workspaceFolder: FIXTURE_FOLDER,
250+
})
251+
const document = FIXTURE_DOCUMENT.BATS_SOURCING
252+
const { uri } = document
253+
analyzer.analyze({ uri, document })
254+
const result = analyzer.findDeclarationLocations({
255+
uri,
256+
word: 'setup_test_env',
257+
position: { character: 4, line: 5 },
258+
})
259+
expect(updateSnapshotUris(result)).toMatchInlineSnapshot(`
260+
[
261+
{
262+
"range": {
263+
"end": {
264+
"character": 1,
265+
"line": 4,
266+
},
267+
"start": {
268+
"character": 0,
269+
"line": 2,
270+
},
271+
},
272+
"uri": "file://__REPO_ROOT_FOLDER__/testing/fixtures/bats/test_helper.bash",
273+
},
274+
]
275+
`)
276+
})
277+
246278
it('returns a local reference if definition is found', async () => {
247279
const analyzer = await getAnalyzer({})
248280
analyzer.analyze({ uri: CURRENT_URI, document: FIXTURE_DOCUMENT.INSTALL })

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +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'
5+
import { FIXTURE_FOLDER, REPO_ROOT_FOLDER } from '../../../../testing/fixtures'
66
import { initializeParser } from '../../parser'
77
import { getSourceCommands } from '../sourcing'
88

@@ -220,4 +220,74 @@ describe('getSourcedUris', () => {
220220
]
221221
`)
222222
})
223+
it('resolves bats `load` commands in .bats files', () => {
224+
jest.restoreAllMocks()
225+
226+
const fileContent = `
227+
load test_helper # bats appends the .bash extension
228+
229+
load ./test_helper.bash # explicit extension
230+
231+
load "${FIXTURE_FOLDER}bats/test_helper" # absolute path
232+
233+
load ../issue101.sh # relative to the test file
234+
235+
load "$SOME_VARIABLE" # dynamic loads are not supported
236+
237+
load # not finished
238+
`
239+
240+
const sourceCommands = getSourceCommands({
241+
fileUri: `${FIXTURE_FOLDER}bats/sourcing.bats`,
242+
rootPath: REPO_ROOT_FOLDER,
243+
tree: parser.parse(fileContent),
244+
})
245+
246+
const sourcedUris = new Set(
247+
sourceCommands
248+
.map((sourceCommand) => sourceCommand.uri)
249+
.filter((uri) => uri !== null),
250+
)
251+
252+
expect(sourcedUris).toEqual(
253+
new Set([
254+
`file://${FIXTURE_FOLDER}bats/test_helper.bash`,
255+
`file://${FIXTURE_FOLDER}issue101.sh`,
256+
]),
257+
)
258+
259+
expect(
260+
sourceCommands
261+
.filter((command) => command.error)
262+
.map(({ error, range }) => ({
263+
error,
264+
line: range.start.line,
265+
})),
266+
).toMatchInlineSnapshot(`
267+
[
268+
{
269+
"error": "non-constant source not supported",
270+
"line": 9,
271+
},
272+
]
273+
`)
274+
})
275+
276+
it('does not treat `load` as a sourcing command outside of .bats files', () => {
277+
jest.restoreAllMocks()
278+
279+
const fileContent = `
280+
load test_helper
281+
282+
load ../issue101.sh
283+
`
284+
285+
const sourceCommands = getSourceCommands({
286+
fileUri: `${FIXTURE_FOLDER}bats/not-a-bats-file.sh`,
287+
rootPath: REPO_ROOT_FOLDER,
288+
tree: parser.parse(fileContent),
289+
})
290+
291+
expect(sourceCommands).toEqual([])
292+
})
223293
})

server/src/util/sourcing.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import * as TreeSitterUtil from './tree-sitter'
1010

1111
const SOURCING_COMMANDS = ['source', '.']
1212

13+
// Bats (https://bats-core.readthedocs.io) test files pull in helper files using
14+
// `load`, which behaves like `source` but resolves relative to the directory of
15+
// the test file and appends ".bash" if the given path does not exist. It is only
16+
// treated as a sourcing command in .bats files, as `load` is a common enough
17+
// name for an unrelated command or function elsewhere.
18+
const BATS_SOURCING_COMMANDS = ['load']
19+
const BATS_SOURCED_EXTENSION = '.bash'
20+
1321
export type SourceCommand = {
1422
range: LSP.Range
1523
uri: string | null // resolved URIs
@@ -31,13 +39,16 @@ export function getSourceCommands({
3139
const sourceCommands: SourceCommand[] = []
3240

3341
const rootPaths = [path.dirname(fileUri), rootPath].filter(Boolean) as string[]
42+
const isBatsFile = fileUri.endsWith('.bats')
3443

3544
TreeSitterUtil.forEach(tree.rootNode, (node) => {
36-
const sourcedPathInfo = getSourcedPathInfoFromNode({ node })
45+
const sourcedPathInfo = getSourcedPathInfoFromNode({ node, isBatsFile })
3746

3847
if (sourcedPathInfo) {
3948
const { sourcedPath, parseError } = sourcedPathInfo
40-
const uri = sourcedPath ? resolveSourcedUri({ rootPaths, sourcedPath }) : null
49+
const uri = sourcedPath
50+
? resolveSourcedUri({ rootPaths, sourcedPath, isBatsFile })
51+
: null
4152

4253
sourceCommands.push({
4354
range: TreeSitterUtil.range(node),
@@ -54,9 +65,15 @@ export function getSourceCommands({
5465

5566
function getSourcedPathInfoFromNode({
5667
node,
68+
isBatsFile,
5769
}: {
5870
node: Parser.SyntaxNode
71+
isBatsFile: boolean
5972
}): null | { sourcedPath?: string; parseError?: string } {
73+
const sourcingCommands = isBatsFile
74+
? [...SOURCING_COMMANDS, ...BATS_SOURCING_COMMANDS]
75+
: SOURCING_COMMANDS
76+
6077
if (node.type === 'command') {
6178
const [commandNameNode, argumentNode] = node.namedChildren
6279

@@ -66,7 +83,7 @@ function getSourcedPathInfoFromNode({
6683

6784
if (
6885
commandNameNode.type === 'command_name' &&
69-
SOURCING_COMMANDS.includes(commandNameNode.text)
86+
sourcingCommands.includes(commandNameNode.text)
7087
) {
7188
const previousCommentNode =
7289
node.previousSibling?.type === 'comment' ? node.previousSibling : null
@@ -148,6 +165,7 @@ function getSourcedPathInfoFromNode({
148165
* - Converts a relative paths to absolute paths
149166
* - Converts a tilde path to an absolute path
150167
* - Resolves the path
168+
* - For bats files, retries with a ".bash" suffix, like bats' own `load` does
151169
*
152170
* NOTE: for future improvements:
153171
* "If filename does not contain a slash, file names in PATH are used to find
@@ -156,28 +174,39 @@ function getSourcedPathInfoFromNode({
156174
function resolveSourcedUri({
157175
rootPaths,
158176
sourcedPath,
177+
isBatsFile,
159178
}: {
160179
rootPaths: string[]
161180
sourcedPath: string
181+
isBatsFile: boolean
162182
}): string | null {
163183
if (sourcedPath.startsWith('~')) {
164184
sourcedPath = untildify(sourcedPath)
165185
}
166186

187+
// bats' `load` falls back to appending ".bash" when the given path is not a file
188+
const sourcedPaths = isBatsFile
189+
? [sourcedPath, `${sourcedPath}${BATS_SOURCED_EXTENSION}`]
190+
: [sourcedPath]
191+
167192
if (sourcedPath.startsWith('/')) {
168-
if (fs.existsSync(sourcedPath)) {
169-
return `file://${sourcedPath}`
193+
for (const candidate of sourcedPaths) {
194+
if (fs.existsSync(candidate)) {
195+
return `file://${candidate}`
196+
}
170197
}
171198
return null
172199
}
173200

174201
// resolve relative path
175202
for (const rootPath of rootPaths) {
176-
const potentialPath = path.join(rootPath.replace('file://', ''), sourcedPath)
203+
for (const candidate of sourcedPaths) {
204+
const potentialPath = path.join(rootPath.replace('file://', ''), candidate)
177205

178-
// check if path is a file
179-
if (fs.existsSync(potentialPath)) {
180-
return `file://${potentialPath}`
206+
// check if path is a file
207+
if (fs.existsSync(potentialPath)) {
208+
return `file://${potentialPath}`
209+
}
181210
}
182211
}
183212

testing/fixtures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ function getDocument(uri: string) {
1616
type FIXTURE_KEY = keyof typeof FIXTURE_URI
1717

1818
export const FIXTURE_URI = {
19+
BATS_SOURCING: `file://${path.join(FIXTURE_FOLDER, 'bats', 'sourcing.bats')}`,
20+
BATS_TEST_HELPER: `file://${path.join(FIXTURE_FOLDER, 'bats', 'test_helper.bash')}`,
1921
COMMENT_DOC: `file://${path.join(FIXTURE_FOLDER, 'comment-doc-on-hover.sh')}`,
2022
CRASH: `file://${path.join(FIXTURE_FOLDER, 'crash.zsh')}`,
2123
INSTALL: `file://${path.join(FIXTURE_FOLDER, 'install.sh')}`,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bats
2+
3+
load test_helper
4+
5+
setup() {
6+
setup_test_env
7+
}
8+
9+
@test "it works" {
10+
run true
11+
[ "$status" -eq 0 ]
12+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
3+
setup_test_env() {
4+
echo "setting up"
5+
}

0 commit comments

Comments
 (0)