Skip to content
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ An extension for [Visual Studio Code](https://code.visualstudio.com) that enable
- Property and Value Autocompletion Hints
- Documentation Hints on Hover
- Document Outline Support
- Diagnostics
- Jump Link Support for the `$include` Function

## Supported File Formats
Expand Down
2 changes: 1 addition & 1 deletion server/documentation/FARM_FILTER_URL.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The `/url` proeprty is used to filter the request based on its URL.
The `/url` property is used to filter the request based on its URL.

---

Expand Down
17 changes: 1 addition & 16 deletions server/src/core/definition-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { INCLUDE_FUNCTION_NAME } from "@language-server/constants/function";
import { DocumentParserTreeManager } from "@language-server/core/document-parser-tree-manager";
import { ApacheDispatcherConfigToken, getCurrentSyntaxNode } from "@language-server/core/tree-sitter";
import { FileExistenceContext, getFileExistenceContext } from "@language-server/utils/file-system";
import { removeOuterQuotes } from "@language-server/utils/string";
import {
DefinitionLink,
DefinitionParams,
Expand All @@ -19,22 +20,6 @@ import Parser = require("web-tree-sitter");
const START_POSITION: Position = Position.create(0, 0);
const START_POSITION_RANGE: Range = Range.create(START_POSITION, START_POSITION);

function isSingleQuoteString(text: string): boolean {
return text.startsWith("'") && text.endsWith("'");
}

function isDoubleQuoteString(text: string): boolean {
return text.startsWith("\"") && text.endsWith("\"");
}

function removeOuterQuotes(text: string): string {
if (isSingleQuoteString(text) || isDoubleQuoteString(text)) {
return text.substring(1, text.length - 1);
}

return text;
}

export async function getDefinition(
documentParserTreeManager: DocumentParserTreeManager,
definitionParams: DefinitionParams
Expand Down
141 changes: 141 additions & 0 deletions server/src/core/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @fileoverview Handles scanning and providing diagnostics regarding an Apache Dispatcher Config file (e.g., duplicate properties).
* @author Darian Benam <[email protected]>
*/

import { PROPERTY_PREFIX_CHARACTER } from "@language-server/constants/trigger-character";
import { ApacheDispatcherConfigToken } from "@language-server/core/tree-sitter";
import { convertValueToMd5Hash } from "@language-server/utils/crypto";
import { getSyntaxNodeRange } from "@language-server/utils/range";
import { removeOuterQuotes } from "@language-server/utils/string";
import { Diagnostic, DiagnosticSeverity, Range } from "vscode-languageserver";
import Parser = require("web-tree-sitter");

function createDuplicatePropertyDiagnostic(
propertyName: string,
range: Range,
scopeId: string
): Diagnostic {
return {
message: `The property '${propertyName}' is already defined in the current scope (Scope ID: ${scopeId}).`,
range: range,
severity: DiagnosticSeverity.Warning
};
}

function createDuplicateStringDiagnostic(
stringValue: string,
range: Range,
scopeId: string
): Diagnostic {
return {
message: `The string '${removeOuterQuotes(stringValue)}' is already defined in the current scope (Scope ID: ${scopeId}).`,
range: range,
severity: DiagnosticSeverity.Warning
};
}

function getSyntaxNodeScopeIdentifier(syntaxNode: Parser.SyntaxNode | null): string {
return syntaxNode === null
? "root"
: `${syntaxNode.startPosition.row}`;
}

function* buildSyntaxNodeScopeMapRecursively(
syntaxNode: Parser.SyntaxNode,
identifier: string
): Generator<[ string, Parser.SyntaxNode ], void, unknown> {
const scopeIdentifier: string = syntaxNode.type === ApacheDispatcherConfigToken.ScopedProperty
? getSyntaxNodeScopeIdentifier(syntaxNode)
: identifier;

if (
syntaxNode.type === ApacheDispatcherConfigToken.PropertyName ||
syntaxNode.type === ApacheDispatcherConfigToken.String
) {
yield [ scopeIdentifier, syntaxNode ];
}

for (const childSyntaxNode of syntaxNode.children) {
yield* buildSyntaxNodeScopeMapRecursively(childSyntaxNode, scopeIdentifier);
}
}

export function getDocumentParseTreeDiagnostics(
documentParseTree: Parser.Tree | undefined
): Diagnostic[] | undefined {
if (documentParseTree === undefined) {
return undefined;
}

const diagnostics: Diagnostic[] = [];
const rootNode: Parser.SyntaxNode = documentParseTree.rootNode;
const syntaxNodeScopeMap = new Map<string, Parser.SyntaxNode[]>();

for (const [ scopeKey, node ] of buildSyntaxNodeScopeMapRecursively(rootNode, "global")) {
if (!syntaxNodeScopeMap.has(scopeKey)) {
syntaxNodeScopeMap.set(scopeKey, []);
}

syntaxNodeScopeMap.get(scopeKey)!.push(node);
}

syntaxNodeScopeMap.forEach(function(currentScopeSyntaxNodes: Parser.SyntaxNode[], rawScopeId: string) {
const scopeIdMd5Hash: string = convertValueToMd5Hash(rawScopeId);
const propertySyntaxNodeOccurrences: Map<string, Parser.SyntaxNode[]> = new Map();
const stringSyntaxNodeOccurrences: Map<string, Parser.SyntaxNode[]> = new Map();

for (const syntaxNode of currentScopeSyntaxNodes) {
const syntaxNodeTextValue: string = syntaxNode.text.trim();

// Ignore properties that are just '/' as we'll assume the user hasn't finished typing
if (syntaxNodeTextValue === PROPERTY_PREFIX_CHARACTER) {
continue;
}

const currentSyntaxNodeRange: Range = getSyntaxNodeRange(syntaxNode);
let syntaxNodeOccurrences: Parser.SyntaxNode[];

if (syntaxNode.type === ApacheDispatcherConfigToken.PropertyName) {
syntaxNodeOccurrences = propertySyntaxNodeOccurrences.get(syntaxNodeTextValue) ?? [];
syntaxNodeOccurrences.push(syntaxNode);

propertySyntaxNodeOccurrences.set(syntaxNodeTextValue, syntaxNodeOccurrences);

if (syntaxNodeOccurrences.length === 2) {
diagnostics.push(createDuplicatePropertyDiagnostic(syntaxNodeTextValue, getSyntaxNodeRange(syntaxNodeOccurrences[0]), scopeIdMd5Hash));
}

if (syntaxNodeOccurrences.length > 1) {
diagnostics.push(createDuplicatePropertyDiagnostic(syntaxNodeTextValue, currentSyntaxNodeRange, scopeIdMd5Hash));
}
} else if (syntaxNode.type === ApacheDispatcherConfigToken.String) {
const syntaxNodeParent: Parser.SyntaxNode | null = syntaxNode.parent;

if (
syntaxNodeParent !== null &&
syntaxNodeParent.type === ApacheDispatcherConfigToken.Property
) {
continue;
}

const stringValueWithoutQuotes: string = removeOuterQuotes(syntaxNodeTextValue);

syntaxNodeOccurrences = stringSyntaxNodeOccurrences.get(stringValueWithoutQuotes) ?? [];
syntaxNodeOccurrences.push(syntaxNode);

stringSyntaxNodeOccurrences.set(stringValueWithoutQuotes, syntaxNodeOccurrences);

if (syntaxNodeOccurrences.length === 2) {
diagnostics.push(createDuplicateStringDiagnostic(syntaxNodeTextValue, getSyntaxNodeRange(syntaxNodeOccurrences[0]), scopeIdMd5Hash));
}

if (syntaxNodeOccurrences.length > 1) {
diagnostics.push(createDuplicateStringDiagnostic(syntaxNodeTextValue, currentSyntaxNodeRange, scopeIdMd5Hash));
}
}
}
});

return diagnostics;
}
41 changes: 12 additions & 29 deletions server/src/core/document-parser-tree-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import {
loadApacheDispatcherConfigTreeSitterLanguage,
tokenizeTextDocument
} from "@language-server/core/tree-sitter";
import { getSyntaxNodeRange } from "@language-server/utils/range";
import { DocumentSymbol, SymbolKind, TextDocuments } from "vscode-languageserver";
import { Range, TextDocument } from "vscode-languageserver-textdocument";
import Parser = require("web-tree-sitter");

type ApacheDispatcherConfigDocoumentSymbol = {
type ApacheDispatcherConfigDocumentSymbol = {
node: Parser.SyntaxNode;
parentSymbols: DocumentSymbol[];
}

export async function waitForDocumentParserTreeManagerInitialization(
documentParserTreeManager: DocumentParserTreeManager
documentParserTreeManager: DocumentParserTreeManager | undefined
): Promise<void> {
return new Promise(function(resolve: (value: void) => void) {
setTimeout(function() {
Expand Down Expand Up @@ -68,27 +69,8 @@ export class DocumentParserTreeManager {
selectionSyntaxNode: Parser.SyntaxNode,
kind: SymbolKind
): DocumentSymbol {
const parentTokenRange: Range = {
start: {
line: parentSyntaxNode.startPosition.row,
character: parentSyntaxNode.startPosition.column
},
end: {
line: parentSyntaxNode.endPosition.row,
character: parentSyntaxNode.endPosition.column
}
};

const selectionRange: Range = {
start: {
line: selectionSyntaxNode.startPosition.row,
character: selectionSyntaxNode.startPosition.column
},
end: {
line: selectionSyntaxNode.endPosition.row,
character: selectionSyntaxNode.endPosition.column
}
};
const parentTokenRange: Range = getSyntaxNodeRange(parentSyntaxNode);
const selectionRange: Range = getSyntaxNodeRange(selectionSyntaxNode);

return {
name: selectionSyntaxNode.text,
Expand All @@ -98,7 +80,6 @@ export class DocumentParserTreeManager {
};
}


private getApacheDispatcherConfigTokenSymbolKind(tokenType: ApacheDispatcherConfigToken): SymbolKind {
switch (tokenType) {
case ApacheDispatcherConfigToken.Function:
Expand All @@ -122,15 +103,15 @@ export class DocumentParserTreeManager {
}

const rootSymbols: DocumentSymbol[] = [];
const documentSymbolsStack: ApacheDispatcherConfigDocoumentSymbol[] = [
const documentSymbolsStack: ApacheDispatcherConfigDocumentSymbol[] = [
{
node: syntaxTree.rootNode,
parentSymbols: rootSymbols
}
];

while (documentSymbolsStack.length > 0) {
const documentSymbol: ApacheDispatcherConfigDocoumentSymbol | undefined = documentSymbolsStack.pop();
const documentSymbol: ApacheDispatcherConfigDocumentSymbol | undefined = documentSymbolsStack.pop();

if (documentSymbol === undefined || documentSymbol.node === undefined) {
continue;
Expand Down Expand Up @@ -194,14 +175,16 @@ export class DocumentParserTreeManager {
return rootSymbols;
}

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

const documentUri = document.uri;
const documentUri: string = document.uri;
const textDocumentParserTree: Parser.Tree = tokenizeTextDocument(this.treeSitterParser, document);

this.documentParseTree.set(documentUri, textDocumentParserTree);

return true;
}
}
43 changes: 35 additions & 8 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { AUTOCOMPLETION_TRIGGER_CHARACTERS, handleAutoCompletion } from "@language-server/core/autocompletion";
import { getDefinition } from "@language-server/core/definition-provider";
import { getDocumentParseTreeDiagnostics } from "@language-server/core/diagnostics";
import { DocumentParserTreeManager, waitForDocumentParserTreeManagerInitialization } from "@language-server/core/document-parser-tree-manager";
import { DocumentationManager } from "@language-server/core/documentation-manager";
import { handleHover } from "@language-server/core/hover-provider";
Expand All @@ -16,6 +17,7 @@ import {
Connection,
DefinitionLink,
DefinitionParams,
Diagnostic,
DidChangeConfigurationNotification,
DocumentSymbol,
DocumentSymbolParams,
Expand All @@ -29,15 +31,40 @@ import {
TextDocuments,
createConnection
} from "vscode-languageserver/node";
import Parser = require("web-tree-sitter");

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

let hasConfigurationCapability: boolean = false;

let documentParserTreeManager: DocumentParserTreeManager;
let documentParserTreeManager: DocumentParserTreeManager | undefined;
let documentationManager: DocumentationManager;

function updateDocumentParseTreeAndSendDiagnostics(
document: TextDocument,
sendDiagnostics: boolean = true
): void {
if (documentParserTreeManager === undefined) {
return;
}

const parseTreeUpdated: boolean = documentParserTreeManager.updateParseTree(document);

if (parseTreeUpdated && sendDiagnostics) {
const documentUri: string = document.uri;
const documentTokenTree: Parser.Tree | undefined = documentParserTreeManager.getDocumentTokenTree(documentUri);
const currentDocumentDiagnostics: Diagnostic[] | undefined = getDocumentParseTreeDiagnostics(documentTokenTree);

if (currentDocumentDiagnostics !== undefined) {
CONNECTION.sendDiagnostics({
uri: documentUri,
diagnostics: currentDocumentDiagnostics
});
}
}
}

CONNECTION.onInitialize(async function(initializeParams: InitializeParams): Promise<InitializeResult> {
console.info("Initializing language server...");

Expand Down Expand Up @@ -89,7 +116,7 @@ CONNECTION.onCompletion(
async (textDocumentPositionParams: TextDocumentPositionParams): Promise<CompletionItem[]> => {
return await handleAutoCompletion(
DOCUMENT_MANAGER,
documentParserTreeManager,
documentParserTreeManager!,
documentationManager,
textDocumentPositionParams
);
Expand All @@ -115,7 +142,7 @@ CONNECTION.onDocumentSymbol(async function(documentSymbolParams: DocumentSymbolP
const document: TextDocument | undefined = DOCUMENT_MANAGER.get(documentSymbolParams.textDocument.uri);

if (document !== undefined) {
return documentParserTreeManager.getDocumentSymbols(document.uri);
return documentParserTreeManager!.getDocumentSymbols(document.uri);
}

return [];
Expand All @@ -125,19 +152,19 @@ CONNECTION.onHover(
async (textDocumentPositionParams: TextDocumentPositionParams): Promise<Hover | null> => {
return await handleHover(
DOCUMENT_MANAGER,
documentParserTreeManager,
documentParserTreeManager!,
documentationManager,
textDocumentPositionParams
);
}
);

DOCUMENT_MANAGER.onDidOpen(function(event: TextDocumentChangeEvent<TextDocument>) {
documentParserTreeManager?.updateParseTree(event.document);
DOCUMENT_MANAGER.onDidOpen(function(event: TextDocumentChangeEvent<TextDocument>): void {
updateDocumentParseTreeAndSendDiagnostics(event.document, false);
});

DOCUMENT_MANAGER.onDidChangeContent(async function(event: TextDocumentChangeEvent<TextDocument>): Promise<void> {
documentParserTreeManager?.updateParseTree(event.document);
DOCUMENT_MANAGER.onDidChangeContent(function(event: TextDocumentChangeEvent<TextDocument>): void {
updateDocumentParseTreeAndSendDiagnostics(event.document);
});

DOCUMENT_MANAGER.listen(CONNECTION);
Expand Down
10 changes: 10 additions & 0 deletions server/src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @fileoverview Utility functions for dealing with cryptography.
* @author Darian Benam <[email protected]>
*/

import { createHash } from "crypto";

export function convertValueToMd5Hash(value: string): string {
return createHash("md5").update(value).digest("hex");
}
Loading