Skip to content

Commit 0f7f7c5

Browse files
authored
Merge pull request #691 from bash-lsp/fix-parser-issue
Fix onHover and goto definition in case of syntax issues
2 parents 17b1aa2 + 083de28 commit 0f7f7c5

File tree

10 files changed

+138
-88
lines changed

10 files changed

+138
-88
lines changed

Diff for: server/CHANGELOG.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# Bash Language Server
22

3+
## 4.5.3
4+
5+
- Fix issue where some features would work as expected in case of a syntax issue https://github.com/bash-lsp/bash-language-server/pull/691
6+
37
## 4.5.2
48

5-
- fixed `onReferences` to respect the `context.includeDeclaration` flag
6-
- removed unnecessary dependency `urijs`
9+
- Fixed `onReferences` to respect the `context.includeDeclaration` flag https://github.com/bash-lsp/bash-language-server/pull/688
10+
- Removed unnecessary dependency `urijs` https://github.com/bash-lsp/bash-language-server/pull/688
711

812
## 4.5.1
913

Diff for: server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "A language server for Bash",
44
"author": "Mads Hartmann",
55
"license": "MIT",
6-
"version": "4.5.2",
6+
"version": "4.5.3",
77
"main": "./out/server.js",
88
"typings": "./out/server.d.ts",
99
"bin": {

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

+22-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let analyzer: Analyzer
1818
const CURRENT_URI = 'dummy-uri.sh'
1919

2020
// if you add a .sh file to testing/fixtures, update this value
21-
const FIXTURE_FILES_MATCHING_GLOB = 15
21+
const FIXTURE_FILES_MATCHING_GLOB = 16
2222

2323
const defaultConfig = getDefaultConfiguration()
2424

@@ -46,12 +46,30 @@ describe('analyze', () => {
4646
expect(loggerWarn).not.toHaveBeenCalled()
4747
})
4848

49-
it('parses files with a missing node', () => {
49+
it('parses files with a missing nodes and return relevant diagnostics', () => {
5050
const diagnostics = analyzer.analyze({
5151
uri: CURRENT_URI,
5252
document: FIXTURE_DOCUMENT.MISSING_NODE,
5353
})
54-
expect(diagnostics).toEqual([])
54+
expect(diagnostics).toMatchInlineSnapshot(`
55+
Array [
56+
Object {
57+
"message": "Syntax error: \\"fi\\" missing",
58+
"range": Object {
59+
"end": Object {
60+
"character": 0,
61+
"line": 12,
62+
},
63+
"start": Object {
64+
"character": 0,
65+
"line": 12,
66+
},
67+
},
68+
"severity": 2,
69+
"source": "bash-language-server",
70+
},
71+
]
72+
`)
5573
expect(loggerWarn).toHaveBeenCalledWith(
5674
'Error while parsing dummy-uri.sh: syntax error',
5775
)
@@ -783,6 +801,7 @@ describe('initiateBackgroundAnalysis', () => {
783801
expect(loggerWarn).toHaveBeenCalled()
784802
expect(loggerWarn.mock.calls).toEqual([
785803
[expect.stringContaining('missing-node.sh: syntax error')],
804+
[expect.stringContaining('missing-node2.sh: syntax error')],
786805
[expect.stringContaining('not-a-shell-script.sh: syntax error')],
787806
[expect.stringContaining('parse-problems.sh: syntax error')],
788807
])

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

+48-66
Original file line numberDiff line numberDiff line change
@@ -955,27 +955,29 @@ describe('server', () => {
955955
})
956956

957957
describe('onHover', () => {
958-
it('responds with documentation for command', async () => {
958+
async function getHoverResult(uri: string, position: LSP.Position) {
959959
const { connection } = await initializeServer()
960960

961961
const onHover = connection.onHover.mock.calls[0][0]
962962

963-
const result = await onHover(
963+
return onHover(
964964
{
965965
textDocument: {
966-
uri: FIXTURE_URI.INSTALL,
967-
},
968-
position: {
969-
// rm
970-
line: 25,
971-
character: 5,
966+
uri,
972967
},
968+
position,
973969
},
974970
{} as any,
975971
{} as any,
976972
)
973+
}
974+
it('responds with documentation for command', async () => {
975+
const result = await getHoverResult(FIXTURE_URI.INSTALL, {
976+
// rm
977+
line: 25,
978+
character: 5,
979+
})
977980

978-
expect(result).toBeDefined()
979981
expect(result).toEqual({
980982
contents: {
981983
kind: 'markdown',
@@ -985,25 +987,11 @@ describe('server', () => {
985987
})
986988

987989
it('responds with function documentation extracted from comments', async () => {
988-
const { connection } = await initializeServer()
989-
990-
const onHover = connection.onHover.mock.calls[0][0]
991-
992-
const result = await onHover(
993-
{
994-
textDocument: {
995-
uri: FIXTURE_URI.COMMENT_DOC,
996-
},
997-
position: {
998-
line: 17,
999-
character: 0,
1000-
},
1001-
},
1002-
{} as any,
1003-
{} as any,
1004-
)
990+
const result = await getHoverResult(FIXTURE_URI.COMMENT_DOC, {
991+
line: 17,
992+
character: 0,
993+
})
1005994

1006-
expect(result).toBeDefined()
1007995
expect(result).toMatchInlineSnapshot(`
1008996
Object {
1009997
"contents": Object {
@@ -1022,25 +1010,11 @@ describe('server', () => {
10221010
})
10231011

10241012
it('displays correct documentation for symbols in file that override path executables', async () => {
1025-
const { connection } = await initializeServer()
1026-
1027-
const onHover = connection.onHover.mock.calls[0][0]
1028-
1029-
const result = await onHover(
1030-
{
1031-
textDocument: {
1032-
uri: FIXTURE_URI.OVERRIDE_SYMBOL,
1033-
},
1034-
position: {
1035-
line: 9,
1036-
character: 1,
1037-
},
1038-
},
1039-
{} as any,
1040-
{} as any,
1041-
)
1013+
const result = await getHoverResult(FIXTURE_URI.OVERRIDE_SYMBOL, {
1014+
line: 9,
1015+
character: 1,
1016+
})
10421017

1043-
expect(result).toBeDefined()
10441018
expect(result).toMatchInlineSnapshot(`
10451019
Object {
10461020
"contents": Object {
@@ -1056,30 +1030,38 @@ describe('server', () => {
10561030
})
10571031

10581032
it('returns executable documentation if the function is not redefined', async () => {
1059-
const { connection } = await initializeServer()
1060-
1061-
const onHover = connection.onHover.mock.calls[0][0]
1033+
const result1 = await getHoverResult(FIXTURE_URI.OVERRIDE_SYMBOL, {
1034+
line: 2,
1035+
character: 1,
1036+
})
1037+
expect(result1).toEqual({
1038+
contents: {
1039+
kind: 'markdown',
1040+
value: expect.stringContaining('list directory contents'),
1041+
},
1042+
})
10621043

1063-
const getHoverResult = (position: LSP.Position) =>
1064-
onHover(
1065-
{
1066-
textDocument: {
1067-
uri: FIXTURE_URI.OVERRIDE_SYMBOL,
1068-
},
1069-
position,
1070-
},
1071-
{} as any,
1072-
{} as any,
1073-
)
1044+
// return null same result if the cursor is on the arguments
1045+
const result2 = await getHoverResult(FIXTURE_URI.OVERRIDE_SYMBOL, {
1046+
line: 2,
1047+
character: 3,
1048+
})
1049+
expect(result2).toEqual(null)
1050+
})
10741051

1075-
const result1 = await getHoverResult({ line: 2, character: 1 })
1076-
expect(result1).toBeDefined()
1077-
expect((result1 as any)?.contents.value).toContain('list directory contents')
1052+
it('responds with documentation even if parsing fails', async () => {
1053+
const result = await getHoverResult(FIXTURE_URI.MISSING_NODE2, {
1054+
// sleep
1055+
line: 12,
1056+
character: 4,
1057+
})
10781058

1079-
// return null same result if the cursor is on the arguments
1080-
const result2 = await getHoverResult({ line: 2, character: 3 })
1081-
expect(result2).toBeDefined()
1082-
expect((result2 as any)?.contents.value).toBeUndefined()
1059+
expect(result).toEqual({
1060+
contents: {
1061+
kind: 'markdown',
1062+
value: expect.stringContaining('sleep'),
1063+
},
1064+
})
10831065
})
10841066

10851067
it.skip('returns documentation from explainshell', async () => {

Diff for: server/src/analyser.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ export default class Analyzer {
116116
logger.warn(`Error while parsing ${uri}: syntax error`)
117117
}
118118

119-
return diagnostics
119+
const missingNodesDiagnostics = TreeSitterUtil.getDiagnosticsForMissingNodes(
120+
tree.rootNode,
121+
)
122+
123+
return diagnostics.concat(missingNodesDiagnostics)
120124
}
121125

122126
/**

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

+13-13
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,20 @@ export function getLocalDeclarations({
154154
node.type === 'program'
155155
? node
156156
: TreeSitterUtil.findParent(node, (p) => p.type === 'program')
157-
if (!rootNode) {
158-
throw new Error('did not find root node')
159-
}
160157

161-
Object.entries(
162-
getAllGlobalVariableDeclarations({
163-
rootNode,
164-
uri,
165-
}),
166-
).map(([name, symbols]) => {
167-
if (!declarations[name]) {
168-
declarations[name] = symbols
169-
}
170-
})
158+
if (rootNode) {
159+
// In case of parsing errors, the root node might not be found
160+
Object.entries(
161+
getAllGlobalVariableDeclarations({
162+
rootNode,
163+
uri,
164+
}),
165+
).map(([name, symbols]) => {
166+
if (!declarations[name]) {
167+
declarations[name] = symbols
168+
}
169+
})
170+
}
171171
}
172172

173173
return declarations

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Range } from 'vscode-languageserver/node'
1+
import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver/node'
22
import { SyntaxNode } from 'web-tree-sitter'
33

44
/**
@@ -56,3 +56,24 @@ export function findParent(
5656
}
5757
return null
5858
}
59+
60+
export function getDiagnosticsForMissingNodes(node: SyntaxNode) {
61+
const diagnostics: Diagnostic[] = []
62+
63+
forEach(node, (node) => {
64+
if (node.isMissing()) {
65+
diagnostics.push(
66+
Diagnostic.create(
67+
range(node),
68+
`Syntax error: "${node.type}" missing`,
69+
DiagnosticSeverity.Warning,
70+
undefined,
71+
'bash-language-server',
72+
),
73+
)
74+
}
75+
return node.hasError()
76+
})
77+
78+
return diagnostics
79+
}

Diff for: testing/fixtures.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const FIXTURE_URI = {
2323
MISSING_EXTENSION: `file://${path.join(FIXTURE_FOLDER, 'extension')}`,
2424
EXTENSION_INC: `file://${path.join(FIXTURE_FOLDER, 'extension.inc')}`,
2525
MISSING_NODE: `file://${path.join(FIXTURE_FOLDER, 'missing-node.sh')}`,
26+
MISSING_NODE2: `file://${path.join(FIXTURE_FOLDER, 'missing-node2.sh')}`,
2627
OPTIONS: `file://${path.join(FIXTURE_FOLDER, 'options.sh')}`,
2728
OVERRIDE_SYMBOL: `file://${path.join(FIXTURE_FOLDER, 'override-executable-symbol.sh')}`,
2829
PARSE_PROBLEMS: `file://${path.join(FIXTURE_FOLDER, 'parse-problems.sh')}`,

Diff for: testing/fixtures/missing-node2.sh

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
3+
a=14
4+
while :; do
5+
# doc for function
6+
random_op() {
7+
# parsing issue: missing node ")", but is not reported as MISSING
8+
a=$(((RANDOM % 1000) + 1))
9+
}
10+
# issue: hovering does not show documentation for function due to parsing error
11+
random_op
12+
13+
sleep 2
14+
15+
echo $a
16+
done
17+
18+
3

Diff for: vscode-client/src/server.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ connection.listen()
2020

2121
// Don't die on unhandled Promise rejections
2222
process.on('unhandledRejection', (reason, p) => {
23-
connection.console.error(`Unhandled Rejection at promise: ${p}, reason: ${reason}`)
23+
const stack = reason instanceof Error ? reason.stack : reason
24+
connection.console.error(`Unhandled Rejection at promise: ${p}, reason: ${stack}`)
2425
})
2526

2627
process.on('SIGPIPE', () => {

0 commit comments

Comments
 (0)