Skip to content

Commit 13c3a66

Browse files
authored
Merge pull request #1221 from mcecode/fix-1183
Support renaming variables within `read` commands
2 parents 07e729e + cd62a5b commit 13c3a66

File tree

9 files changed

+626
-27
lines changed

9 files changed

+626
-27
lines changed

Diff for: server/src/__tests__/__snapshots__/server.test.ts.snap

+437
Large diffs are not rendered by default.

Diff for: server/src/__tests__/analyzer.test.ts

+1-1
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 = 19
19+
const FIXTURE_FILES_MATCHING_GLOB = 20
2020

2121
const defaultConfig = getDefaultConfiguration()
2222

Diff for: server/src/__tests__/server.test.ts

+98-13
Original file line numberDiff line numberDiff line change
@@ -1408,11 +1408,15 @@ describe('server', () => {
14081408
})
14091409

14101410
describe('onPrepareRename', () => {
1411-
async function getPrepareRenameResult(line: LSP.uinteger, character: LSP.uinteger) {
1411+
async function getPrepareRenameResult(
1412+
line: LSP.uinteger,
1413+
character: LSP.uinteger,
1414+
{ uri = FIXTURE_URI.RENAMING } = {},
1415+
) {
14121416
const { connection } = await initializeServer()
14131417

14141418
return connection.onPrepareRename.mock.calls[0][0](
1415-
{ textDocument: { uri: FIXTURE_URI.RENAMING }, position: { line, character } },
1419+
{ textDocument: { uri }, position: { line, character } },
14161420
{} as any,
14171421
)
14181422
}
@@ -1480,6 +1484,22 @@ describe('server', () => {
14801484
},
14811485
}
14821486
`)
1487+
1488+
const readvar = await getPrepareRenameResult(2, 18, {
1489+
uri: FIXTURE_URI.RENAMING_READ,
1490+
})
1491+
expect(readvar).toMatchInlineSnapshot(`
1492+
{
1493+
"end": {
1494+
"character": 20,
1495+
"line": 2,
1496+
},
1497+
"start": {
1498+
"character": 13,
1499+
"line": 2,
1500+
},
1501+
}
1502+
`)
14831503
})
14841504
})
14851505

@@ -1660,6 +1680,75 @@ describe('server', () => {
16601680
expect(somevarInsideSomefunc).toStrictEqual(s)
16611681
}
16621682
})
1683+
1684+
it('returns correct WorkspaceEdits for variables within read commands', async () => {
1685+
const [readvar, ...readvars] = await getRenameRequestResults(
1686+
[2, 8, { uri: FIXTURE_URI.RENAMING_READ }],
1687+
[2, 19, { uri: FIXTURE_URI.RENAMING_READ }],
1688+
[2, 21, { uri: FIXTURE_URI.RENAMING_READ }],
1689+
[3, 10, { uri: FIXTURE_URI.RENAMING_READ }],
1690+
[3, 19, { uri: FIXTURE_URI.RENAMING_READ }],
1691+
[6, 14, { uri: FIXTURE_URI.RENAMING_READ }],
1692+
[7, 15, { uri: FIXTURE_URI.RENAMING_READ }],
1693+
[8, 32, { uri: FIXTURE_URI.RENAMING_READ }],
1694+
[9, 7, { uri: FIXTURE_URI.RENAMING_READ }],
1695+
[11, 23, { uri: FIXTURE_URI.RENAMING_READ }],
1696+
[12, 30, { uri: FIXTURE_URI.RENAMING_READ }],
1697+
[13, 10, { uri: FIXTURE_URI.RENAMING_READ }],
1698+
[15, 10, { uri: FIXTURE_URI.RENAMING_READ }],
1699+
[15, 31, { uri: FIXTURE_URI.RENAMING_READ }],
1700+
[16, 11, { uri: FIXTURE_URI.RENAMING_READ }],
1701+
[16, 30, { uri: FIXTURE_URI.RENAMING_READ }],
1702+
[17, 23, { uri: FIXTURE_URI.RENAMING_READ }],
1703+
[17, 33, { uri: FIXTURE_URI.RENAMING_READ }],
1704+
)
1705+
expect(readvar).toMatchSnapshot()
1706+
for (const r of readvars) {
1707+
expect(readvar).toStrictEqual(r)
1708+
}
1709+
1710+
const [readloop, ...readloops] = await getRenameRequestResults(
1711+
[21, 21, { uri: FIXTURE_URI.RENAMING_READ }],
1712+
[23, 12, { uri: FIXTURE_URI.RENAMING_READ }],
1713+
)
1714+
expect(readloop).toMatchSnapshot()
1715+
for (const r of readloops) {
1716+
expect(readloop).toStrictEqual(r)
1717+
}
1718+
1719+
const [readscope, ...readscopes] = await getRenameRequestResults(
1720+
[28, 8, { uri: FIXTURE_URI.RENAMING_READ }],
1721+
[30, 11, { uri: FIXTURE_URI.RENAMING_READ }],
1722+
[31, 12, { uri: FIXTURE_URI.RENAMING_READ }],
1723+
[38, 15, { uri: FIXTURE_URI.RENAMING_READ }],
1724+
[43, 9, { uri: FIXTURE_URI.RENAMING_READ }],
1725+
)
1726+
expect(readscope).toMatchSnapshot()
1727+
for (const r of readscopes) {
1728+
expect(readscope).toStrictEqual(r)
1729+
}
1730+
1731+
const [readscopeInsideFunction, ...readscopesInsideFunction] =
1732+
await getRenameRequestResults(
1733+
[33, 11, { uri: FIXTURE_URI.RENAMING_READ }],
1734+
[34, 14, { uri: FIXTURE_URI.RENAMING_READ }],
1735+
[35, 8, { uri: FIXTURE_URI.RENAMING_READ }],
1736+
)
1737+
expect(readscopeInsideFunction).toMatchSnapshot()
1738+
for (const r of readscopesInsideFunction) {
1739+
expect(readscopeInsideFunction).toStrictEqual(r)
1740+
}
1741+
1742+
const [readscopeInsideSubshell, ...readscopesInsideSubshell] =
1743+
await getRenameRequestResults(
1744+
[40, 14, { uri: FIXTURE_URI.RENAMING_READ }],
1745+
[41, 10, { uri: FIXTURE_URI.RENAMING_READ }],
1746+
)
1747+
expect(readscopeInsideSubshell).toMatchSnapshot()
1748+
for (const r of readscopesInsideSubshell) {
1749+
expect(readscopeInsideSubshell).toStrictEqual(r)
1750+
}
1751+
})
16631752
})
16641753

16651754
describe('Workspace-wide rename', () => {
@@ -1741,47 +1830,43 @@ describe('server', () => {
17411830
// These may fail in the future when tree-sitter-bash's parsing gets better
17421831
// or when the rename symbol implementation is improved.
17431832
describe('Edge or not covered cases', () => {
1744-
it('only includes variables typed as variable_name', async () => {
1833+
it('does not include some variables typed as word', async () => {
17451834
const iRanges = await getFirstChangeRanges(getRenameRequestResult(106, 4))
17461835
// This should be 6 if all instances within let, postfix, and binary
17471836
// expressions are included.
17481837
expect(iRanges.length).toBe(3)
1749-
1750-
const lineRanges = await getFirstChangeRanges(getRenameRequestResult(118, 10))
1751-
// This should be 2 if the declaration of `line` is included.
1752-
expect(lineRanges.length).toBe(1)
17531838
})
17541839

17551840
it('includes incorrect number of symbols for complex scopes and nesting', async () => {
1756-
const varRanges = await getFirstChangeRanges(getRenameRequestResult(124, 8))
1841+
const varRanges = await getFirstChangeRanges(getRenameRequestResult(118, 8))
17571842
// This should only be 2 if `$var` from `3` is not included.
17581843
expect(varRanges.length).toBe(3)
17591844

1760-
const localFuncRanges = await getFirstChangeRanges(getRenameRequestResult(144, 5))
1845+
const localFuncRanges = await getFirstChangeRanges(getRenameRequestResult(138, 5))
17611846
// This should be 2 if the instance of `localFunc` in `callerFunc` is
17621847
// also included.
17631848
expect(localFuncRanges.length).toBe(1)
17641849
})
17651850

17661851
it('only takes into account subshells created with ( and )', async () => {
17671852
const pipelinevarRanges = await getFirstChangeRanges(
1768-
getRenameRequestResult(150, 7),
1853+
getRenameRequestResult(144, 7),
17691854
)
17701855
// This should only be 1 if pipeline subshell scoping is recognized.
17711856
expect(pipelinevarRanges.length).toBe(2)
17721857
})
17731858

17741859
it('does not take into account sourcing location and scope', async () => {
1775-
const FOOUris = await getChangeUris(getRenameRequestResult(154, 8))
1860+
const FOOUris = await getChangeUris(getRenameRequestResult(148, 8))
17761861
// This should only be 1 if sourcing after a symbol does not affect it.
17771862
expect(FOOUris.length).toBe(2)
17781863

1779-
const hello_worldUris = await getChangeUris(getRenameRequestResult(160, 6))
1864+
const hello_worldUris = await getChangeUris(getRenameRequestResult(154, 6))
17801865
// This should only be 1 if sourcing inside an uncalled function does
17811866
// not affect symbols outside of it.
17821867
expect(hello_worldUris.length).toBe(2)
17831868

1784-
const PATH_INPUTUris = await getChangeUris(getRenameRequestResult(163, 9))
1869+
const PATH_INPUTUris = await getChangeUris(getRenameRequestResult(157, 9))
17851870
// This should only be 1 if sourcing inside a subshell does not affect
17861871
// symbols outside of it.
17871872
expect(PATH_INPUTUris.length).toBe(2)

Diff for: server/src/analyser.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -466,15 +466,18 @@ export default class Analyzer {
466466

467467
const typeOfDescendants =
468468
kind === LSP.SymbolKind.Variable
469-
? 'variable_name'
469+
? ['variable_name', 'word']
470470
: ['function_definition', 'command_name']
471471
const startPosition = start
472472
? { row: start.line, column: start.character }
473473
: baseNode.startPosition
474474

475475
const ignoredRanges: LSP.Range[] = []
476476
const filterVariables = (n: Parser.SyntaxNode) => {
477-
if (n.text !== word) {
477+
if (
478+
n.text !== word ||
479+
(n.type === 'word' && !TreeSitterUtil.isVariableInReadCommand(n))
480+
) {
478481
return false
479482
}
480483

@@ -510,11 +513,14 @@ export default class Analyzer {
510513

511514
const declarationCommand = TreeSitterUtil.findParentOfType(n, 'declaration_command')
512515
const isLocal =
513-
(definedVariable?.text === word || !!(!definition && declarationCommand)) &&
514-
(parent.type === 'subshell' ||
515-
['local', 'declare', 'typeset'].includes(
516-
declarationCommand?.firstChild?.text as any,
517-
))
516+
// Local `variable_name`s
517+
((definedVariable?.text === word || !!(!definition && declarationCommand)) &&
518+
(parent.type === 'subshell' ||
519+
['local', 'declare', 'typeset'].includes(
520+
declarationCommand?.firstChild?.text as any,
521+
))) ||
522+
// Local variables within `read` command that are typed as `word`
523+
(parent.type === 'subshell' && n.type === 'word')
518524
if (isLocal) {
519525
if (includeDeclaration) {
520526
ignoredRanges.push(TreeSitterUtil.range(parent))
@@ -785,6 +791,14 @@ export default class Analyzer {
785791
}
786792
}
787793

794+
if (TreeSitterUtil.isVariableInReadCommand(node)) {
795+
return {
796+
word: node.text,
797+
range: TreeSitterUtil.range(node),
798+
kind: LSP.SymbolKind.Variable,
799+
}
800+
}
801+
788802
return null
789803
}
790804

Diff for: server/src/util/declarations.ts

+10
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,16 @@ export function findDeclarationUsingGlobalSemantics({
361361
return true
362362
}
363363

364+
if (
365+
kind === LSP.SymbolKind.Variable &&
366+
TreeSitterUtil.isVariableInReadCommand(n) &&
367+
n.text === word
368+
) {
369+
declaration = n
370+
continueSearching = false
371+
return false
372+
}
373+
364374
if (
365375
kind === LSP.SymbolKind.Function &&
366376
n.type === 'function_definition' &&

Diff for: server/src/util/tree-sitter.ts

+14
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ export function isReference(n: SyntaxNode): boolean {
4343
}
4444
}
4545

46+
export function isVariableInReadCommand(n: SyntaxNode): boolean {
47+
if (
48+
n.type === 'word' &&
49+
n.parent?.type === 'command' &&
50+
n.parent.firstChild?.text === 'read' &&
51+
!n.text.startsWith('-') &&
52+
!/^-.*[dinNptu]$/.test(n.previousSibling?.text ?? '')
53+
) {
54+
return true
55+
}
56+
57+
return false
58+
}
59+
4660
export function findParent(
4761
start: SyntaxNode,
4862
predicate: (n: SyntaxNode) => boolean,

Diff for: testing/fixtures.ts

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const FIXTURE_URI = {
3838
SOURCING: `file://${path.join(FIXTURE_FOLDER, 'sourcing.sh')}`,
3939
SOURCING2: `file://${path.join(FIXTURE_FOLDER, 'sourcing2.sh')}`,
4040
RENAMING: `file://${path.join(FIXTURE_FOLDER, 'renaming.sh')}`,
41+
RENAMING_READ: `file://${path.join(FIXTURE_FOLDER, 'renaming-read.sh')}`,
4142
}
4243

4344
export const FIXTURE_DOCUMENT: Record<FIXTURE_KEY, TextDocument> = (

Diff for: testing/fixtures/renaming-read.sh

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Varying cases and flag usage
2+
3+
read readvar readvar readvar
4+
read -r readvar readvar <<< "some output"
5+
echo readvar
6+
7+
read -n1 readvar
8+
read -t 10 readvar
9+
read -rd readvar -p readvar readvar
10+
echo $readvar
11+
12+
read -p readvar -a readvar -er
13+
read -sp "Prompt: " -ra readvar
14+
echo "${readvar[2]}"
15+
16+
read readvar -rp readvar readvar
17+
read -r readvar -p readvar readvar
18+
read -p readvar -ir readvar readvar
19+
20+
# While loop
21+
22+
while read -r readloop; do
23+
printf readloop
24+
echo "$readloop"
25+
done < somefile.txt
26+
27+
# Different scopes
28+
29+
read readscope
30+
readfunc() {
31+
read readscope
32+
echo $readscope
33+
34+
local readscope
35+
read readscope
36+
echo $readscope
37+
}
38+
(
39+
echo $readscope
40+
41+
read readscope
42+
echo $readscope
43+
)
44+
echo $readscope

Diff for: testing/fixtures/renaming.sh

-6
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,6 @@ for (( ; i < 10; i++)); do
113113
echo "$((2 * i))"
114114
done
115115

116-
# tree-sitter-bash's parsing limitations with while read loops
117-
118-
while read -r line; do
119-
echo "$line"
120-
done < somefile.txt
121-
122116
# Complex nesting affects self-assignment handling
123117

124118
f1() {

0 commit comments

Comments
 (0)