Skip to content

Commit ac11e2c

Browse files
committed
Improve findOriginalDeclaration heuristics
Handle multiple variable assignments and declarations per declaration command when searching globally
1 parent 187971a commit ac11e2c

File tree

1 file changed

+118
-63
lines changed

1 file changed

+118
-63
lines changed

server/src/analyser.ts

Lines changed: 118 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,6 @@ export default class Analyzer {
278278
/**
279279
* Find a symbol's original declaration and parent scope based on its original
280280
* definition with respect to its scope.
281-
*
282-
* TODO: Improve handling of multiple variable assignments and declarations in
283-
* one declaration command.
284281
*/
285282
public findOriginalDeclaration({
286283
position,
@@ -304,7 +301,23 @@ export default class Analyzer {
304301
let continueSearching = false
305302
let boundary = position.line
306303

307-
const findUsingGlobalSemantics = (base: Parser.SyntaxNode) => {
304+
/**
305+
* Estimates if the `position` of the `word` given is within the expression
306+
* (after the equals sign) of the `definition` node (should be a declaration
307+
* command or variable assignment) given.
308+
*
309+
* Important in cases of `var="$var"`, if the `word` is `$var` then `var`
310+
* should be skipped and a higher scope should be checked for the original
311+
* declaration.
312+
*/
313+
const isDefinedVariableInExpression = (
314+
definition: Parser.SyntaxNode,
315+
variable: Parser.SyntaxNode,
316+
) =>
317+
definition.endPosition.row >= position.line &&
318+
(variable.endPosition.column < position.character ||
319+
variable.endPosition.row < position.line)
320+
const findUsingGlobalSemantics = (base: Parser.SyntaxNode, baseIsInUri = true) => {
308321
TreeSitterUtil.forEach(base, (n) => {
309322
if (
310323
(declaration && !continueSearching) ||
@@ -314,47 +327,83 @@ export default class Analyzer {
314327
return false
315328
}
316329

317-
let definedSymbol: Parser.SyntaxNode | null | undefined
318-
// In cases of var="$var", if $var is being renamed, the definition
319-
// containing $var should be skipped and a higher scope should be
320-
// checked.
321-
let definedVariableInExpression = false
322-
323-
if (kind === LSP.SymbolKind.Variable && n.type === 'variable_assignment') {
324-
const declarationCommand = TreeSitterUtil.findParentOfType(
330+
// Declaration commands are handled separately from variable assignments
331+
// because declaration commands can declare variables without defining
332+
// them, while variable assignments require both declaration and
333+
// definition, so, there can be variable names within declaration
334+
// commands that are not children of variable assignments.
335+
if (kind === LSP.SymbolKind.Variable && n.type === 'declaration_command') {
336+
const functionDefinition = TreeSitterUtil.findParentOfType(
325337
n,
326-
'declaration_command',
338+
'function_definition',
327339
)
340+
const isLocalDeclaration =
341+
!!functionDefinition &&
342+
functionDefinition.lastChild?.type === 'compound_statement' &&
343+
['local', 'declare', 'typeset'].includes(n.firstChild?.text as any) &&
344+
(base.type !== 'subshell' ||
345+
base.startPosition.row < functionDefinition.startPosition.row)
346+
347+
for (const v of n.descendantsOfType('variable_name')) {
348+
if (
349+
v.text !== word ||
350+
TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])
351+
) {
352+
continue
353+
}
328354

329-
if (
330-
declarationCommand &&
331-
TreeSitterUtil.findParentOfType(declarationCommand, 'function_definition')
332-
?.lastChild?.type === 'compound_statement' &&
333-
['local', 'declare', 'typeset'].includes(
334-
declarationCommand.firstChild?.text as any,
335-
)
336-
) {
337-
return false
355+
if (isLocalDeclaration) {
356+
// Update boundary since any other instance of word below n can
357+
// now be considered local to a function or out of scope.
358+
boundary = n.startPosition.row
359+
break
360+
}
361+
362+
if (baseIsInUri && !isDefinedVariableInExpression(n, v)) {
363+
declaration = v
364+
continueSearching = false
365+
break
366+
}
338367
}
339368

340-
definedSymbol = n.descendantsOfType('variable_name').at(0)
341-
definedVariableInExpression =
342-
n.endPosition.row >= position.line &&
343-
!!definedSymbol &&
344-
(definedSymbol.endPosition.column < position.character ||
345-
definedSymbol.endPosition.row < position.line)
346-
} else if (
369+
// This return is important as it makes sure that the next if
370+
// statement only catches variable assignments outside of declaration
371+
// commands.
372+
return false
373+
}
374+
375+
if (
347376
kind === LSP.SymbolKind.Variable &&
348-
(n.type === 'for_statement' || (n.type === 'command' && n.text.includes(':=')))
377+
(['variable_assignment', 'for_statement'].includes(n.type) ||
378+
(n.type === 'command' && n.text.includes(':=')))
349379
) {
350-
definedSymbol = n.descendantsOfType('variable_name').at(0)
351-
} else if (kind === LSP.SymbolKind.Function && n.type === 'function_definition') {
352-
definedSymbol = n.firstNamedChild
380+
const definedVariable = n.descendantsOfType('variable_name').at(0)
381+
const definedVariableInExpression =
382+
baseIsInUri &&
383+
n.type === 'variable_assignment' &&
384+
!!definedVariable &&
385+
isDefinedVariableInExpression(n, definedVariable)
386+
387+
if (definedVariable?.text === word && !definedVariableInExpression) {
388+
declaration = definedVariable
389+
continueSearching = n.type === 'command'
390+
391+
// The original declaration could be inside a for statement, so only
392+
// return false when the original declaration is found.
393+
return false
394+
}
395+
396+
return true
353397
}
354398

355-
if (definedSymbol?.text === word && !definedVariableInExpression) {
356-
declaration = definedSymbol
357-
continueSearching = n.type === 'command'
399+
if (
400+
kind === LSP.SymbolKind.Function &&
401+
n.type === 'function_definition' &&
402+
n.firstNamedChild?.text === word
403+
) {
404+
declaration = n.firstNamedChild
405+
continueSearching = false
406+
return false
358407
}
359408

360409
return true
@@ -377,35 +426,30 @@ export default class Analyzer {
377426
return false
378427
}
379428

380-
if (n.type === 'declaration_command') {
381-
if (!['local', 'declare', 'typeset'].includes(n.firstChild?.text as any)) {
382-
return false
383-
}
429+
if (n.type !== 'declaration_command') {
430+
return true
431+
}
384432

385-
for (const v of n.descendantsOfType('variable_name')) {
386-
if (TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])) {
387-
continue
388-
}
433+
if (!['local', 'declare', 'typeset'].includes(n.firstChild?.text as any)) {
434+
return false
435+
}
389436

390-
// In cases of var="$var", if $var is being renamed, var should be
391-
// skipped and a higher scope should be checked for the original
392-
// declaration.
393-
const definedVariableInExpression =
394-
n.endPosition.row >= position.line &&
395-
(v.endPosition.column < position.character ||
396-
v.endPosition.row < position.line)
397-
398-
if (v.text === word && !definedVariableInExpression) {
399-
declaration = v
400-
continueSearching = false
401-
break
402-
}
437+
for (const v of n.descendantsOfType('variable_name')) {
438+
if (
439+
v.text !== word ||
440+
TreeSitterUtil.findParentOfType(v, ['simple_expansion', 'expansion'])
441+
) {
442+
continue
403443
}
404444

405-
return false
445+
if (!isDefinedVariableInExpression(n, v)) {
446+
declaration = v
447+
continueSearching = false
448+
break
449+
}
406450
}
407451

408-
return true
452+
return false
409453
})
410454
} else if (parent.type === 'subshell') {
411455
findUsingGlobalSemantics(parent)
@@ -415,6 +459,9 @@ export default class Analyzer {
415459
break
416460
}
417461

462+
// Update boundary since any other instance of word within or below the
463+
// current parent can now be considered local to that parent or out of
464+
// scope.
418465
boundary = parent.startPosition.row
419466
parent = this.parentScope(parent)
420467
}
@@ -423,12 +470,20 @@ export default class Analyzer {
423470
for (const u of this.getReachableUris({ fromUri: uri })) {
424471
const root = this.uriToAnalyzedDocument[u]?.tree.rootNode
425472

426-
if (root) {
427-
if (u !== uri) {
428-
boundary = root.endPosition.row
429-
}
473+
if (!root) {
474+
continue
475+
}
430476

477+
if (u === uri) {
478+
// Reset boundary so globally defined variables within any functions
479+
// already searched can be found.
480+
boundary = position.line
431481
findUsingGlobalSemantics(root)
482+
} else {
483+
// Set boundary to EOF since any position taken from the original
484+
// uri/file does not apply to other uris/files.
485+
boundary = root.endPosition.row
486+
findUsingGlobalSemantics(root, false)
432487
}
433488

434489
if (declaration && !continueSearching) {

0 commit comments

Comments
 (0)