@@ -9,6 +9,7 @@ import { untildify } from './fs'
9
9
import * as TreeSitterUtil from './tree-sitter'
10
10
11
11
const SOURCING_COMMANDS = [ 'source' , '.' ]
12
+ const VARIABLE_NODE_TYPES = [ 'expansion' , 'simple_expansion' ]
12
13
13
14
export type SourceCommand = {
14
15
range : LSP . Range
@@ -102,34 +103,36 @@ function getSourcedPathInfoFromNode({
102
103
}
103
104
}
104
105
105
- if ( argumentNode . type === 'word' ) {
106
+ const strValue = resolveStaticString ( argumentNode )
107
+ if ( strValue !== null ) {
106
108
return {
107
- sourcedPath : argumentNode . text ,
109
+ sourcedPath : strValue ,
108
110
}
109
111
}
110
112
111
- if ( argumentNode . type === 'string' || argumentNode . type === 'raw_string' ) {
112
- const stringContents = argumentNode . text . slice ( 1 , - 1 )
113
- if ( argumentNode . namedChildren . length === 0 ) {
114
- return {
115
- sourcedPath : stringContents ,
116
- }
117
- } else if ( argumentNode . namedChildren . length === 1 ) {
118
- const [ variableNode ] = argumentNode . namedChildren
119
- if (
120
- variableNode . type == 'simple_expansion' ||
121
- variableNode . type == 'expansion'
122
- ) {
123
- const variableText = `${ variableNode . text } `
124
- if ( stringContents . startsWith ( variableText + '/' ) ) {
125
- return {
126
- sourcedPath : '.' + stringContents . slice ( variableText . length ) ,
127
- }
113
+ // Strip one leading dynamic section.
114
+ if ( argumentNode . type === 'string' && argumentNode . namedChildren . length === 1 ) {
115
+ const [ variableNode ] = argumentNode . namedChildren
116
+ if ( VARIABLE_NODE_TYPES . includes ( variableNode . type ) ) {
117
+ const stringContents = argumentNode . text . slice ( 1 , - 1 )
118
+ if ( stringContents . startsWith ( `${ variableNode . text } /` ) ) {
119
+ return {
120
+ sourcedPath : `.${ stringContents . slice ( variableNode . text . length ) } ` ,
128
121
}
129
122
}
130
123
}
131
124
}
132
125
126
+ if ( argumentNode . type === 'concatenation' ) {
127
+ // Strip one leading dynamic section from a concatenation node.
128
+ const sourcedPath = resolveSourceFromConcatenation ( argumentNode )
129
+ if ( sourcedPath ) {
130
+ return {
131
+ sourcedPath,
132
+ }
133
+ }
134
+ }
135
+
133
136
// TODO: we could try to parse any ShellCheck "source "directive
134
137
// # shellcheck source=src/examples/config.sh
135
138
return {
@@ -181,3 +184,72 @@ function resolveSourcedUri({
181
184
182
185
return null
183
186
}
187
+
188
+ /*
189
+ * Resolves the source path from a concatenation node, stripping a leading dynamic directory segment.
190
+ * Returns null if the source path can't be statically determined after stripping a segment.
191
+ * Note: If a non-concatenation node is passed, null will be returned. This is likely a programmer error.
192
+ */
193
+ function resolveSourceFromConcatenation ( node : Parser . SyntaxNode ) : string | null {
194
+ if ( node . type !== 'concatenation' ) return null
195
+ const stringValue = resolveStaticString ( node )
196
+ if ( stringValue !== null ) return stringValue // This string is fully static.
197
+
198
+ const values : string [ ] = [ ]
199
+ // Since the string must begin with the variable, the variable must be in the first child.
200
+ const [ firstNode , ...rest ] = node . namedChildren
201
+ // The first child is static, this means one of the other children is not!
202
+ if ( resolveStaticString ( firstNode ) !== null ) return null
203
+
204
+ // if the string is unquoted, the first child is the variable, so there's no more text in it.
205
+ if ( ! VARIABLE_NODE_TYPES . includes ( firstNode . type ) ) {
206
+ if ( firstNode . namedChildCount > 1 ) return null // Only one variable is allowed.
207
+ // Since the string must begin with the variable, the variable must be first child.
208
+ const variableNode = firstNode . namedChildren [ 0 ] // Get the variable (quoted case)
209
+ // This is command substitution!
210
+ if ( ! VARIABLE_NODE_TYPES . includes ( variableNode . type ) ) return null
211
+ const stringContents = firstNode . text . slice ( 1 , - 1 )
212
+ // The string doesn't start with the variable!
213
+ if ( ! stringContents . startsWith ( variableNode . text ) ) return null
214
+ // Get the remaining static portion the string
215
+ values . push ( stringContents . slice ( variableNode . text . length ) )
216
+ }
217
+
218
+ for ( const child of rest ) {
219
+ const value = resolveStaticString ( child )
220
+ // The other values weren't statically determinable!
221
+ if ( value === null ) return null
222
+ values . push ( value )
223
+ }
224
+
225
+ // Join all our found static values together.
226
+ const staticResult = values . join ( '' )
227
+ // The path starts with slash, so trim the leading variable and replace with a dot
228
+ if ( staticResult . startsWith ( '/' ) ) return `.${ staticResult } `
229
+ // The path doesn't start with a slash, so it's invalid
230
+ // PERF: can we fail earlier than this?
231
+ return null
232
+ }
233
+
234
+ /**
235
+ * Resolves the full string value of a node
236
+ * Returns null if the value can't be statically determined (ie, it contains a variable or command substition).
237
+ * Supports: word, string, raw_string, and concatenation
238
+ */
239
+ function resolveStaticString ( node : Parser . SyntaxNode ) : string | null {
240
+ if ( node . type === 'concatenation' ) {
241
+ const values = [ ]
242
+ for ( const child of node . namedChildren ) {
243
+ const value = resolveStaticString ( child )
244
+ if ( value === null ) return null
245
+ values . push ( value )
246
+ }
247
+ return values . join ( '' )
248
+ }
249
+ if ( node . type === 'word' ) return node . text
250
+ if ( node . type === 'string' || node . type === 'raw_string' ) {
251
+ if ( node . namedChildCount === 0 ) return node . text . slice ( 1 , - 1 )
252
+ return null
253
+ }
254
+ return null
255
+ }
0 commit comments