@@ -10,6 +10,14 @@ import * as TreeSitterUtil from './tree-sitter'
1010
1111const 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+
1321export 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
5566function 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({
156174function 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
0 commit comments