Skip to content

Commit 4973795

Browse files
authored
Add diagnostic and code action for unused anchor (#621)
* Add diagnostic and code action for unused anchor Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * fix review comments Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * update 'yaml' to 2.0.0-10 Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com>
1 parent 72d417c commit 4973795

File tree

18 files changed

+285
-33
lines changed

18 files changed

+285
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"vscode-languageserver-types": "^3.16.0",
4141
"vscode-nls": "^5.0.0",
4242
"vscode-uri": "^3.0.2",
43-
"yaml": "2.0.0-8"
43+
"yaml": "2.0.0-10"
4444
},
4545
"devDependencies": {
4646
"@types/chai": "^4.2.12",

src/languageserver/handlers/validationHandlers.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@ export class ValidationHandler {
5353
.doValidation(textDocument, isKubernetesAssociatedDocument(textDocument, this.yamlSettings.specificValidatorPaths))
5454
.then((diagnosticResults) => {
5555
const diagnostics = [];
56-
for (const diagnosticItem in diagnosticResults) {
57-
diagnosticResults[diagnosticItem].severity = 1; //Convert all warnings to errors
58-
diagnostics.push(diagnosticResults[diagnosticItem]);
56+
for (const diagnosticItem of diagnosticResults) {
57+
// Convert all warnings to errors
58+
if (diagnosticItem.severity === 2) {
59+
diagnosticItem.severity = 1;
60+
}
61+
diagnostics.push(diagnosticItem);
5962
}
6063

6164
const removeDuplicatesDiagnostics = removeDuplicatesObj(diagnostics);

src/languageservice/jsonASTTypes.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Node } from 'yaml';
6+
import { Node, Pair } from 'yaml';
7+
8+
export type YamlNode = Node | Pair;
79

810
export type ASTNode =
911
| ObjectASTNode
@@ -21,7 +23,7 @@ export interface BaseASTNode {
2123
readonly length: number;
2224
readonly children?: ASTNode[];
2325
readonly value?: string | boolean | number | null;
24-
readonly internalNode: Node;
26+
readonly internalNode: YamlNode;
2527
location: string;
2628
getNodeFromOffsetEndInclusive(offset: number): ASTNode;
2729
}

src/languageservice/parser/ast-converter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
Document,
2020
LineCounter,
2121
} from 'yaml';
22-
import { ASTNode } from '../jsonASTTypes';
22+
import { ASTNode, YamlNode } from '../jsonASTTypes';
2323
import {
2424
NullASTNodeImpl,
2525
PropertyASTNodeImpl,
@@ -35,7 +35,7 @@ type NodeRange = [number, number, number];
3535
const maxRefCount = 1000;
3636
let refDepth = 0;
3737

38-
export function convertAST(parent: ASTNode, node: Node, doc: Document, lineCounter: LineCounter): ASTNode {
38+
export function convertAST(parent: ASTNode, node: YamlNode, doc: Document, lineCounter: LineCounter): ASTNode {
3939
if (!parent) {
4040
// first invocation
4141
refDepth = 0;

src/languageservice/parser/jsonParser07.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
StringASTNode,
1717
NullASTNode,
1818
PropertyASTNode,
19+
YamlNode,
1920
} from '../jsonASTTypes';
2021
import { ErrorCode } from 'vscode-json-languageservice';
2122
import * as nls from 'vscode-nls';
@@ -24,7 +25,7 @@ import { DiagnosticSeverity, Range } from 'vscode-languageserver-types';
2425
import { TextDocument } from 'vscode-languageserver-textdocument';
2526
import { Diagnostic } from 'vscode-languageserver';
2627
import { isArrayEqual } from '../utils/arrUtils';
27-
import { Node } from 'yaml';
28+
import { Node, Pair } from 'yaml';
2829
import { safeCreateUnicodeRegExp } from '../utils/strings';
2930

3031
const localize = nls.loadMessageBundle();
@@ -89,9 +90,9 @@ export abstract class ASTNodeImpl {
8990
public length: number;
9091
public readonly parent: ASTNode;
9192
public location: string;
92-
readonly internalNode: Node;
93+
readonly internalNode: YamlNode;
9394

94-
constructor(parent: ASTNode, internalNode: Node, offset: number, length?: number) {
95+
constructor(parent: ASTNode, internalNode: YamlNode, offset: number, length?: number) {
9596
this.offset = offset;
9697
this.length = length;
9798
this.parent = parent;
@@ -204,7 +205,7 @@ export class PropertyASTNodeImpl extends ASTNodeImpl implements PropertyASTNode
204205
public valueNode: ASTNode;
205206
public colonOffset: number;
206207

207-
constructor(parent: ObjectASTNode, internalNode: Node, offset: number, length?: number) {
208+
constructor(parent: ObjectASTNode, internalNode: Pair, offset: number, length?: number) {
208209
super(parent, internalNode, offset, length);
209210
this.colonOffset = -1;
210211
}

src/languageservice/parser/yaml-documents.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { TextDocument } from 'vscode-languageserver-textdocument';
77
import { JSONDocument } from './jsonParser07';
88
import { Document, isNode, isPair, isScalar, LineCounter, visit, YAMLError } from 'yaml';
9-
import { ASTNode } from '../jsonASTTypes';
9+
import { ASTNode, YamlNode } from '../jsonASTTypes';
1010
import { defaultOptions, parse as parseYAML, ParserOptions } from './yamlParser07';
1111
import { ErrorCode } from 'vscode-json-languageservice';
1212
import { Node } from 'yaml';
@@ -81,7 +81,7 @@ export class SingleYAMLDocument extends JSONDocument {
8181
return this.internalDocument.warnings.map(YAMLErrorToYamlDocDiagnostics);
8282
}
8383

84-
getNodeFromPosition(positionOffset: number, textBuffer: TextBuffer): [Node | undefined, boolean] {
84+
getNodeFromPosition(positionOffset: number, textBuffer: TextBuffer): [YamlNode | undefined, boolean] {
8585
const position = textBuffer.getPosition(positionOffset);
8686
const lineContent = textBuffer.getLineContent(position.line);
8787
if (lineContent.trim().length === 0) {
@@ -108,10 +108,10 @@ export class SingleYAMLDocument extends JSONDocument {
108108
return [closestNode, false];
109109
}
110110

111-
findClosestNode(offset: number, textBuffer: TextBuffer): Node {
111+
findClosestNode(offset: number, textBuffer: TextBuffer): YamlNode {
112112
let offsetDiff = this.internalDocument.range[2];
113113
let maxOffset = this.internalDocument.range[0];
114-
let closestNode: Node;
114+
let closestNode: YamlNode;
115115
visit(this.internalDocument, (key, node: Node) => {
116116
if (!node) {
117117
return;
@@ -143,11 +143,11 @@ export class SingleYAMLDocument extends JSONDocument {
143143
return closestNode;
144144
}
145145

146-
private getProperParentByIndentation(indentation: number, node: Node, textBuffer: TextBuffer): Node {
146+
private getProperParentByIndentation(indentation: number, node: YamlNode, textBuffer: TextBuffer): YamlNode {
147147
if (!node) {
148148
return this.internalDocument.contents as Node;
149149
}
150-
if (node.range) {
150+
if (isNode(node) && node.range) {
151151
const position = textBuffer.getPosition(node.range[0]);
152152
if (position.character > indentation && position.character > 0) {
153153
const parent = this.getParent(node);
@@ -169,7 +169,7 @@ export class SingleYAMLDocument extends JSONDocument {
169169
return node;
170170
}
171171

172-
getParent(node: Node): Node | undefined {
172+
getParent(node: YamlNode): YamlNode | undefined {
173173
return getParent(this.internalDocument, node);
174174
}
175175
}

src/languageservice/parser/yamlParser07.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function parse(text: string, parserOptions: ParserOptions = defaultOption
3030
strict: false,
3131
customTags: getCustomTags(parserOptions.customTags),
3232
version: parserOptions.yamlVersion,
33+
keepSourceTokens: true,
3334
};
3435
const composer = new Composer(options);
3536
const lineCounter = new LineCounter();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { TextDocument } from 'vscode-languageserver-textdocument';
7+
import { Diagnostic } from 'vscode-languageserver-types';
8+
import { SingleYAMLDocument } from '../../parser/yaml-documents';
9+
10+
export interface AdditionalValidator {
11+
validate(document: TextDocument, yamlDoc: SingleYAMLDocument): Diagnostic[];
12+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { TextDocument } from 'vscode-languageserver-textdocument';
7+
import { Diagnostic, DiagnosticSeverity, DiagnosticTag, Range } from 'vscode-languageserver-types';
8+
import { isAlias, isCollection, isNode, isScalar, Node, Scalar, visit, YAMLMap, YAMLSeq, CST, Pair } from 'yaml';
9+
import { YamlNode } from '../../jsonASTTypes';
10+
import { SingleYAMLDocument } from '../../parser/yaml-documents';
11+
import { AdditionalValidator } from './types';
12+
import { isCollectionItem } from '../../../languageservice/utils/astUtils';
13+
14+
export class UnusedAnchorsValidator implements AdditionalValidator {
15+
validate(document: TextDocument, yamlDoc: SingleYAMLDocument): Diagnostic[] {
16+
const result = [];
17+
const anchors = new Set<Scalar | YAMLMap | YAMLSeq>();
18+
const usedAnchors = new Set<Node>();
19+
const anchorParent = new Map<Scalar | YAMLMap | YAMLSeq, Node | Pair>();
20+
21+
visit(yamlDoc.internalDocument, (key, node, path) => {
22+
if (!isNode(node)) {
23+
return;
24+
}
25+
if ((isCollection(node) || isScalar(node)) && node.anchor) {
26+
anchors.add(node);
27+
anchorParent.set(node, path[path.length - 1] as Node);
28+
}
29+
if (isAlias(node)) {
30+
usedAnchors.add(node.resolve(yamlDoc.internalDocument));
31+
}
32+
});
33+
34+
for (const anchor of anchors) {
35+
if (!usedAnchors.has(anchor)) {
36+
const aToken = this.getAnchorNode(anchorParent.get(anchor));
37+
if (aToken) {
38+
const range = Range.create(
39+
document.positionAt(aToken.offset),
40+
document.positionAt(aToken.offset + aToken.source.length)
41+
);
42+
const warningDiagnostic = Diagnostic.create(range, `Unused anchor "${aToken.source}"`, DiagnosticSeverity.Hint, 0);
43+
warningDiagnostic.tags = [DiagnosticTag.Unnecessary];
44+
warningDiagnostic.data = { name: aToken.source };
45+
result.push(warningDiagnostic);
46+
}
47+
}
48+
}
49+
50+
return result;
51+
}
52+
private getAnchorNode(parentNode: YamlNode): CST.SourceToken | undefined {
53+
if (parentNode && parentNode.srcToken) {
54+
const token = parentNode.srcToken;
55+
if (isCollectionItem(token)) {
56+
return getAnchorFromCollectionItem(token);
57+
} else if (CST.isCollection(token)) {
58+
for (const t of token.items) {
59+
const anchor = getAnchorFromCollectionItem(t);
60+
if (anchor) {
61+
return anchor;
62+
}
63+
}
64+
}
65+
}
66+
return undefined;
67+
}
68+
}
69+
function getAnchorFromCollectionItem(token: CST.CollectionItem): CST.SourceToken | undefined {
70+
for (const t of token.start) {
71+
if (t.type === 'anchor') {
72+
return t;
73+
}
74+
}
75+
if (token.sep && Array.isArray(token.sep)) {
76+
for (const t of token.sep) {
77+
if (t.type === 'anchor') {
78+
return t;
79+
}
80+
}
81+
}
82+
}

src/languageservice/services/yamlCodeActions.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { YamlCommands } from '../../commands';
2020
import * as path from 'path';
2121
import { TextBuffer } from '../utils/textBuffer';
2222
import { LanguageSettings } from '../yamlLanguageService';
23+
import { YAML_SOURCE } from '../parser/jsonParser07';
24+
import { getFirstNonWhitespaceCharacterAfterOffset } from '../utils/strings';
2325

2426
interface YamlDiagnosticData {
2527
schemaUri: string[];
@@ -43,6 +45,7 @@ export class YamlCodeActions {
4345
result.push(...this.getConvertToBooleanActions(params.context.diagnostics, document));
4446
result.push(...this.getJumpToSchemaActions(params.context.diagnostics));
4547
result.push(...this.getTabToSpaceConverting(params.context.diagnostics, document));
48+
result.push(...this.getUnusedAnchorsDelete(params.context.diagnostics, document));
4649

4750
return result;
4851
}
@@ -163,6 +166,29 @@ export class YamlCodeActions {
163166

164167
return result;
165168
}
169+
170+
private getUnusedAnchorsDelete(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] {
171+
const result = [];
172+
const buffer = new TextBuffer(document);
173+
for (const diag of diagnostics) {
174+
if (diag.message.startsWith('Unused anchor') && diag.source === YAML_SOURCE) {
175+
const { name } = diag.data as { name: string };
176+
const range = Range.create(diag.range.start, diag.range.end);
177+
const lineContent = buffer.getLineContent(range.end.line);
178+
const lastWhitespaceChar = getFirstNonWhitespaceCharacterAfterOffset(lineContent, range.end.character);
179+
range.end.character = lastWhitespaceChar;
180+
const action = CodeAction.create(
181+
`Delete unused anchor: ${name}`,
182+
createWorkspaceEdit(document.uri, [TextEdit.del(range)]),
183+
CodeActionKind.QuickFix
184+
);
185+
action.diagnostics = [diag];
186+
result.push(action);
187+
}
188+
}
189+
return result;
190+
}
191+
166192
private getConvertToBooleanActions(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] {
167193
const results: CodeAction[] = [];
168194
for (const diagnostic of diagnostics) {

0 commit comments

Comments
 (0)