Skip to content

Commit 5e319f3

Browse files
committed
Simplify code action fingerprinting
This solves an issue with some clients returning diagnostics with properties sorted differently than on node... Embarassing and silly bug.
1 parent 7e66c75 commit 5e319f3

File tree

4 files changed

+82
-94
lines changed

4 files changed

+82
-94
lines changed

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

+24-23
Original file line numberDiff line numberDiff line change
@@ -177,29 +177,30 @@ describe('server', () => {
177177
const fixableDiagnostic = diagnostics.filter(({ code }) => code === 'SC2086')[0]
178178

179179
expect(fixableDiagnostic).toMatchInlineSnapshot(`
180-
Object {
181-
"code": "SC2086",
182-
"codeDescription": Object {
183-
"href": "https://www.shellcheck.net/wiki/SC2086",
184-
},
185-
"message": "Double quote to prevent globbing and word splitting.",
186-
"range": Object {
187-
"end": Object {
188-
"character": 13,
189-
"line": 55,
190-
},
191-
"start": Object {
192-
"character": 5,
193-
"line": 55,
194-
},
195-
},
196-
"severity": 3,
197-
"source": "shellcheck",
198-
"tags": undefined,
199-
}
200-
`)
201-
202-
// TODO: we could find the diagnostics and then use the range to test the code action
180+
Object {
181+
"code": "SC2086",
182+
"codeDescription": Object {
183+
"href": "https://www.shellcheck.net/wiki/SC2086",
184+
},
185+
"data": Object {
186+
"id": "shellcheck|2086|55:5-55:13",
187+
},
188+
"message": "Double quote to prevent globbing and word splitting.",
189+
"range": Object {
190+
"end": Object {
191+
"character": 13,
192+
"line": 55,
193+
},
194+
"start": Object {
195+
"character": 5,
196+
"line": 55,
197+
},
198+
},
199+
"severity": 3,
200+
"source": "shellcheck",
201+
"tags": undefined,
202+
}
203+
`)
203204

204205
const onCodeAction = connection.onCodeAction.mock.calls[0][0]
205206

Diff for: server/src/server.ts

+10-35
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { isDeepStrictEqual } from 'node:util'
44

55
import * as TurndownService from 'turndown'
66
import * as LSP from 'vscode-languageserver/node'
7-
import { CodeAction } from 'vscode-languageserver/node'
87
import { TextDocument } from 'vscode-languageserver-textdocument'
98

109
import Analyzer from './analyser'
@@ -13,7 +12,7 @@ import * as config from './config'
1312
import Executables from './executables'
1413
import { initializeParser } from './parser'
1514
import * as ReservedWords from './reserved-words'
16-
import { Linter } from './shellcheck'
15+
import { Linter, LintingResult } from './shellcheck'
1716
import { SNIPPETS } from './snippets'
1817
import { BashCompletionItem, CompletionItemDataType } from './types'
1918
import { uniqueBasedOnHash } from './util/array'
@@ -37,7 +36,9 @@ export default class BashServer {
3736
private executables: Executables
3837
private linter?: Linter
3938
private workspaceFolder: string | null
40-
private uriToCodeActions: { [uri: string]: CodeAction[] | undefined } = {}
39+
private uriToCodeActions: {
40+
[uri: string]: LintingResult['codeActions'] | undefined
41+
} = {}
4142

4243
private constructor({
4344
analyzer,
@@ -385,41 +386,15 @@ export default class BashServer {
385386
// ==============================
386387

387388
private async onCodeAction(params: LSP.CodeActionParams): Promise<LSP.CodeAction[]> {
388-
const codeActions = this.uriToCodeActions[params.textDocument.uri]
389+
const codeActionsForUri = this.uriToCodeActions[params.textDocument.uri] || {}
389390

390-
if (!codeActions) {
391-
return []
392-
}
393-
394-
const getDiagnosticsFingerPrint = (diagnostics?: LSP.Diagnostic[]): string[] =>
395-
diagnostics
396-
?.map(({ code, source, range }) =>
397-
code !== undefined && source && range
398-
? JSON.stringify({
399-
code,
400-
source,
401-
range,
402-
})
403-
: null,
404-
)
405-
.filter((fingerPrint): fingerPrint is string => fingerPrint != null) || []
391+
const codeActions = params.context.diagnostics
392+
.map(({ data }) => codeActionsForUri[data?.id])
393+
.filter((action): action is LSP.CodeAction => action != null)
406394

407-
const paramsDiagnosticsKeys = getDiagnosticsFingerPrint(params.context.diagnostics)
408-
409-
// find actions that match the paramsDiagnosticsKeys
410-
const actions = codeActions.filter((action) => {
411-
const actionDiagnosticsKeys = getDiagnosticsFingerPrint(action.diagnostics)
412-
// actions without diagnostics are always returned
413-
if (actionDiagnosticsKeys.length === 0) {
414-
return true
415-
}
416-
417-
return actionDiagnosticsKeys.some((actionDiagnosticKey) =>
418-
paramsDiagnosticsKeys.includes(actionDiagnosticKey),
419-
)
420-
})
395+
logger.debug(`onCodeAction: found ${codeActions.length} code action(s)`)
421396

422-
return actions
397+
return codeActions
423398
}
424399

425400
private onCompletion(params: LSP.TextDocumentPositionParams): BashCompletionItem[] {

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

+23-8
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('linter', () => {
5454
})
5555

5656
expect(result).toEqual({
57-
codeActions: [],
57+
codeActions: {},
5858
diagnostics: [],
5959
})
6060

@@ -76,14 +76,17 @@ describe('linter', () => {
7676
const [result] = await getLintingResult({ document: textToDoc(shell) })
7777
expect(result).toMatchInlineSnapshot(`
7878
Object {
79-
"codeActions": Array [
80-
Object {
79+
"codeActions": Object {
80+
"shellcheck|2086|1:5-1:9": Object {
8181
"diagnostics": Array [
8282
Object {
8383
"code": "SC2086",
8484
"codeDescription": Object {
8585
"href": "https://www.shellcheck.net/wiki/SC2086",
8686
},
87+
"data": Object {
88+
"id": "shellcheck|2086|1:5-1:9",
89+
},
8790
"message": "Double quote to prevent globbing and word splitting.",
8891
"range": Object {
8992
"end": Object {
@@ -135,13 +138,16 @@ describe('linter', () => {
135138
"kind": "quickfix",
136139
"title": "Apply fix for SC2086",
137140
},
138-
],
141+
},
139142
"diagnostics": Array [
140143
Object {
141144
"code": "SC2154",
142145
"codeDescription": Object {
143146
"href": "https://www.shellcheck.net/wiki/SC2154",
144147
},
148+
"data": Object {
149+
"id": "shellcheck|2154|1:5-1:9",
150+
},
145151
"message": "foo is referenced but not assigned.",
146152
"range": Object {
147153
"end": Object {
@@ -162,6 +168,9 @@ describe('linter', () => {
162168
"codeDescription": Object {
163169
"href": "https://www.shellcheck.net/wiki/SC2086",
164170
},
171+
"data": Object {
172+
"id": "shellcheck|2086|1:5-1:9",
173+
},
165174
"message": "Double quote to prevent globbing and word splitting.",
166175
"range": Object {
167176
"end": Object {
@@ -197,7 +206,7 @@ describe('linter', () => {
197206

198207
const result = await promises[promises.length - 1]
199208
expect(result).toEqual({
200-
codeActions: [],
209+
codeActions: {},
201210
diagnostics: [],
202211
})
203212
})
@@ -209,7 +218,7 @@ describe('linter', () => {
209218
})
210219

211220
expect(result).toEqual({
212-
codeActions: [],
221+
codeActions: {},
213222
diagnostics: [],
214223
})
215224
})
@@ -222,13 +231,16 @@ describe('linter', () => {
222231

223232
expect(result).toMatchInlineSnapshot(`
224233
Object {
225-
"codeActions": Array [],
234+
"codeActions": Object {},
226235
"diagnostics": Array [
227236
Object {
228237
"code": "SC1091",
229238
"codeDescription": Object {
230239
"href": "https://www.shellcheck.net/wiki/SC1091",
231240
},
241+
"data": Object {
242+
"id": "shellcheck|1091|3:7-3:19",
243+
},
232244
"message": "Not following: shellcheck/sourced.sh: openBinaryFile: does not exist (No such file or directory)",
233245
"range": Object {
234246
"end": Object {
@@ -249,6 +261,9 @@ describe('linter', () => {
249261
"codeDescription": Object {
250262
"href": "https://www.shellcheck.net/wiki/SC2154",
251263
},
264+
"data": Object {
265+
"id": "shellcheck|2154|5:6-5:10",
266+
},
252267
"message": "foo is referenced but not assigned.",
253268
"range": Object {
254269
"end": Object {
@@ -276,7 +291,7 @@ describe('linter', () => {
276291
sourcePaths: [path.resolve(FIXTURE_FOLDER)],
277292
})
278293
expect(result).toEqual({
279-
codeActions: [],
294+
codeActions: {},
280295
diagnostics: [],
281296
})
282297
})

Diff for: server/src/shellcheck/index.ts

+25-28
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ type LinterOptions = {
2323
cwd?: string
2424
}
2525

26-
type LintingResult = {
26+
export type LintingResult = {
2727
diagnostics: LSP.Diagnostic[]
28-
codeActions: LSP.CodeAction[]
28+
codeActions: Record<string, LSP.CodeAction | undefined>
2929
}
3030

3131
export class Linter {
@@ -53,7 +53,7 @@ export class Linter {
5353
additionalShellCheckArguments: string[] = [],
5454
): Promise<LintingResult> {
5555
if (!this._canLint) {
56-
return { diagnostics: [], codeActions: [] }
56+
return { diagnostics: [], codeActions: {} }
5757
}
5858

5959
const { uri } = document
@@ -80,7 +80,7 @@ export class Linter {
8080

8181
if (shellDialect && !SUPPORTED_BASH_DIALECTS.includes(shellDialect)) {
8282
// We found a dialect that isn't supported by ShellCheck.
83-
return { diagnostics: [], codeActions: [] }
83+
return { diagnostics: [], codeActions: {} }
8484
}
8585

8686
// NOTE: that ShellCheck actually does shebang parsing, but we manually
@@ -99,7 +99,7 @@ export class Linter {
9999
)
100100

101101
if (!this._canLint) {
102-
return { diagnostics: [], codeActions: [] }
102+
return { diagnostics: [], codeActions: {} }
103103
}
104104

105105
// Clean up the debounced function
@@ -180,40 +180,37 @@ export class Linter {
180180
}
181181
}
182182

183-
function mapShellCheckResult({
184-
uri,
185-
result,
186-
}: {
187-
uri: string
188-
result: ShellCheckResult
189-
}): {
190-
diagnostics: LSP.Diagnostic[]
191-
codeActions: LSP.CodeAction[]
192-
} {
193-
const diagnostics: LSP.Diagnostic[] = []
194-
const codeActions: LSP.CodeAction[] = []
183+
function mapShellCheckResult({ uri, result }: { uri: string; result: ShellCheckResult }) {
184+
const diagnostics: LintingResult['diagnostics'] = []
185+
const codeActions: LintingResult['codeActions'] = {}
195186

196187
for (const comment of result.comments) {
197-
const start: LSP.Position = {
198-
line: comment.line - 1,
199-
character: comment.column - 1,
200-
}
201-
const end: LSP.Position = {
202-
line: comment.endLine - 1,
203-
character: comment.endColumn - 1,
204-
}
188+
const range = LSP.Range.create(
189+
{
190+
line: comment.line - 1,
191+
character: comment.column - 1,
192+
},
193+
{
194+
line: comment.endLine - 1,
195+
character: comment.endColumn - 1,
196+
},
197+
)
198+
199+
const id = `shellcheck|${comment.code}|${range.start.line}:${range.start.character}-${range.end.line}:${range.end.character}`
205200

206201
const diagnostic: LSP.Diagnostic = {
207202
message: comment.message,
208203
severity: LEVEL_TO_SEVERITY[comment.level] || LSP.DiagnosticSeverity.Error,
209204
code: `SC${comment.code}`,
210205
source: 'shellcheck',
211-
range: LSP.Range.create(start, end),
206+
range,
212207
codeDescription: {
213208
href: `https://www.shellcheck.net/wiki/SC${comment.code}`,
214209
},
215210
tags: CODE_TO_TAGS[comment.code],
216-
// NOTE: we could use the 'data' property this enable easier fingerprinting
211+
data: {
212+
id,
213+
},
217214
}
218215

219216
diagnostics.push(diagnostic)
@@ -225,7 +222,7 @@ function mapShellCheckResult({
225222
})
226223

227224
if (codeAction) {
228-
codeActions.push(codeAction)
225+
codeActions[id] = codeAction
229226
}
230227
}
231228

0 commit comments

Comments
 (0)