Skip to content

Commit 5990d5f

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 5990d5f

File tree

4 files changed

+82
-93
lines changed

4 files changed

+82
-93
lines changed

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

server/src/server.ts

+10-34
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as config from './config'
1313
import Executables from './executables'
1414
import { initializeParser } from './parser'
1515
import * as ReservedWords from './reserved-words'
16-
import { Linter } from './shellcheck'
16+
import { Linter, LintingResult } from './shellcheck'
1717
import { SNIPPETS } from './snippets'
1818
import { BashCompletionItem, CompletionItemDataType } from './types'
1919
import { uniqueBasedOnHash } from './util/array'
@@ -37,7 +37,9 @@ export default class BashServer {
3737
private executables: Executables
3838
private linter?: Linter
3939
private workspaceFolder: string | null
40-
private uriToCodeActions: { [uri: string]: CodeAction[] | undefined } = {}
40+
private uriToCodeActions: {
41+
[uri: string]: LintingResult['codeActions'] | undefined
42+
} = {}
4143

4244
private constructor({
4345
analyzer,
@@ -385,41 +387,15 @@ export default class BashServer {
385387
// ==============================
386388

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

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) || []
392+
const codeActions = params.context.diagnostics
393+
.map(({ data }) => codeActionsForUri[data?.id])
394+
.filter((action): action is LSP.CodeAction => action != null)
406395

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-
})
396+
logger.debug(`onCodeAction: found ${codeActions.length} code action(s)`)
421397

422-
return actions
398+
return codeActions
423399
}
424400

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

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
})

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)