Skip to content

Commit ec4c493

Browse files
committed
Added diagnostics support (work in progress)
1 parent 0fe0425 commit ec4c493

15 files changed

+438
-53
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ An extension for [Visual Studio Code](https://code.visualstudio.com) that enable
1616
- Property and Value Autocompletion Hints
1717
- Documentation Hints on Hover
1818
- Document Outline Support
19+
- Diagnostics
1920
- Jump Link Support for the `$include` Function
2021

2122
## Supported File Formats

server/src/core/definition-provider.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { INCLUDE_FUNCTION_NAME } from "@language-server/constants/function";
77
import { DocumentParserTreeManager } from "@language-server/core/document-parser-tree-manager";
88
import { ApacheDispatcherConfigToken, getCurrentSyntaxNode } from "@language-server/core/tree-sitter";
99
import { FileExistenceContext, getFileExistenceContext } from "@language-server/utils/file-system";
10+
import { removeOuterQuotes } from "@language-server/utils/string";
1011
import {
1112
DefinitionLink,
1213
DefinitionParams,
@@ -19,22 +20,6 @@ import Parser = require("web-tree-sitter");
1920
const START_POSITION: Position = Position.create(0, 0);
2021
const START_POSITION_RANGE: Range = Range.create(START_POSITION, START_POSITION);
2122

22-
function isSingleQuoteString(text: string): boolean {
23-
return text.startsWith("'") && text.endsWith("'");
24-
}
25-
26-
function isDoubleQuoteString(text: string): boolean {
27-
return text.startsWith("\"") && text.endsWith("\"");
28-
}
29-
30-
function removeOuterQuotes(text: string): string {
31-
if (isSingleQuoteString(text) || isDoubleQuoteString(text)) {
32-
return text.substring(1, text.length - 1);
33-
}
34-
35-
return text;
36-
}
37-
3823
export async function getDefinition(
3924
documentParserTreeManager: DocumentParserTreeManager,
4025
definitionParams: DefinitionParams

server/src/core/diagnostics.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @fileoverview Handles scanning and providing diagnostics regarding an Apache Dispatcher Config file (e.g., duplicate properties).
3+
* @author Darian Benam <[email protected]>
4+
*/
5+
6+
import { PROPERTY_PREFIX_CHARACTER } from "@language-server/constants/trigger-character";
7+
import { ApacheDispatcherConfigToken } from "@language-server/core/tree-sitter";
8+
import { convertValueToMd5Hash } from "@language-server/utils/crypto";
9+
import { getSyntaxNodeRange } from "@language-server/utils/range";
10+
import { removeOuterQuotes } from "@language-server/utils/string";
11+
import { Diagnostic, DiagnosticSeverity, Range } from "vscode-languageserver";
12+
import Parser = require("web-tree-sitter");
13+
14+
function createDuplicatePropertyDiagnostic(
15+
propertyName: string,
16+
range: Range,
17+
scopeId: string
18+
): Diagnostic {
19+
return {
20+
message: `The property '${propertyName}' is already defined in the current scope (Scope ID: ${scopeId}).`,
21+
range: range,
22+
severity: DiagnosticSeverity.Warning
23+
};
24+
}
25+
26+
function createDuplicateStringDiagnostic(
27+
stringValue: string,
28+
range: Range,
29+
scopeId: string
30+
): Diagnostic {
31+
return {
32+
message: `The string '${removeOuterQuotes(stringValue)}' is already defined in the current scope (Scope ID: ${scopeId}).`,
33+
range: range,
34+
severity: DiagnosticSeverity.Warning
35+
};
36+
}
37+
38+
function getSyntaxNodeScopeIdentifier(syntaxNode: Parser.SyntaxNode | null): string {
39+
return syntaxNode
40+
? `${syntaxNode.startPosition.row}`
41+
: "root";
42+
}
43+
44+
function* buildSyntaxNodeScopeMapRecursively(
45+
syntaxNode: Parser.SyntaxNode,
46+
identifier: string
47+
): Generator<[ string, Parser.SyntaxNode ], void, unknown> {
48+
const scopeIdentifier: string = syntaxNode.type === ApacheDispatcherConfigToken.ScopedProperty
49+
? getSyntaxNodeScopeIdentifier(syntaxNode)
50+
: identifier;
51+
52+
if (
53+
syntaxNode.type === ApacheDispatcherConfigToken.PropertyName ||
54+
syntaxNode.type === ApacheDispatcherConfigToken.String
55+
) {
56+
yield [ scopeIdentifier, syntaxNode ];
57+
}
58+
59+
for (const childSyntaxNode of syntaxNode.children) {
60+
yield* buildSyntaxNodeScopeMapRecursively(childSyntaxNode, scopeIdentifier);
61+
}
62+
}
63+
64+
export function getDocumentParseTreeDiagnostics(
65+
documentParseTree: Parser.Tree | undefined
66+
): Diagnostic[] | undefined {
67+
if (documentParseTree === undefined) {
68+
return undefined;
69+
}
70+
71+
const diagnostics: Diagnostic[] = [];
72+
const rootNode: Parser.SyntaxNode = documentParseTree.rootNode;
73+
const syntaxNodeScopeMap = new Map<string, Parser.SyntaxNode[]>();
74+
75+
for (const [ scopeKey, node ] of buildSyntaxNodeScopeMapRecursively(rootNode, "global")) {
76+
if (!syntaxNodeScopeMap.has(scopeKey)) {
77+
syntaxNodeScopeMap.set(scopeKey, []);
78+
}
79+
80+
syntaxNodeScopeMap.get(scopeKey)!.push(node);
81+
}
82+
83+
syntaxNodeScopeMap.forEach(function(currentScopeSyntaxNodes: Parser.SyntaxNode[], rawScopeId: string) {
84+
const scopeIdMd5Hash: string = convertValueToMd5Hash(rawScopeId);
85+
const propertySyntaxNodeOccurrences: Map<string, Parser.SyntaxNode> = new Map();
86+
const stringOccurrences: Map<string, Parser.SyntaxNode> = new Map();
87+
88+
for (const syntaxNode of currentScopeSyntaxNodes) {
89+
const syntaxNodeTextValue: string = syntaxNode.text.trim();
90+
91+
// Ignore properties that are just '/' as we'll assume the user hasn't finished typing
92+
if (syntaxNodeTextValue === PROPERTY_PREFIX_CHARACTER) {
93+
continue;
94+
}
95+
96+
const currentSyntaxNodeRange: Range = getSyntaxNodeRange(syntaxNode);
97+
98+
if (syntaxNode.type === ApacheDispatcherConfigToken.PropertyName) {
99+
const propertySyntaxNode: Parser.SyntaxNode | undefined = propertySyntaxNodeOccurrences.get(syntaxNodeTextValue)
100+
101+
if (propertySyntaxNode !== undefined) {
102+
const isDuplicateDiagnostic: boolean = diagnostics.some(diagnostic => diagnostic.range.start === getSyntaxNodeRange(propertySyntaxNode).start);
103+
104+
if (!isDuplicateDiagnostic) {
105+
diagnostics.push(
106+
createDuplicatePropertyDiagnostic(
107+
syntaxNodeTextValue,
108+
getSyntaxNodeRange(propertySyntaxNode),
109+
scopeIdMd5Hash
110+
)
111+
);
112+
}
113+
114+
diagnostics.push(createDuplicatePropertyDiagnostic(syntaxNodeTextValue, currentSyntaxNodeRange, scopeIdMd5Hash));
115+
} else {
116+
propertySyntaxNodeOccurrences.set(syntaxNodeTextValue, syntaxNode);
117+
}
118+
} else if (syntaxNode.type === ApacheDispatcherConfigToken.String) {
119+
const syntaxNodeParent: Parser.SyntaxNode | null = syntaxNode.parent;
120+
121+
if (
122+
syntaxNodeParent !== null &&
123+
syntaxNodeParent.type === ApacheDispatcherConfigToken.Property
124+
) {
125+
continue;
126+
}
127+
128+
const stringValueWithoutQuotes: string = removeOuterQuotes(syntaxNodeTextValue);
129+
130+
if (stringOccurrences.has(stringValueWithoutQuotes)) {
131+
const isDuplicateDiagnostic: boolean = diagnostics.some(diagnostic => diagnostic.range.start === getSyntaxNodeRange(syntaxNode).start);
132+
133+
if (!isDuplicateDiagnostic) {
134+
diagnostics.push(createDuplicateStringDiagnostic(syntaxNodeTextValue, getSyntaxNodeRange(stringOccurrences.get(stringValueWithoutQuotes)!), scopeIdMd5Hash));
135+
}
136+
137+
diagnostics.push(createDuplicateStringDiagnostic(syntaxNodeTextValue, currentSyntaxNodeRange, scopeIdMd5Hash));
138+
} else {
139+
stringOccurrences.set(stringValueWithoutQuotes, syntaxNode);
140+
}
141+
}
142+
}
143+
});
144+
145+
return diagnostics;
146+
}

server/src/core/document-parser-tree-manager.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
loadApacheDispatcherConfigTreeSitterLanguage,
99
tokenizeTextDocument
1010
} from "@language-server/core/tree-sitter";
11+
import { getSyntaxNodeRange } from "@language-server/utils/range";
1112
import { DocumentSymbol, SymbolKind, TextDocuments } from "vscode-languageserver";
1213
import { Range, TextDocument } from "vscode-languageserver-textdocument";
1314
import Parser = require("web-tree-sitter");
@@ -18,7 +19,7 @@ type ApacheDispatcherConfigDocumentSymbol = {
1819
}
1920

2021
export async function waitForDocumentParserTreeManagerInitialization(
21-
documentParserTreeManager: DocumentParserTreeManager
22+
documentParserTreeManager: DocumentParserTreeManager | undefined
2223
): Promise<void> {
2324
return new Promise(function(resolve: (value: void) => void) {
2425
setTimeout(function() {
@@ -68,27 +69,8 @@ export class DocumentParserTreeManager {
6869
selectionSyntaxNode: Parser.SyntaxNode,
6970
kind: SymbolKind
7071
): DocumentSymbol {
71-
const parentTokenRange: Range = {
72-
start: {
73-
line: parentSyntaxNode.startPosition.row,
74-
character: parentSyntaxNode.startPosition.column
75-
},
76-
end: {
77-
line: parentSyntaxNode.endPosition.row,
78-
character: parentSyntaxNode.endPosition.column
79-
}
80-
};
81-
82-
const selectionRange: Range = {
83-
start: {
84-
line: selectionSyntaxNode.startPosition.row,
85-
character: selectionSyntaxNode.startPosition.column
86-
},
87-
end: {
88-
line: selectionSyntaxNode.endPosition.row,
89-
character: selectionSyntaxNode.endPosition.column
90-
}
91-
};
72+
const parentTokenRange: Range = getSyntaxNodeRange(parentSyntaxNode);
73+
const selectionRange: Range = getSyntaxNodeRange(selectionSyntaxNode);
9274

9375
return {
9476
name: selectionSyntaxNode.text,
@@ -98,7 +80,6 @@ export class DocumentParserTreeManager {
9880
};
9981
}
10082

101-
10283
private getApacheDispatcherConfigTokenSymbolKind(tokenType: ApacheDispatcherConfigToken): SymbolKind {
10384
switch (tokenType) {
10485
case ApacheDispatcherConfigToken.Function:
@@ -194,14 +175,16 @@ export class DocumentParserTreeManager {
194175
return rootSymbols;
195176
}
196177

197-
public updateParseTree(document: TextDocument): void {
178+
public updateParseTree(document: TextDocument): boolean {
198179
if (this.treeSitterParser === undefined) {
199-
throw new Error("Tree-sitter parser has not been initialized!");
180+
return false;
200181
}
201182

202-
const documentUri = document.uri;
183+
const documentUri: string = document.uri;
203184
const textDocumentParserTree: Parser.Tree = tokenizeTextDocument(this.treeSitterParser, document);
204185

205186
this.documentParseTree.set(documentUri, textDocumentParserTree);
187+
188+
return true;
206189
}
207190
}

server/src/server.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { AUTOCOMPLETION_TRIGGER_CHARACTERS, handleAutoCompletion } from "@language-server/core/autocompletion";
77
import { getDefinition } from "@language-server/core/definition-provider";
8+
import { getDocumentParseTreeDiagnostics } from "@language-server/core/diagnostics";
89
import { DocumentParserTreeManager, waitForDocumentParserTreeManagerInitialization } from "@language-server/core/document-parser-tree-manager";
910
import { DocumentationManager } from "@language-server/core/documentation-manager";
1011
import { handleHover } from "@language-server/core/hover-provider";
@@ -16,6 +17,7 @@ import {
1617
Connection,
1718
DefinitionLink,
1819
DefinitionParams,
20+
Diagnostic,
1921
DidChangeConfigurationNotification,
2022
DocumentSymbol,
2123
DocumentSymbolParams,
@@ -29,15 +31,40 @@ import {
2931
TextDocuments,
3032
createConnection
3133
} from "vscode-languageserver/node";
34+
import Parser = require("web-tree-sitter");
3235

3336
const CONNECTION: Connection = createConnection(ProposedFeatures.all);
3437
const DOCUMENT_MANAGER: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
3538

3639
let hasConfigurationCapability: boolean = false;
3740

38-
let documentParserTreeManager: DocumentParserTreeManager;
41+
let documentParserTreeManager: DocumentParserTreeManager | undefined;
3942
let documentationManager: DocumentationManager;
4043

44+
function updateDocumentParseTreeAndSendDiagnostics(
45+
document: TextDocument,
46+
sendDiagnostics: boolean = true
47+
): void {
48+
if (documentParserTreeManager === undefined) {
49+
return;
50+
}
51+
52+
const parseTreeUpdated: boolean = documentParserTreeManager.updateParseTree(document);
53+
54+
if (parseTreeUpdated && sendDiagnostics) {
55+
const documentUri: string = document.uri;
56+
const documentTokenTree: Parser.Tree | undefined = documentParserTreeManager.getDocumentTokenTree(documentUri);
57+
const currentDocumentDiagnostics: Diagnostic[] | undefined = getDocumentParseTreeDiagnostics(documentTokenTree);
58+
59+
if (currentDocumentDiagnostics !== undefined) {
60+
CONNECTION.sendDiagnostics({
61+
uri: documentUri,
62+
diagnostics: currentDocumentDiagnostics
63+
});
64+
}
65+
}
66+
}
67+
4168
CONNECTION.onInitialize(async function(initializeParams: InitializeParams): Promise<InitializeResult> {
4269
console.info("Initializing language server...");
4370

@@ -89,7 +116,7 @@ CONNECTION.onCompletion(
89116
async (textDocumentPositionParams: TextDocumentPositionParams): Promise<CompletionItem[]> => {
90117
return await handleAutoCompletion(
91118
DOCUMENT_MANAGER,
92-
documentParserTreeManager,
119+
documentParserTreeManager!,
93120
documentationManager,
94121
textDocumentPositionParams
95122
);
@@ -115,7 +142,7 @@ CONNECTION.onDocumentSymbol(async function(documentSymbolParams: DocumentSymbolP
115142
const document: TextDocument | undefined = DOCUMENT_MANAGER.get(documentSymbolParams.textDocument.uri);
116143

117144
if (document !== undefined) {
118-
return documentParserTreeManager.getDocumentSymbols(document.uri);
145+
return documentParserTreeManager!.getDocumentSymbols(document.uri);
119146
}
120147

121148
return [];
@@ -125,19 +152,19 @@ CONNECTION.onHover(
125152
async (textDocumentPositionParams: TextDocumentPositionParams): Promise<Hover | null> => {
126153
return await handleHover(
127154
DOCUMENT_MANAGER,
128-
documentParserTreeManager,
155+
documentParserTreeManager!,
129156
documentationManager,
130157
textDocumentPositionParams
131158
);
132159
}
133160
);
134161

135-
DOCUMENT_MANAGER.onDidOpen(function(event: TextDocumentChangeEvent<TextDocument>) {
136-
documentParserTreeManager?.updateParseTree(event.document);
162+
DOCUMENT_MANAGER.onDidOpen(function(event: TextDocumentChangeEvent<TextDocument>): void {
163+
updateDocumentParseTreeAndSendDiagnostics(event.document, false);
137164
});
138165

139-
DOCUMENT_MANAGER.onDidChangeContent(async function(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
140-
documentParserTreeManager?.updateParseTree(event.document);
166+
DOCUMENT_MANAGER.onDidChangeContent(function(event: TextDocumentChangeEvent<TextDocument>): void {
167+
updateDocumentParseTreeAndSendDiagnostics(event.document);
141168
});
142169

143170
DOCUMENT_MANAGER.listen(CONNECTION);

server/src/utils/crypto.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @fileoverview Utility functions for dealing with cryptography.
3+
* @author Darian Benam <[email protected]>
4+
*/
5+
6+
import { createHash } from "crypto";
7+
8+
export function convertValueToMd5Hash(value: string): string {
9+
return createHash("md5").update(value).digest("hex");
10+
}

server/src/utils/range.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @fileoverview Utility functions for dealing with text editor ranges.
3+
* @author Darian Benam <[email protected]>
4+
*/
5+
6+
import { Range } from "vscode-languageserver";
7+
import Parser = require("web-tree-sitter");
8+
9+
export function getSyntaxNodeRange(syntaxNode: Parser.SyntaxNode): Range {
10+
return {
11+
start: {
12+
line: syntaxNode.startPosition.row,
13+
character: syntaxNode.startPosition.column
14+
},
15+
end: {
16+
line: syntaxNode.endPosition.row,
17+
character: syntaxNode.endPosition.column
18+
}
19+
};
20+
}

0 commit comments

Comments
 (0)