Skip to content

Commit f1f5a94

Browse files
X-GuardianSimon Heather
andauthored
Add yaml-lint-disable comment to suppress diagnostics per-line (redhat-developer#1189)
* Add yaml-lint-disable comment to suppress diagnostics per-line * Update tests * Rename yaml-lint-disable to yaml-language-server-disable * Fix formatting --------- Co-authored-by: Simon Heather <simon.heather@yulife.com>
1 parent 97effb9 commit f1f5a94

File tree

5 files changed

+409
-1
lines changed

5 files changed

+409
-1
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,33 @@ The following settings are supported:
5656
- `yaml.style.flowSequence` : Forbids flow style sequences if set to `forbid`
5757
- `yaml.keyOrdering` : Enforces alphabetical ordering of keys in mappings when set to `true`. Default is `false`
5858

59+
## Suppressing diagnostics
60+
61+
You can suppress specific validation warnings on a per-line basis by adding a `# yaml-language-server-disable` comment on the line immediately before the one producing the diagnostic.
62+
63+
### Suppress all diagnostics on a line
64+
65+
```yaml
66+
# yaml-language-server-disable
67+
version: 123
68+
```
69+
70+
### Suppress only specific diagnostics
71+
72+
Provide one or more message substrings (comma-separated, case-insensitive). Only diagnostics whose message contains a matching substring will be suppressed; the rest are kept.
73+
74+
```yaml
75+
# yaml-language-server-disable Incorrect type
76+
version: 123
77+
```
78+
79+
```yaml
80+
# yaml-language-server-disable Incorrect type, not accepted
81+
version: 123
82+
```
83+
84+
The substrings are matched against the diagnostic message text reported by the language server.
85+
5986
##### Adding custom tags
6087
6188
In order to use the custom tags in your YAML file you need to first specify the custom tags in the setting of your code editor. For example, we can have the following custom tags:

src/languageservice/services/yamlValidation.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
1313
import { JSONValidation } from 'vscode-json-languageservice/lib/umd/services/jsonValidation';
1414
import { YAML_SOURCE } from '../parser/schemaValidation/baseValidator';
1515
import { TextBuffer } from '../utils/textBuffer';
16+
import { filterSuppressedDiagnostics } from '../utils/diagnostic-filter';
1617
import { yamlDocumentsCache } from '../parser/yaml-documents';
1718
import { Telemetry } from '../telemetry';
1819
import { AdditionalValidator } from './validation/types';
@@ -152,7 +153,18 @@ export class YAMLValidation {
152153
}
153154
}
154155

155-
return duplicateMessagesRemoved;
156+
const textBuffer = new TextBuffer(textDocument);
157+
return filterSuppressedDiagnostics(
158+
duplicateMessagesRemoved,
159+
(d) => d.range.start.line,
160+
(d) => d.message,
161+
(line) => {
162+
if (line < 0 || line >= textBuffer.getLineCount()) {
163+
return undefined;
164+
}
165+
return textBuffer.getLineContent(line).replace(/[\r\n]+$/, '');
166+
}
167+
);
156168
}
157169
private runAdditionalValidators(document: TextDocument, yarnDoc: SingleYAMLDocument): Diagnostic[] {
158170
const result = [];
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* Pattern that matches a `# yaml-language-server-disable` comment.
8+
*
9+
* Usage in YAML files:
10+
*
11+
* - `# yaml-language-server-disable` - suppress ALL diagnostics on the next line
12+
* - `# yaml-language-server-disable Incorrect type` - suppress diagnostics whose message contains "Incorrect type"
13+
* - `# yaml-language-server-disable Incorrect type, not accepted` - suppress diagnostics matching any of the substrings
14+
*
15+
* Capture group 1 (optional) contains the comma-separated list of message
16+
* substrings to match against. If absent, all diagnostics are suppressed.
17+
*/
18+
export const YAML_DISABLE_PATTERN = /^\s*#\s*yaml-language-server-disable\b(.*)$/;
19+
20+
/**
21+
* A callback that returns the text content of a given zero-based line number,
22+
* or `undefined` if the line does not exist.
23+
*/
24+
export type GetLineText = (line: number) => string | undefined;
25+
26+
/**
27+
* Parse the text after `yaml-language-server-disable` into an array of trimmed,
28+
* lower-cased message substrings. Returns an empty array when no
29+
* specifiers are provided (meaning "suppress all").
30+
*/
31+
export function parseDisableSpecifiers(raw: string): string[] {
32+
const trimmed = raw.trim();
33+
if (!trimmed) {
34+
return [];
35+
}
36+
return trimmed
37+
.split(',')
38+
.map((s) => s.trim().toLowerCase())
39+
.filter((s) => s.length > 0);
40+
}
41+
42+
/**
43+
* Determine whether a diagnostic should be suppressed based on the
44+
* specifiers from a `# yaml-language-server-disable` comment.
45+
*
46+
* @param specifiers - Parsed specifiers (empty means suppress all).
47+
* @param diagnosticMessage - The diagnostic's message text.
48+
* @returns `true` if the diagnostic should be suppressed.
49+
*/
50+
export function shouldSuppressDiagnostic(specifiers: string[], diagnosticMessage: string): boolean {
51+
if (specifiers.length === 0) {
52+
return true;
53+
}
54+
const lowerMessage = diagnosticMessage.toLowerCase();
55+
return specifiers.some((spec) => lowerMessage.includes(spec));
56+
}
57+
58+
/**
59+
* Filters an array of diagnostics, removing any whose starting line is
60+
* immediately preceded by a `# yaml-language-server-disable` comment.
61+
*
62+
* When the comment includes one or more comma-separated message substrings,
63+
* only diagnostics whose message contains at least one of those substrings
64+
* (case-insensitive) are suppressed. Without specifiers, all diagnostics
65+
* on the next line are suppressed.
66+
*
67+
* @param diagnostics - The diagnostics to filter.
68+
* @param getStartLine - Extracts the zero-based starting line number from a diagnostic.
69+
* @param getMessage - Extracts the message string from a diagnostic.
70+
* @param getLineText - Returns the text of a document line by its zero-based index,
71+
* or `undefined` if the line is out of range.
72+
* @returns A new array containing only the diagnostics that are not suppressed.
73+
*/
74+
export function filterSuppressedDiagnostics<T>(
75+
diagnostics: T[],
76+
getStartLine: (diag: T) => number,
77+
getMessage: (diag: T) => string,
78+
getLineText: GetLineText
79+
): T[] {
80+
return diagnostics.filter((diag) => {
81+
const line = getStartLine(diag);
82+
if (line === 0) {
83+
return true;
84+
}
85+
const prevLineText = getLineText(line - 1);
86+
if (prevLineText === undefined) {
87+
return true;
88+
}
89+
const match = YAML_DISABLE_PATTERN.exec(prevLineText);
90+
if (!match) {
91+
return true;
92+
}
93+
const specifiers = parseDisableSpecifiers(match[1]);
94+
return !shouldSuppressDiagnostic(specifiers, getMessage(diag));
95+
});
96+
}

test/diagnostic-filter.test.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { expect } from 'chai';
7+
import {
8+
filterSuppressedDiagnostics,
9+
YAML_DISABLE_PATTERN,
10+
parseDisableSpecifiers,
11+
shouldSuppressDiagnostic,
12+
GetLineText,
13+
} from '../src/languageservice/utils/diagnostic-filter';
14+
15+
function makeDiag(startLine: number, message: string): { startLine: number; message: string } {
16+
return { startLine, message };
17+
}
18+
19+
function linesOf(lines: string[]): GetLineText {
20+
return (line: number) => (line >= 0 && line < lines.length ? lines[line] : undefined);
21+
}
22+
23+
describe('YAML_DISABLE_PATTERN', () => {
24+
it('should capture specifiers in group 1', () => {
25+
const match = YAML_DISABLE_PATTERN.exec('# yaml-language-server-disable Incorrect type, not accepted');
26+
expect(match).to.not.be.null;
27+
expect(match[1].trim()).to.equal('Incorrect type, not accepted');
28+
});
29+
30+
it('should capture empty group 1 when no specifiers given', () => {
31+
const match = YAML_DISABLE_PATTERN.exec('# yaml-language-server-disable');
32+
expect(match).to.not.be.null;
33+
expect(match[1].trim()).to.equal('');
34+
});
35+
});
36+
37+
describe('parseDisableSpecifiers', () => {
38+
it('should return empty array for empty string', () => {
39+
expect(parseDisableSpecifiers('')).to.deep.equal([]);
40+
});
41+
42+
it('should return empty array for whitespace-only string', () => {
43+
expect(parseDisableSpecifiers(' ')).to.deep.equal([]);
44+
});
45+
46+
it('should parse a single specifier', () => {
47+
expect(parseDisableSpecifiers('Incorrect type')).to.deep.equal(['incorrect type']);
48+
});
49+
50+
it('should parse comma-separated specifiers', () => {
51+
expect(parseDisableSpecifiers('Incorrect type, not accepted')).to.deep.equal(['incorrect type', 'not accepted']);
52+
});
53+
54+
it('should trim whitespace around specifiers', () => {
55+
expect(parseDisableSpecifiers(' foo , bar ')).to.deep.equal(['foo', 'bar']);
56+
});
57+
58+
it('should ignore empty entries from trailing commas', () => {
59+
expect(parseDisableSpecifiers('foo,')).to.deep.equal(['foo']);
60+
});
61+
62+
it('should lower-case all specifiers', () => {
63+
expect(parseDisableSpecifiers('Value Is NOT Accepted')).to.deep.equal(['value is not accepted']);
64+
});
65+
});
66+
67+
describe('shouldSuppressDiagnostic', () => {
68+
it('should suppress when specifiers is empty (suppress all)', () => {
69+
expect(shouldSuppressDiagnostic([], 'any message')).to.be.true;
70+
});
71+
72+
it('should suppress when message contains the specifier (case-insensitive)', () => {
73+
expect(shouldSuppressDiagnostic(['incorrect type'], 'Incorrect type. Expected string.')).to.be.true;
74+
});
75+
76+
it('should not suppress when message does not contain the specifier', () => {
77+
expect(shouldSuppressDiagnostic(['not accepted'], 'Incorrect type. Expected string.')).to.be.false;
78+
});
79+
80+
it('should suppress when any of multiple specifiers matches', () => {
81+
expect(shouldSuppressDiagnostic(['not accepted', 'incorrect type'], 'Incorrect type. Expected string.')).to.be.true;
82+
});
83+
84+
it('should not suppress when none of multiple specifiers match', () => {
85+
expect(shouldSuppressDiagnostic(['not accepted', 'missing property'], 'Incorrect type. Expected string.')).to.be.false;
86+
});
87+
});
88+
89+
describe('filterSuppressedDiagnostics', () => {
90+
const filter = (diagnostics: ReturnType<typeof makeDiag>[], lines: GetLineText): ReturnType<typeof makeDiag>[] =>
91+
filterSuppressedDiagnostics(
92+
diagnostics,
93+
(d) => d.startLine,
94+
(d) => d.message,
95+
lines
96+
);
97+
98+
it('should return all diagnostics when there are no suppression comments', () => {
99+
const lines = linesOf(['key: value', 'other: 123']);
100+
const diagnostics = [makeDiag(0, 'error on line 0'), makeDiag(1, 'error on line 1')];
101+
102+
const result = filter(diagnostics, lines);
103+
104+
expect(result).to.have.length(2);
105+
});
106+
107+
it('should suppress all diagnostics when no specifiers are given', () => {
108+
const lines = linesOf(['name: hello', '# yaml-language-server-disable', 'age: not-a-number']);
109+
const diagnostics = [makeDiag(2, 'Incorrect type'), makeDiag(2, 'Value not accepted')];
110+
111+
const result = filter(diagnostics, lines);
112+
113+
expect(result).to.be.empty;
114+
});
115+
116+
it('should suppress only matching diagnostics when specifiers are given', () => {
117+
const lines = linesOf(['name: hello', '# yaml-language-server-disable Incorrect type', 'age: not-a-number']);
118+
const diagnostics = [makeDiag(2, 'Incorrect type. Expected string.'), makeDiag(2, 'Value is not accepted.')];
119+
120+
const result = filter(diagnostics, lines);
121+
122+
expect(result).to.have.length(1);
123+
expect(result[0].message).to.equal('Value is not accepted.');
124+
});
125+
126+
it('should suppress diagnostics matching any of multiple comma-separated specifiers', () => {
127+
const lines = linesOf(['# yaml-language-server-disable Incorrect type, not accepted', 'key: bad']);
128+
const diagnostics = [
129+
makeDiag(1, 'Incorrect type. Expected string.'),
130+
makeDiag(1, 'Value is not accepted.'),
131+
makeDiag(1, 'Missing required property "name".'),
132+
];
133+
134+
const result = filter(diagnostics, lines);
135+
136+
expect(result).to.have.length(1);
137+
expect(result[0].message).to.equal('Missing required property "name".');
138+
});
139+
140+
it('should match specifiers case-insensitively', () => {
141+
const lines = linesOf(['# yaml-language-server-disable incorrect TYPE', 'key: bad']);
142+
const diagnostics = [makeDiag(1, 'Incorrect type. Expected string.')];
143+
144+
const result = filter(diagnostics, lines);
145+
146+
expect(result).to.be.empty;
147+
});
148+
149+
it('should keep diagnostics on lines NOT preceded by a disable comment', () => {
150+
const lines = linesOf(['name: hello', '# yaml-language-server-disable', 'age: bad', 'score: bad']);
151+
const diagnostics = [makeDiag(2, 'error on line 2'), makeDiag(3, 'error on line 3')];
152+
153+
const result = filter(diagnostics, lines);
154+
155+
expect(result).to.have.length(1);
156+
expect(result[0].message).to.equal('error on line 3');
157+
});
158+
159+
it('should not filter a diagnostic on line 0 (no preceding line)', () => {
160+
const lines = linesOf(['bad: value']);
161+
const diagnostics = [makeDiag(0, 'error on first line')];
162+
163+
const result = filter(diagnostics, lines);
164+
165+
expect(result).to.have.length(1);
166+
});
167+
168+
it('should handle indented disable comments', () => {
169+
const lines = linesOf(['root:', ' # yaml-language-server-disable', ' child: bad-value']);
170+
const diagnostics = [makeDiag(2, 'invalid value')];
171+
172+
const result = filter(diagnostics, lines);
173+
174+
expect(result).to.be.empty;
175+
});
176+
177+
it('should not suppress when the disable comment is two lines above', () => {
178+
const lines = linesOf(['# yaml-language-server-disable', 'good: value', 'bad: value']);
179+
const diagnostics = [makeDiag(2, 'error on line 2')];
180+
181+
const result = filter(diagnostics, lines);
182+
183+
expect(result).to.have.length(1);
184+
});
185+
186+
it('should handle multiple disable comments for different lines', () => {
187+
const lines = linesOf([
188+
'# yaml-language-server-disable',
189+
'line1: bad',
190+
'line2: ok',
191+
'# yaml-language-server-disable',
192+
'line4: also-bad',
193+
]);
194+
const diagnostics = [makeDiag(1, 'error on line 1'), makeDiag(2, 'error on line 2'), makeDiag(4, 'error on line 4')];
195+
196+
const result = filter(diagnostics, lines);
197+
198+
expect(result).to.have.length(1);
199+
expect(result[0].message).to.equal('error on line 2');
200+
});
201+
202+
it('should return all diagnostics when the document cannot be read', () => {
203+
const noDocument: GetLineText = () => undefined;
204+
const diagnostics = [makeDiag(1, 'some error')];
205+
206+
const result = filter(diagnostics, noDocument);
207+
208+
expect(result).to.have.length(1);
209+
});
210+
211+
it('should return an empty array when given no diagnostics', () => {
212+
const lines = linesOf(['# yaml-language-server-disable', 'key: value']);
213+
214+
const result = filter([], lines);
215+
216+
expect(result).to.be.empty;
217+
});
218+
219+
it('should not treat a non-comment line containing the keyword as suppression', () => {
220+
const lines = linesOf(['key: yaml-language-server-disable', 'other: bad']);
221+
const diagnostics = [makeDiag(1, 'error on line 1')];
222+
223+
const result = filter(diagnostics, lines);
224+
225+
expect(result).to.have.length(1);
226+
});
227+
});

0 commit comments

Comments
 (0)