[vscode-go] extension/src/diagnostics: de-duplicate diags based on column

2 views
Skip to first unread message

Hongxiang Jiang (Gerrit)

unread,
Aug 8, 2026, 8:38:22 PM (2 days ago) Aug 8
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Hongxiang Jiang has uploaded the change for review

Commit message

extension/src/diagnostics: de-duplicate diags based on column

Based on my researches based on the last 5 minor versions of all
third party linter we support, i.e. golangci-lint golangci-lint-v2,
staticcheck, revive and firt party tool (i.e.t go vet, go build,
gopls), all tools report diagnostics based on file + line + column.

For golang/go#3511
Change-Id: I0470770918a4129591a30549d831b8db48cd771a

Change diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d15848..ea5920e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,10 @@

## Unreleased

-vscode-go now supports [v3.18](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification) of the Language Server Protocol.
+* vscode-go now supports [v3.18](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification) of the Language Server Protocol.
+* Improved diagnostic deduplication across `gopls`, build, vet, and linters ([#3511](https://github.com/golang/vscode-go/issues/3511)):
+ - Deduplication now checks line, start column, and severity level.
+ - Diagnostics on different columns of the same line can now coexist.

## v0.57.0 (prerelease)

diff --git a/extension/src/diagnostics/diagnostics.ts b/extension/src/diagnostics/diagnostics.ts
index 5f24019..96a0b02 100644
--- a/extension/src/diagnostics/diagnostics.ts
+++ b/extension/src/diagnostics/diagnostics.ts
@@ -17,16 +17,17 @@
* -------------------------------------------------------------------------------------
* ### Priority Hierarchy & Deduplication Rules
* -------------------------------------------------------------------------------------
- * Diagnostics are deduplicated on a per-line basis using two symmetric rules:
+ * Diagnostics are deduplicated on a per-(line, start column) basis using two symmetric rules:
*
* 1. Upstream Filtering:
- * When a lower-priority tool runs, any incoming diagnostic on a line that already
- * contains a diagnostic from a higher-priority source with equal or higher severity
- * is ignored. If the incoming diagnostic is strictly more severe, it is surfaced.
+ * When a lower-priority tool runs, any incoming diagnostic on a line and start column
+ * that already contains a diagnostic from a higher-priority source with equal or higher
+ * severity is ignored. If the incoming diagnostic is strictly more severe, it is surfaced.
*
* 2. Downstream Eviction:
* When a higher-priority source publishes diagnostics, any existing diagnostics
- * from lower-priority sources on those same lines with equal or lower severity are evicted.
+ * from lower-priority sources on those same line and start column positions with equal
+ * or lower severity are evicted.
*
* -------------------------------------------------------------------------------------
* ### Runtime Modes
@@ -248,29 +249,31 @@
}

/**
- * Returns targetDiags with any diagnostics that coincide on the same line
- * with a diagnostic in maskingDiags of equal or higher severity removed.
+ * Returns targetDiags with any diagnostics that coincide on the same line and
+ * start column with a diagnostic in maskingDiags of equal or higher severity
+ * removed.
*
* Diagnostics from targetDiags that are strictly more severe than all masking
- * diagnostics on the same line are preserved.
+ * diagnostics at the same line and start column are preserved.
*/
export function filterDiags(
targetDiags: readonly vscode.Diagnostic[],
maskingDiags: readonly vscode.Diagnostic[]
): vscode.Diagnostic[] {
- // Max severity for each line number.
- const maxSeverity = new Map<number, vscode.DiagnosticSeverity>();
+ // Max severity for each (line, start character) position.
+ const maxSeverity = new Map<string, vscode.DiagnosticSeverity>();
for (const diag of maskingDiags) {
- const line = diag.range.start.line;
- const current = maxSeverity.get(line);
+ const key = `${diag.range.start.line}:${diag.range.start.character}`;
+ const current = maxSeverity.get(key);
if (current === undefined || diag.severity < current) {
- maxSeverity.set(line, diag.severity);
+ maxSeverity.set(key, diag.severity);
}
}

const deduped: vscode.Diagnostic[] = [];
for (const diag of targetDiags) {
- const maxMaskingSeverity = maxSeverity.get(diag.range.start.line);
+ const key = `${diag.range.start.line}:${diag.range.start.character}`;
+ const maxMaskingSeverity = maxSeverity.get(key);
if (maxMaskingSeverity === undefined || diag.severity < maxMaskingSeverity) {
deduped.push(diag);
}
diff --git a/extension/test/gopls/diagnostics.test.ts b/extension/test/gopls/diagnostics.test.ts
index 3c52a1b..7e26bc0 100644
--- a/extension/test/gopls/diagnostics.test.ts
+++ b/extension/test/gopls/diagnostics.test.ts
@@ -103,19 +103,23 @@
{ line: 40, source: 'lint-test', severity: vscode.DiagnosticSeverity.Warning }
]
},
- // TODO(hxjiang): update test case once dedup based on line and column
{
name: 'Same line, different columns, same severity',
diags: {
gopls: [{ file: filePath, line: 10, col: 5, msg: 'unmasked - highest priority', severity: 'error' }],
build: [
{ file: filePath, line: 10, col: 5, msg: 'masked by gopls', severity: 'error' },
- { file: filePath, line: 10, col: 15, msg: 'masked by gopls', severity: 'error' }
+ { file: filePath, line: 10, col: 15, msg: 'unmasked - no higher priority', severity: 'error' }
],
- vet: [{ file: filePath, line: 10, col: 25, msg: 'masked by gopls', severity: 'error' }],
- lint: [{ file: filePath, line: 10, col: 35, msg: 'masked by gopls', severity: 'error' }]
+ vet: [{ file: filePath, line: 10, col: 25, msg: 'unmasked - no higher priority', severity: 'error' }],
+ lint: [{ file: filePath, line: 10, col: 35, msg: 'unmasked - no higher priority', severity: 'error' }]
},
- want: [{ line: 10, source: 'gopls-test', severity: vscode.DiagnosticSeverity.Error }]
+ want: [
+ { line: 10, source: 'gopls-test', severity: vscode.DiagnosticSeverity.Error },
+ { line: 10, source: 'build-test', severity: vscode.DiagnosticSeverity.Error },
+ { line: 10, source: 'vet-test', severity: vscode.DiagnosticSeverity.Error },
+ { line: 10, source: 'lint-test', severity: vscode.DiagnosticSeverity.Error }
+ ]
},
{
name: 'Same line and column, lower priority has higher severity',
diff --git a/extension/test/integration/utils.test.ts b/extension/test/integration/utils.test.ts
index 907619e..5b4c051 100644
--- a/extension/test/integration/utils.test.ts
+++ b/extension/test/integration/utils.test.ts
@@ -56,43 +56,53 @@
});

suite('Diagnostic Deduplication Tests', () => {
- test('filterDiags removes duplicate diagnostics on same line with equal or lower severity', async () => {
+ test('filterDiags removes duplicate diagnostics on same line and start column with equal or lower severity', async () => {
const targetDiagnostics = [
new vscode.Diagnostic(
new vscode.Range(1, 2, 1, 3),
- 'first line diagnostic',
+ 'first line diagnostic (col 2)',
vscode.DiagnosticSeverity.Warning
),
new vscode.Diagnostic(
new vscode.Range(2, 0, 2, 3),
- 'second line diagnostic',
+ 'second line diagnostic (col 0)',
vscode.DiagnosticSeverity.Warning
),
- new vscode.Diagnostic(new vscode.Range(2, 3, 2, 5), 'second line error', vscode.DiagnosticSeverity.Error),
+ new vscode.Diagnostic(
+ new vscode.Range(2, 3, 2, 5),
+ 'second line error (col 3)',
+ vscode.DiagnosticSeverity.Error
+ ),
new vscode.Diagnostic(
new vscode.Range(4, 0, 4, 3),
- 'fourth line diagnostic',
+ 'fourth line diagnostic (col 0)',
vscode.DiagnosticSeverity.Warning
)
];

const maskingDiagnostics = [
new vscode.Diagnostic(
- new vscode.Range(1, 2, 1, 3),
+ new vscode.Range(1, 2, 1, 10), // Same line 1, same start col 2, different end col
'first line diagnostic',
vscode.DiagnosticSeverity.Warning
),
- new vscode.Diagnostic(new vscode.Range(2, 3, 2, 5), 'second line error', vscode.DiagnosticSeverity.Error)
+ new vscode.Diagnostic(
+ new vscode.Range(2, 3, 2, 8), // Same line 2, same start col 3, different end col
+ 'second line error',
+ vscode.DiagnosticSeverity.Error
+ )
];

const result = filterDiags(targetDiagnostics, maskingDiagnostics);

- // Diagnostics on line 1 and 2 are masked; only line 4 remains.
- assert.strictEqual(result.length, 1);
- assert.strictEqual(result[0], targetDiagnostics[3]);
+ // Diagnostics on (line 1, col 2) and (line 2, col 3) are masked.
+ // (line 2, col 0) and (line 4, col 0) remain because (line 2, col 0) has a different start column.
+ assert.strictEqual(result.length, 2);
+ assert.strictEqual(result[0], targetDiagnostics[1]);
+ assert.strictEqual(result[1], targetDiagnostics[3]);
});

- test('filterDiags preserves lower priority diagnostics if they have strictly higher severity', async () => {
+ test('filterDiags preserves lower priority diagnostics on same line and start column if they have strictly higher severity', async () => {
const targetDiagnostics = [
new vscode.Diagnostic(
new vscode.Range(1, 2, 1, 3),
@@ -108,7 +118,7 @@

const maskingDiagnostics = [
new vscode.Diagnostic(
- new vscode.Range(1, 0, 1, 5),
+ new vscode.Range(1, 2, 1, 5),
'first line warning (higher priority, lower severity)',
vscode.DiagnosticSeverity.Warning
),
@@ -121,8 +131,8 @@

const result = filterDiags(targetDiagnostics, maskingDiagnostics);

- // Line 1 Error is preserved because Error (0) is more severe than Warning (1).
- // Line 2 Warning is masked because Warning (1) is not more severe than Warning (1).
+ // (line 1, col 2) Error is preserved because Error (0) is more severe than Warning (1).
+ // (line 2, col 0) Warning is masked because Warning (1) is not more severe than Warning (1).
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0], targetDiagnostics[0]);
});

Change information

Files:
  • M CHANGELOG.md
  • M extension/src/diagnostics/diagnostics.ts
  • M extension/test/gopls/diagnostics.test.ts
  • M extension/test/integration/utils.test.ts
Change size: M
Delta: 4 files changed, 54 insertions(+), 34 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: vscode-go
Gerrit-Branch: master
Gerrit-Change-Id: I0470770918a4129591a30549d831b8db48cd771a
Gerrit-Change-Number: 812380
Gerrit-PatchSet: 1
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
Aug 8, 2026, 8:39:07 PM (2 days ago) Aug 8
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Hongxiang Jiang voted Commit-Queue+1

Commit-Queue+1
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: vscode-go
Gerrit-Branch: master
Gerrit-Change-Id: I0470770918a4129591a30549d831b8db48cd771a
Gerrit-Change-Number: 812380
Gerrit-PatchSet: 1
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Comment-Date: Sun, 09 Aug 2026 00:39:02 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
Aug 8, 2026, 8:39:39 PM (2 days ago) Aug 8
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Hongxiang Jiang uploaded new patchset

Hongxiang Jiang uploaded patch set #2 to this change.
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newpatchset
Gerrit-Project: vscode-go
Gerrit-Branch: master
Gerrit-Change-Id: I0470770918a4129591a30549d831b8db48cd771a
Gerrit-Change-Number: 812380
Gerrit-PatchSet: 2
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Hongxiang Jiang <hxj...@golang.org>
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
Aug 9, 2026, 3:36:25 PM (14 hours ago) Aug 9
to goph...@pubsubhelper.golang.org, Madeline Kalil, Gopher Robot, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com
Attention needed from Madeline Kalil

Hongxiang Jiang voted Commit-Queue+1

Commit-Queue+1
Open in Gerrit

Related details

Attention is currently required from:
  • Madeline Kalil
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: vscode-go
Gerrit-Branch: master
Gerrit-Change-Id: I0470770918a4129591a30549d831b8db48cd771a
Gerrit-Change-Number: 812380
Gerrit-PatchSet: 2
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
Gerrit-CC: Gopher Robot <go...@golang.org>
Gerrit-Attention: Madeline Kalil <mka...@google.com>
Gerrit-Comment-Date: Sun, 09 Aug 2026 19:36:21 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy
Reply all
Reply to author
Forward
0 new messages