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