Skip to content

Commit 7920729

Browse files
authored
Merge pull request #673 from bash-lsp/stable-sourcing
Improving sourcing analysis and diagnostics
2 parents d9dc4cd + eee60f0 commit 7920729

File tree

14 files changed

+447
-179
lines changed

14 files changed

+447
-179
lines changed

server/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Bash Language Server
22

3+
## 4.4.0
4+
5+
- Improve source command parser and include diagnostics when parser fails https://github.com/bash-lsp/bash-language-server/pull/673
6+
- Fix `onHover` bug where sourced symbols on the same line as a reference would hide documentation https://github.com/bash-lsp/bash-language-server/pull/673
7+
8+
## 4.3.2
9+
10+
- Improved CLI output https://github.com/bash-lsp/bash-language-server/pull/672
11+
312
## 4.3.0
413

514
- Add centralized and configurable logger that can be controlled using the `BASH_IDE_LOG_LEVEL` environment variable and workspace configuration. https://github.com/bash-lsp/bash-language-server/pull/669

server/package.json

Lines changed: 1 addition & 1 deletion
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.3.2",
6+
"version": "4.4.0",
77
"main": "./out/server.js",
88
"typings": "./out/server.d.ts",
99
"bin": {

server/src/__tests__/__snapshots__/analyzer.test.ts.snap

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,5 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`analyze returns a list of errors for a file with a missing node 1`] = `
4-
Array [
5-
Object {
6-
"message": "Syntax error: expected \\"fi\\" somewhere in the file",
7-
"range": Object {
8-
"end": Object {
9-
"character": 0,
10-
"line": 12,
11-
},
12-
"start": Object {
13-
"character": 0,
14-
"line": 12,
15-
},
16-
},
17-
"severity": 2,
18-
},
19-
]
20-
`;
21-
22-
exports[`analyze returns a list of errors for a file with parsing errors 1`] = `
23-
Array [
24-
Object {
25-
"message": "Failed to parse",
26-
"range": Object {
27-
"end": Object {
28-
"character": 1,
29-
"line": 9,
30-
},
31-
"start": Object {
32-
"character": 0,
33-
"line": 2,
34-
},
35-
},
36-
"severity": 1,
37-
},
38-
]
39-
`;
40-
413
exports[`findReferences returns a list of locations if parameter is found 1`] = `
424
Array [
435
Object {

server/src/__tests__/analyzer.test.ts

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,110 @@ beforeAll(async () => {
3636
})
3737

3838
describe('analyze', () => {
39-
it('returns an empty list of errors for a file with no parsing errors', () => {
40-
const result = analyzer.analyze({
39+
it('returns an empty list of diagnostics for a file with no parsing errors', () => {
40+
const diagnostics = analyzer.analyze({
4141
uri: CURRENT_URI,
4242
document: FIXTURE_DOCUMENT.INSTALL,
4343
})
44-
expect(result).toEqual([])
44+
expect(diagnostics).toEqual([])
4545
})
4646

47-
it('returns a list of errors for a file with a missing node', () => {
48-
const result = analyzer.analyze({
47+
it('returns a list of diagnostics for a file with a missing node', () => {
48+
const diagnostics = analyzer.analyze({
4949
uri: CURRENT_URI,
5050
document: FIXTURE_DOCUMENT.MISSING_NODE,
5151
})
52-
expect(result).not.toEqual([])
53-
expect(result).toMatchSnapshot()
52+
expect(diagnostics).not.toEqual([])
53+
expect(diagnostics).toMatchInlineSnapshot(`
54+
Array [
55+
Object {
56+
"message": "Syntax error: expected \\"fi\\" somewhere in the file",
57+
"range": Object {
58+
"end": Object {
59+
"character": 0,
60+
"line": 12,
61+
},
62+
"start": Object {
63+
"character": 0,
64+
"line": 12,
65+
},
66+
},
67+
"severity": 2,
68+
"source": "bash-language-server",
69+
},
70+
]
71+
`)
5472
})
5573

56-
it('returns a list of errors for a file with parsing errors', () => {
57-
const result = analyzer.analyze({
74+
it('returns a list of diagnostics for a file with parsing errors', () => {
75+
const diagnostics = analyzer.analyze({
5876
uri: CURRENT_URI,
5977
document: FIXTURE_DOCUMENT.PARSE_PROBLEMS,
6078
})
61-
expect(result).not.toEqual([])
62-
expect(result).toMatchSnapshot()
79+
expect(diagnostics).not.toEqual([])
80+
expect(diagnostics).toMatchInlineSnapshot(`
81+
Array [
82+
Object {
83+
"message": "Failed to parse",
84+
"range": Object {
85+
"end": Object {
86+
"character": 1,
87+
"line": 9,
88+
},
89+
"start": Object {
90+
"character": 0,
91+
"line": 2,
92+
},
93+
},
94+
"severity": 1,
95+
"source": "bash-language-server",
96+
},
97+
]
98+
`)
99+
})
100+
101+
it('returns a list of diagnostics for a file with sourcing issues', async () => {
102+
const parser = await initializeParser()
103+
const newAnalyzer = new Analyzer({
104+
parser,
105+
workspaceFolder: FIXTURE_FOLDER,
106+
})
107+
const diagnostics = newAnalyzer.analyze({
108+
uri: CURRENT_URI,
109+
document: FIXTURE_DOCUMENT.SOURCING,
110+
})
111+
expect(diagnostics).not.toEqual([])
112+
expect(diagnostics).toMatchInlineSnapshot(`
113+
Array [
114+
Object {
115+
"message": "Source command could not be analyzed: expansion not supported.
116+
117+
Note that enabling the configuration flag \\"includeAllWorkspaceSymbols\\"
118+
would include all symbols in the workspace regardless of source commands.
119+
But be aware that this will lead to false positive suggestions.",
120+
"range": Object {
121+
"end": Object {
122+
"character": 16,
123+
"line": 21,
124+
},
125+
"start": Object {
126+
"character": 2,
127+
"line": 21,
128+
},
129+
},
130+
"severity": 3,
131+
"source": "bash-language-server",
132+
},
133+
]
134+
`)
135+
136+
// unless setIncludeAllWorkspaceSymbols set
137+
newAnalyzer.setIncludeAllWorkspaceSymbols(true)
138+
const diagnostics2 = newAnalyzer.analyze({
139+
uri: CURRENT_URI,
140+
document: FIXTURE_DOCUMENT.SOURCING,
141+
})
142+
expect(diagnostics2).toEqual([])
63143
})
64144
})
65145

server/src/__tests__/server.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ describe('server', () => {
208208
},
209209
"name": "BOLD",
210210
},
211+
Object {
212+
"kind": 12,
213+
"location": Object {
214+
"range": Object {
215+
"end": Object {
216+
"character": 1,
217+
"line": 22,
218+
},
219+
"start": Object {
220+
"character": 0,
221+
"line": 20,
222+
},
223+
},
224+
"uri": "file://${FIXTURE_FOLDER}sourcing.sh",
225+
},
226+
"name": "loadlib",
227+
},
211228
]
212229
`)
213230
})

server/src/analyser.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from './util/declarations'
1818
import { getFilePaths } from './util/fs'
1919
import { logger } from './util/logger'
20+
import { isPositionIncludedInRange } from './util/lsp'
2021
import { analyzeShebang } from './util/shebang'
2122
import * as sourcing from './util/sourcing'
2223
import * as TreeSitterUtil from './util/tree-sitter'
@@ -27,6 +28,7 @@ type AnalyzedDocument = {
2728
document: TextDocument
2829
globalDeclarations: GlobalDeclarations
2930
sourcedUris: Set<string>
31+
sourceCommands: sourcing.SourceCommand[]
3032
tree: Parser.Tree
3133
}
3234

@@ -74,25 +76,56 @@ export default class Analyzer {
7476

7577
const { diagnostics, globalDeclarations } = getGlobalDeclarations({ tree, uri })
7678

79+
const sourceCommands = sourcing.getSourceCommands({
80+
fileUri: uri,
81+
rootPath: this.workspaceFolder,
82+
tree,
83+
})
84+
85+
const sourcedUris = new Set(
86+
sourceCommands
87+
.map((sourceCommand) => sourceCommand.uri)
88+
.filter((uri): uri is string => uri !== null),
89+
)
90+
7791
this.uriToAnalyzedDocument[uri] = {
7892
document,
7993
globalDeclarations,
80-
sourcedUris: sourcing.getSourcedUris({
81-
fileContent,
82-
fileUri: uri,
83-
rootPath: this.workspaceFolder,
84-
tree,
85-
}),
94+
sourcedUris,
95+
sourceCommands: sourceCommands.filter((sourceCommand) => !sourceCommand.error),
8696
tree,
8797
}
8898

99+
if (!this.includeAllWorkspaceSymbols) {
100+
sourceCommands
101+
.filter((sourceCommand) => sourceCommand.error)
102+
.forEach((sourceCommand) => {
103+
diagnostics.push(
104+
LSP.Diagnostic.create(
105+
sourceCommand.range,
106+
[
107+
`Source command could not be analyzed: ${sourceCommand.error}.\n`,
108+
'Note that enabling the configuration flag "includeAllWorkspaceSymbols"',
109+
'would include all symbols in the workspace regardless of source commands.',
110+
'But be aware that this will lead to false positive suggestions.',
111+
].join('\n'),
112+
LSP.DiagnosticSeverity.Information,
113+
undefined,
114+
'bash-language-server',
115+
),
116+
)
117+
})
118+
}
119+
89120
function findMissingNodes(node: Parser.SyntaxNode) {
90121
if (node.isMissing()) {
91122
diagnostics.push(
92123
LSP.Diagnostic.create(
93124
TreeSitterUtil.range(node),
94125
`Syntax error: expected "${node.type}" somewhere in the file`,
95126
LSP.DiagnosticSeverity.Warning,
127+
undefined,
128+
'bash-language-server',
96129
),
97130
)
98131
} else if (node.hasError()) {
@@ -198,20 +231,13 @@ export default class Analyzer {
198231
uri: string
199232
word: string
200233
}): LSP.Location[] {
201-
const tree = this.uriToAnalyzedDocument[uri]?.tree
234+
// If the word is sourced, return the location of the source file
235+
const sourcedUri = this.uriToAnalyzedDocument[uri]?.sourceCommands
236+
.filter((sourceCommand) => isPositionIncludedInRange(position, sourceCommand.range))
237+
.map((sourceCommand) => sourceCommand.uri)[0]
202238

203-
if (position && tree) {
204-
// NOTE: when a word is a file path to a sourced file, we return a location to it.
205-
const sourcedLocation = sourcing.getSourcedLocation({
206-
position,
207-
rootPath: this.workspaceFolder,
208-
tree,
209-
uri,
210-
word,
211-
})
212-
if (sourcedLocation) {
213-
return [sourcedLocation]
214-
}
239+
if (sourcedUri) {
240+
return [LSP.Location.create(sourcedUri, LSP.Range.create(0, 0, 0, 0))]
215241
}
216242

217243
return this.findDeclarationsMatchingWord({

server/src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export function getConfigFromEnvironmentVariables(): {
6161

6262
const environmentVariablesUsed = Object.entries(rawConfig)
6363
.map(([key, value]) => (typeof value !== 'undefined' ? key : null))
64-
.filter((key) => key !== null) as string[]
64+
.filter((key): key is string => key !== null)
65+
.filter((key) => key !== 'logLevel') // logLevel is a special case that we ignore
6566

6667
const config = ConfigSchema.parse(rawConfig)
6768

server/src/server.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ export default class BashServer {
364364
symbol: LSP.SymbolInformation
365365
currentUri: string
366366
}): LSP.MarkupContent {
367+
logger.debug(
368+
`getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`,
369+
)
367370
const symbolUri = symbol.location.uri
368371
const symbolStartLine = symbol.location.range.start.line
369372

@@ -665,6 +668,9 @@ export default class BashServer {
665668
Builtins.isBuiltin(word) ||
666669
(this.executables.isExecutableOnPATH(word) && symbolsMatchingWord.length == 0)
667670
) {
671+
logger.debug(
672+
`onHover: getting shell documentation for reserved word or builtin or executable`,
673+
)
668674
const shellDocumentation = await getShellDocumentation({ word })
669675
if (shellDocumentation) {
670676
return { contents: getMarkdownContent(shellDocumentation, 'man') }
@@ -675,7 +681,11 @@ export default class BashServer {
675681
currentUri,
676682
})
677683
// do not return hover referencing for the current line
678-
.filter((symbol) => symbol.location.range.start.line !== params.position.line)
684+
.filter(
685+
(symbol) =>
686+
symbol.location.uri !== currentUri ||
687+
symbol.location.range.start.line !== params.position.line,
688+
)
679689
.map((symbol: LSP.SymbolInformation) =>
680690
this.getDocumentationForSymbol({ currentUri, symbol }),
681691
)

0 commit comments

Comments
 (0)