[vscode-go] src/diagnostics: refactor deduplication and priority hierarchy

1 view
Skip to first unread message

Hongxiang Jiang (Gerrit)

unread,
Aug 2, 2026, 9:57:18 PM (5 days ago) Aug 2
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Hongxiang Jiang has uploaded the change for review

Commit message

src/diagnostics: refactor deduplication and priority hierarchy

Refactor diagnostic deduplication across gopls, build, vet, and
lint collections to use a unified priority-ordered cascade (gopls
> build > vet > lint).

Incoming diagnostics now filter themselves against higher-priority
sources before publishing, and then evict colliding diagnostics
from lower-priority collections.
Change-Id: I9ded88485928bcb4de34b65032c1e7cf40b84cd7

Change diff

diff --git a/extension/src/diagnostics/diagnostics.ts b/extension/src/diagnostics/diagnostics.ts
index 5ec0590..ae87136 100644
--- a/extension/src/diagnostics/diagnostics.ts
+++ b/extension/src/diagnostics/diagnostics.ts
@@ -86,52 +86,64 @@
}

collection?.clear();
+
+ // Ordered from highest priority (#0) to lowest priority (#3)
+ const prioritized: (vscode.DiagnosticCollection | undefined)[] = [
+ goCtx.languageClient?.diagnostics, // #0 Gopls (Highest)
+ goCtx.buildDiagnosticCollection, // #1 Build
+ goCtx.vetDiagnosticCollection, // #2 Vet
+ goCtx.lintDiagnosticCollection // #3 Lint (Lowest)
+ ];
+
+ // Find the current collection's rank (if unknown/custom, treat as lowest priority)
+ const rank = collection ? prioritized.indexOf(collection) : -1;
+ if (rank === -1) {
+ return; // the collection is disposed, no longer managed by the go extension.
+ }
+
for (const [uriStr, fileDiags] of diagsMap) {
let diags = fileDiags;
const uri = vscode.Uri.parse(uriStr);

- const { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection, languageClient } = goCtx;
- if (collection === buildDiagnosticCollection) {
- // If there are lint/vet warnings on current file, remove the ones
- // co-inciding with the new build errors.
- removeDuplicateDiagnostics(lintDiagnosticCollection, uri, diags);
- removeDuplicateDiagnostics(vetDiagnosticCollection, uri, diags);
- } else if (buildDiagnosticCollection && buildDiagnosticCollection.has(uri)) {
- // If there are build errors on current file, ignore the new lint/vet
- // warnings co-inciding with them.
- diags = deDupeDiagnostics(buildDiagnosticCollection.get(uri)!.slice(), diags);
+ for (let i = 0; i < prioritized.length; i++) {
+ const other = prioritized[i];
+
+ if (i < rank) {
+ // 1. Upstream: filter incoming diagnostics against higher-priority source
+ if (other?.has(uri)) {
+ diags = filterCollisions(diags, other.get(uri)!);
+ }
+ } else if (i === rank) {
+ // 2. Publish: all higher-priority filtering is done, publish now
+ collection?.set(uri, diags);
+ } else {
+ // 3. Downstream: evict colliding diagnostics from lower-priority collection
+ if (other?.has(uri)) {
+ other.set(uri, filterCollisions(other.get(uri)!, diags));
+ }
+ }
}
- // If there are errors from the language client that are on the current file,
- // ignore the warnings co-inciding with them.
- if (languageClient && languageClient.diagnostics?.has(uri)) {
- diags = deDupeDiagnostics(languageClient.diagnostics.get(uri)!.slice(), diags);
- }
- collection?.set(uri, diags);
}
}

/**
- * Removes any diagnostics in collection, where there is a diagnostic in
- * newDiagnostics on the same line in fileUri.
+ * Returns targetDiags with any diagnostics that coincide on the same line
+ * with a diagnostic in maskingDiags removed.
*/
-export function removeDuplicateDiagnostics(
- collection: vscode.DiagnosticCollection | undefined,
- fileUri: vscode.Uri,
- newDiagnostics: vscode.Diagnostic[]
-) {
- if (collection && collection.has(fileUri)) {
- collection.set(fileUri, deDupeDiagnostics(newDiagnostics, collection.get(fileUri)!.slice()));
- }
-}
-
-/**
- * Removes any diagnostics in otherDiagnostics, where there is a diagnostic in
- * buildDiagnostics on the same line.
- */
-function deDupeDiagnostics(
- buildDiagnostics: vscode.Diagnostic[],
- otherDiagnostics: vscode.Diagnostic[]
+export function filterCollisions(
+ targetDiags: readonly vscode.Diagnostic[],
+ maskingDiags: readonly vscode.Diagnostic[]
): vscode.Diagnostic[] {
- const buildDiagnosticsLines = buildDiagnostics.map((x) => x.range.start.line);
- return otherDiagnostics.filter((x) => buildDiagnosticsLines.indexOf(x.range.start.line) === -1);
+ const lines = new Set<number>();
+ for (const diag of maskingDiags) {
+ lines.add(diag.range.start.line);
+ }
+
+ const deduped: vscode.Diagnostic[] = [];
+ for (const diag of targetDiags) {
+ if (!lines.has(diag.range.start.line)) {
+ deduped.push(diag);
+ }
+ }
+ return deduped;
}
diff --git a/extension/src/language/goLanguageServer.ts b/extension/src/language/goLanguageServer.ts
index f6770d5..1c7c106 100644
--- a/extension/src/language/goLanguageServer.ts
+++ b/extension/src/language/goLanguageServer.ts
@@ -50,7 +50,7 @@
getCurrentGoPath,
getGoVersion,
getWorkspaceFolderPath,
- removeDuplicateDiagnostics
+ filterCollisions
} from '../util';
import { getToolFromToolPath } from '../utils/pathUtils';
import fetch from 'node-fetch';
@@ -695,11 +695,15 @@
diagnostics: vscode.Diagnostic[],
next: HandleDiagnosticsSignature
) => {
- const { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection } = goCtx;
- // Deduplicate diagnostics with those found by the other tools.
- removeDuplicateDiagnostics(vetDiagnosticCollection, uri, diagnostics);
- removeDuplicateDiagnostics(buildDiagnosticCollection, uri, diagnostics);
- removeDuplicateDiagnostics(lintDiagnosticCollection, uri, diagnostics);
+ for (const collection of [
+ goCtx.buildDiagnosticCollection,
+ goCtx.vetDiagnosticCollection,
+ goCtx.lintDiagnosticCollection
+ ]) {
+ if (collection?.has(uri)) {
+ collection.set(uri, filterCollisions(collection.get(uri)!, diagnostics));
+ }
+ }

return next(uri, diagnostics);
},
diff --git a/extension/src/util.ts b/extension/src/util.ts
index 7042170..bd8308a 100644
--- a/extension/src/util.ts
+++ b/extension/src/util.ts
@@ -27,7 +27,7 @@
} from './utils/pathUtils';
import { killProcessTree } from './utils/processUtils';
import { ICheckResult } from './diagnostics/diagnostics';
-export { ICheckResult, handleErrors, removeDuplicateDiagnostics } from './diagnostics/diagnostics';
+export { ICheckResult, handleErrors, filterCollisions } from './diagnostics/diagnostics';

export class GoVersion {
public sv?: semver.SemVer;
diff --git a/extension/test/integration/utils.test.ts b/extension/test/integration/utils.test.ts
index 25d7b54..a78074f 100644
--- a/extension/test/integration/utils.test.ts
+++ b/extension/test/integration/utils.test.ts
@@ -7,7 +7,7 @@

import assert from 'assert';
import * as vscode from 'vscode';
-import { GoVersion, removeDuplicateDiagnostics, substituteEnv } from '../../src/util';
+import { GoVersion, filterCollisions, substituteEnv } from '../../src/util';
import path = require('path');
import { toolExecutionEnvironment } from '../../src/goEnv';
import sinon = require('sinon');
@@ -57,16 +57,9 @@
});
});

-suite('Duplicate Diagnostics Tests', () => {
- test('remove duplicate diagnostics', async () => {
- const fixturePath = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
- const uri1 = vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_1.go'));
- const uri2 = vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_2.go'));
-
- const diagnosticCollection = vscode.languages.createDiagnosticCollection('linttest');
-
- // Populate the diagnostic collection
- const diag1 = [
+suite('Diagnostic Deduplication Tests', () => {
+ test('filterCollisions removes duplicate diagnostics on same line', async () => {
+ const targetDiagnostics = [
new vscode.Diagnostic(
new vscode.Range(1, 2, 1, 3),
'first line diagnostic',
@@ -84,36 +77,8 @@
vscode.DiagnosticSeverity.Warning
)
];
- const diag2 = [
- new vscode.Diagnostic(
- new vscode.Range(1, 2, 1, 3),
- 'first line diagnostic',
- vscode.DiagnosticSeverity.Warning
- ),
- new vscode.Diagnostic(
- new vscode.Range(2, 0, 2, 3),
- 'second 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(4, 0, 4, 3),
- 'fourth line diagnostic',
- vscode.DiagnosticSeverity.Warning
- )
- ];
- diagnosticCollection.set(uri1, diag1);
- diagnosticCollection.set(uri2, diag2);

- // After removing diagnostics from uri1, there should only be one diagnostic remaining, and
- // the diagnostics for uri2 should not be changed.
- const want1 = [diag1[3]];
- const want2: vscode.Diagnostic[] = [];
- diag2.forEach((diag) => {
- want2.push(diag);
- });
-
- const newDiagnostics: vscode.Diagnostic[] = [
+ const maskingDiagnostics = [
new vscode.Diagnostic(
new vscode.Range(1, 2, 1, 3),
'first line diagnostic',
@@ -122,17 +87,11 @@
new vscode.Diagnostic(new vscode.Range(2, 3, 2, 5), 'second line error', vscode.DiagnosticSeverity.Error)
];

- removeDuplicateDiagnostics(diagnosticCollection, uri1, newDiagnostics);
+ const result = filterCollisions(targetDiagnostics, maskingDiagnostics);

- assert.strictEqual(diagnosticCollection.get(uri1)?.length, want1.length);
- for (let i = 0; i < want1.length; i++) {
- assert.strictEqual(diagnosticCollection.get(uri1)?.[i], want1[i]);
- }
-
- assert.strictEqual(diagnosticCollection.get(uri2)?.length, want2.length);
- for (let i = 0; i < want2.length; i++) {
- assert.strictEqual(diagnosticCollection.get(uri2)?.[i], want2[i]);
- }
+ // Diagnostics on line 1 and 2 are masked; only line 4 remains.
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(result[0], targetDiagnostics[3]);
});
});

Change information

Files:
  • M extension/src/diagnostics/diagnostics.ts
  • M extension/src/language/goLanguageServer.ts
  • M extension/src/util.ts
  • M extension/test/integration/utils.test.ts
Change size: M
Delta: 4 files changed, 69 insertions(+), 94 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: I9ded88485928bcb4de34b65032c1e7cf40b84cd7
Gerrit-Change-Number: 809340
Gerrit-PatchSet: 1
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
Aug 2, 2026, 9:59:11 PM (5 days ago) Aug 2
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: I9ded88485928bcb4de34b65032c1e7cf40b84cd7
Gerrit-Change-Number: 809340
Gerrit-PatchSet: 2
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
Aug 2, 2026, 10:16:58 PM (5 days ago) Aug 2
to goph...@pubsubhelper.golang.org, Madeline Kalil, Gopher Robot, 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: I9ded88485928bcb4de34b65032c1e7cf40b84cd7
Gerrit-Change-Number: 809340
Gerrit-PatchSet: 3
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: Mon, 03 Aug 2026 02:16:54 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
8:40 PM (2 hours ago) 8:40 PM
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Hongxiang Jiang has uploaded the change for review

Commit message

src/diagnostics: refactor deduplication and priority hierarchy

Refactor diagnostic deduplication across gopls, build, vet, and
lint collections to use a unified priority-ordered cascade (gopls
> build > vet > lint).

Incoming diagnostics now filter themselves against higher-priority
sources before publishing, and then evict colliding diagnostics
from lower-priority collections. Document gopls priority handling
and lifecycle.
Change-Id: I7871dcc27c59ee7261fc45639c4a6c5f1027f685

Change diff

diff --git a/extension/src/diagnostics/diagnostics.ts b/extension/src/diagnostics/diagnostics.ts
index c1f47b8..38c1f27 100644
--- a/extension/src/diagnostics/diagnostics.ts
+++ b/extension/src/diagnostics/diagnostics.ts
@@ -21,6 +21,28 @@
severity: string;
}

+/**
+ * Publishes diagnostic errors/warnings to the target diagnostic collection while
+ * deduplicating against other diagnostic collections according to a strict priority hierarchy:
+ *
+ * #0 Language Server (gopls - languageClient.diagnostics)
+ * #1 Build (buildDiagnosticCollection)
+ * #2 Vet (vetDiagnosticCollection)
+ * #3 Lint (lintDiagnosticCollection)
+ *
+ * ### Deduplication Rules:
+ * 1. **Upstream Filtering**: When a lower-priority tool runs, any incoming diagnostic on a line
+ * that already contains a diagnostic in a higher-priority collection is ignored.
+ * 2. **Downstream Eviction**: When a higher-priority tool publishes diagnostics, any existing
+ * diagnostics in lower-priority collections on those same lines are evicted.
+ *
+ * ### Gopls Priority Handling:
+ * Gopls diagnostics are owned and managed by the language server client (`languageClient.diagnostics`).
+ * Gopls diagnostic events are intercepted in {@link GoDiagnosticsFeature.addMiddleware}
+ * via `middleware.handleDiagnostics`. When gopls publishes new diagnostics, the middleware
+ * evicts colliding lines from `buildDiagnosticCollection`, `vetDiagnosticCollection`, and
+ * `lintDiagnosticCollection`, establishing gopls as the highest priority diagnostic source.
+ */
export function handleErrors(
goCtx: GoExtensionContext,
document: vscode.TextDocument | undefined,
@@ -92,55 +114,72 @@
diagsMap.set(uri, diags);
}

+ const { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection, languageClient } = goCtx;
+
+ // Priority hierarchy from highest to lowest:
+ // #0: Language Client (gopls)
+ // #1: Build
+ // #2: Vet
+ // #3: Lint

+ const prioritized: (vscode.DiagnosticCollection | undefined)[] = [
+		languageClient?.diagnostics,
+ buildDiagnosticCollection,
+ vetDiagnosticCollection,
+ lintDiagnosticCollection
+ ];
+

+ const rank = collection ? prioritized.indexOf(collection) : -1;
+
 	collection?.clear();

for (const [uriStr, fileDiags] of diagsMap) {
let diags = fileDiags;
const uri = vscode.Uri.parse(uriStr);

- const { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection, languageClient } = goCtx;
- if (collection === buildDiagnosticCollection) {
- // If there are lint/vet warnings on current file, remove the ones
- // co-inciding with the new build errors.
- removeDuplicateDiagnostics(lintDiagnosticCollection, uri, diags);
- removeDuplicateDiagnostics(vetDiagnosticCollection, uri, diags);
- } else if (buildDiagnosticCollection && buildDiagnosticCollection.has(uri)) {
- // If there are build errors on current file, ignore the new lint/vet
- // warnings co-inciding with them.
- diags = deDupeDiagnostics(buildDiagnosticCollection.get(uri)!.slice(), diags);
+		if (rank !== -1) {
+ // Step 1 (Upstream Filtering): Filter incoming diagnostics against higher-priority collections.
+ for (let i = 0; i < rank; i++) {
+ const higher = prioritized[i];
+ if (higher?.has(uri)) {
+ diags = filterDiags(diags, higher.get(uri)!);
+ }
+ }
+
+ // Step 2 (Publish): Set filtered diagnostics in target collection.
+ collection?.set(uri, diags);
+
+ // Step 3 (Downstream Eviction): Remove colliding diagnostics from lower-priority collections.
+ for (let i = rank + 1; i < prioritized.length; i++) {
+ const lower = prioritized[i];
+ if (lower?.has(uri)) {
+ lower.set(uri, filterDiags(lower.get(uri)!, diags));
+ }
+ }
+ } else {
+ collection?.set(uri, diags);
+export function filterDiags(

+ targetDiags: readonly vscode.Diagnostic[],
+ maskingDiags: readonly vscode.Diagnostic[]
): vscode.Diagnostic[] {
- const buildDiagnosticsLines = buildDiagnostics.map((x) => x.range.start.line);
- return otherDiagnostics.filter((x) => buildDiagnosticsLines.indexOf(x.range.start.line) === -1);
+ const lines = new Set<number>();
+ for (const diag of maskingDiags) {
+ lines.add(diag.range.start.line);
+ }
+
+ const deduped: vscode.Diagnostic[] = [];
+ for (const diag of targetDiags) {
+ if (!lines.has(diag.range.start.line)) {
+ deduped.push(diag);
+ }
+ }
+ return deduped;
}

 /**
@@ -286,11 +325,15 @@

diagnostics: vscode.Diagnostic[],
next: HandleDiagnosticsSignature
) => {
-			const { buildDiagnosticCollection, lintDiagnosticCollection, vetDiagnosticCollection } = this.goCtx;

- // Deduplicate diagnostics with those found by the other tools.
- removeDuplicateDiagnostics(vetDiagnosticCollection, uri, diagnostics);
- removeDuplicateDiagnostics(buildDiagnosticCollection, uri, diagnostics);
- removeDuplicateDiagnostics(lintDiagnosticCollection, uri, diagnostics);
+ for (const collection of [
+				this.goCtx.buildDiagnosticCollection,
+ this.goCtx.vetDiagnosticCollection,
+ this.goCtx.lintDiagnosticCollection

+ ]) {
+ if (collection?.has(uri)) {
+					collection.set(uri, filterDiags(collection.get(uri)!, diagnostics));
+ }
+ }

return original ? original(uri, diagnostics, next) : next(uri, diagnostics);
};
diff --git a/extension/test/integration/diagnostics.test.ts b/extension/test/integration/diagnostics.test.ts
new file mode 100644
index 0000000..23ce9fe
--- /dev/null
+++ b/extension/test/integration/diagnostics.test.ts
@@ -0,0 +1,172 @@
+/*---------------------------------------------------------
+ * Copyright 2026 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+
+import assert from 'assert';
+import os = require('os');
+import path = require('path');
+import * as vscode from 'vscode';
+import { GoExtensionContext } from '../../src/context';
+import { handleErrors, ICheckResult } from '../../src/diagnostics/diagnostics';
+
+interface DiagnosticTestCase {
+ name: string;
+ diags: {
+ gopls?: ICheckResult[];
+ build?: ICheckResult[];
+ vet?: ICheckResult[];
+ lint?: ICheckResult[];
+ };
+ want: {
+ line: number;
+ source: string;
+ }[];
+}
+
+suite('Diagnostic consolidation', () => {
+ let goCtx: GoExtensionContext;
+
+ const fileURI = vscode.Uri.file(path.join(os.tmpdir(), 'diagnostic_priority_test.go')); // fake file
+ const filePath = fileURI.fsPath;
+
+ const testCases: DiagnosticTestCase[] = [
+ {
+ name: 'Symmetric priority masking (Gopls > Build > Vet > Lint)',
+ diags: {
+ gopls: [{ file: filePath, line: 10, msg: 'unmasked - highest priority', severity: 'warning' }],
+ build: [
+ { file: filePath, line: 10, msg: 'masked by gopls', severity: 'warning' },
+ { file: filePath, line: 20, msg: 'unmasked - no higher priority', severity: 'warning' }
+ ],
+ vet: [
+ { file: filePath, line: 10, msg: 'masked by gopls', severity: 'warning' },
+ { file: filePath, line: 20, msg: 'masked by build', severity: 'warning' },
+ { file: filePath, line: 30, msg: 'unmasked - no higher priority', severity: 'warning' }
+ ],
+ lint: [
+ { file: filePath, line: 10, msg: 'masked by gopls', severity: 'warning' },
+ { file: filePath, line: 20, msg: 'masked by build', severity: 'warning' },
+ { file: filePath, line: 30, msg: 'masked by vet', severity: 'warning' },
+ { file: filePath, line: 40, msg: 'unmasked - no higher priority', severity: 'warning' }
+ ]
+ },
+ want: [
+ { line: 10, source: 'gopls-test' },
+ { line: 20, source: 'build-test' },
+ { line: 30, source: 'vet-test' },
+ { line: 40, source: 'lint-test' }
+ ]
+ },
+ {
+ name: 'Diagnostics with columns and 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: 20, col: 12, msg: 'unmasked - no higher priority', severity: 'error' }
+ ],
+ vet: [
+ { file: filePath, line: 20, col: 12, msg: 'masked by build', severity: 'warning' },
+ { file: filePath, line: 30, col: 8, msg: 'unmasked - no higher priority', severity: 'warning' }
+ ],
+ lint: [
+ { file: filePath, line: 30, col: 8, msg: 'masked by vet', severity: 'warning' },
+ { file: filePath, line: 40, col: 15, msg: 'unmasked - no higher priority', severity: 'warning' }
+ ]
+ },
+ want: [
+ { line: 10, source: 'gopls-test' },
+ { line: 20, source: 'build-test' },
+ { line: 30, source: 'vet-test' },
+ { line: 40, source: 'lint-test' }
+ ]
+ },
+ // TODO(hxjiang): update test case once dedup based on line and column and severity
+ {
+ 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' }
+ ],
+ 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' }]
+ },
+ want: [{ line: 10, source: 'gopls-test' }]
+ },
+ // TODO(hxjiang): update test case once dedup based on line and column and severity
+ {
+ name: 'Same line and column, lower priority has higher severity',
+ diags: {
+ gopls: [{ file: filePath, line: 10, col: 5, msg: 'unmasked - highest priority', severity: 'warning' }],
+ lint: [
+ { file: filePath, line: 10, col: 5, msg: 'masked by gopls', severity: 'warning' },
+ { file: filePath, line: 10, col: 5, msg: 'masked by gopls', severity: 'error' }
+ ]
+ },
+ want: [{ line: 10, source: 'gopls-test' }]
+ }
+ ];
+
+ setup(() => {
+ goCtx = {
+ languageClient: { diagnostics: vscode.languages.createDiagnosticCollection('gopls-test') } as any,
+ buildDiagnosticCollection: vscode.languages.createDiagnosticCollection('build-test'),
+ vetDiagnosticCollection: vscode.languages.createDiagnosticCollection('vet-test'),
+ lintDiagnosticCollection: vscode.languages.createDiagnosticCollection('lint-test')
+ };
+ });
+
+ teardown(() => {
+ goCtx.languageClient?.diagnostics?.dispose();
+ goCtx.buildDiagnosticCollection?.dispose();
+ goCtx.vetDiagnosticCollection?.dispose();
+ goCtx.lintDiagnosticCollection?.dispose();
+ });
+
+ for (const tc of testCases) {
+ for (let round = 1; round <= 5; round++) {
+ test(`${tc.name} (round ${round})`, async () => {
+ const collections = [
+ { key: 'gopls' as const, collection: goCtx.languageClient!.diagnostics! },
+ { key: 'build' as const, collection: goCtx.buildDiagnosticCollection! },
+ { key: 'vet' as const, collection: goCtx.vetDiagnosticCollection! },
+ { key: 'lint' as const, collection: goCtx.lintDiagnosticCollection! }
+ ];
+
+ // Simulate 4 concurrent diagnostic providers reporting diags
+ // independently with random delays to verify eventual consistency.
+ const tasks = collections.map(({ key, collection }) => {
+ const errors = tc.diags[key] || [];
+ return new Promise<void>((resolve) => {
+ const delay = Math.floor(Math.random() * 15);
+ setTimeout(() => {
+ handleErrors(goCtx, undefined, errors, collection);
+ resolve();
+ }, delay);
+ });
+ });
+
+ // Wait for all concurrent diagnostic providers to finish reporting.
+ await Promise.all(tasks);
+
+ // Read diagnostics directly from the "PROBLEMS" tab.
+ const problems = vscode.languages.getDiagnostics(fileURI);
+ assert.strictEqual(
+ problems.length,
+ tc.want.length,
+ `[${tc.name}] Expected ${tc.want.length} diagnostics in problem tab, got ${problems.length}: ${JSON.stringify(problems.map((p) => ({ source: p.source, msg: p.message })))}`
+ );
+ const sorted = [...problems].sort((a, b) => a.range.start.line - b.range.start.line);
+ for (let i = 0; i < tc.want.length; i++) {
+ const exp = tc.want[i];
+ const act = sorted[i];
+ assert.strictEqual(act.range.start.line, exp.line - 1, `[${tc.name}] Line mismatch at index ${i}`);
+ assert.strictEqual(act.source, exp.source, `[${tc.name}] Source mismatch at index ${i}`);
+ }
+ });
+ }
+ }
+});
diff --git a/extension/test/integration/linting.test.ts b/extension/test/integration/linting.test.ts
index 25b505b..398cf25 100644
--- a/extension/test/integration/linting.test.ts
+++ b/extension/test/integration/linting.test.ts
@@ -102,13 +102,13 @@
);
const warnings = await goLint(file2.uri, config, 'package');


- const diagnosticCollection = vscode.languages.createDiagnosticCollection('linttest');
-			handleErrors({}, file2, warnings, diagnosticCollection);
+ const collection = vscode.languages.createDiagnosticCollection('linttest');
+ handleErrors({ lintDiagnosticCollection: collection }, file2, warnings, collection);

// The first diagnostic message for each file should be about the use of MixedCaps in package name.
// Both files belong to the same package name, and we want them to be identical.
- const file1Diagnostics = diagnosticCollection.get(file1.uri);
- const file2Diagnostics = diagnosticCollection.get(file2.uri);
+ const file1Diagnostics = collection.get(file1.uri);
+ const file2Diagnostics = collection.get(file2.uri);
assert(file1Diagnostics);
assert(file2Diagnostics);
assert(file1Diagnostics.length > 0);
diff --git a/extension/test/integration/utils.test.ts b/extension/test/integration/utils.test.ts
index 215ab0f..971bb62 100644
--- a/extension/test/integration/utils.test.ts
+++ b/extension/test/integration/utils.test.ts
@@ -6,7 +6,7 @@

import assert from 'assert';
import * as vscode from 'vscode';
 import { GoVersion, substituteEnv } from '../../src/util';
-import { removeDuplicateDiagnostics } from '../../src/diagnostics/diagnostics';
+import { filterDiags } from '../../src/diagnostics/diagnostics';

import path = require('path');
import { toolExecutionEnvironment } from '../../src/goEnv';
import sinon = require('sinon');
@@ -56,16 +56,9 @@

});
});

-suite('Duplicate Diagnostics Tests', () => {
- test('remove duplicate diagnostics', async () => {
- const fixturePath = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
- const uri1 = vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_1.go'));
- const uri2 = vscode.Uri.file(path.join(fixturePath, 'linterTest', 'linter_2.go'));
-
- const diagnosticCollection = vscode.languages.createDiagnosticCollection('linttest');
-
- // Populate the diagnostic collection
- const diag1 = [
+suite('Diagnostic Deduplication Tests', () => {
+	test('filterDiags removes duplicate diagnostics on same line', async () => {

+ const targetDiagnostics = [
new vscode.Diagnostic(
new vscode.Range(1, 2, 1, 3),
'first line diagnostic',
@@ -83,36 +76,8 @@
@@ -121,17 +86,11 @@

new vscode.Diagnostic(new vscode.Range(2, 3, 2, 5), 'second line error', vscode.DiagnosticSeverity.Error)
];

- removeDuplicateDiagnostics(diagnosticCollection, uri1, newDiagnostics);
+		const result = filterDiags(targetDiagnostics, maskingDiagnostics);


- assert.strictEqual(diagnosticCollection.get(uri1)?.length, want1.length);
- for (let i = 0; i < want1.length; i++) {
- assert.strictEqual(diagnosticCollection.get(uri1)?.[i], want1[i]);
- }
-
- assert.strictEqual(diagnosticCollection.get(uri2)?.length, want2.length);
- for (let i = 0; i < want2.length; i++) {
- assert.strictEqual(diagnosticCollection.get(uri2)?.[i], want2[i]);
- }
+ // Diagnostics on line 1 and 2 are masked; only line 4 remains.
+ assert.strictEqual(result.length, 1);
+ assert.strictEqual(result[0], targetDiagnostics[3]);
});
});

Change information

Files:
  • M extension/src/diagnostics/diagnostics.ts
  • A extension/test/integration/diagnostics.test.ts
  • M extension/test/integration/linting.test.ts
  • M extension/test/integration/utils.test.ts
Change size: L
Delta: 4 files changed, 270 insertions(+), 96 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: I7871dcc27c59ee7261fc45639c4a6c5f1027f685
Gerrit-Change-Number: 812101
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
8:45 PM (2 hours ago) 8:45 PM
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: I7871dcc27c59ee7261fc45639c4a6c5f1027f685
Gerrit-Change-Number: 812101
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
8:46 PM (2 hours ago) 8:46 PM
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Hongxiang Jiang uploaded new patchset

Hongxiang Jiang uploaded patch set #3 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: I7871dcc27c59ee7261fc45639c4a6c5f1027f685
Gerrit-Change-Number: 812101
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
8:47 PM (2 hours ago) 8:47 PM
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: I7871dcc27c59ee7261fc45639c4a6c5f1027f685
Gerrit-Change-Number: 812101
Gerrit-PatchSet: 3
Gerrit-Owner: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Reviewer: Hongxiang Jiang <hxj...@golang.org>
Gerrit-Comment-Date: Sat, 08 Aug 2026 00:47:08 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy

Hongxiang Jiang (Gerrit)

unread,
8:47 PM (2 hours ago) 8:47 PM
to goph...@pubsubhelper.golang.org, golang...@luci-project-accounts.iam.gserviceaccount.com, Gopher Robot, golang-co...@googlegroups.com

Hongxiang Jiang abandoned this change.

View Change

Abandoned

Hongxiang Jiang abandoned this change

Related details

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