diff --git a/README.md b/README.md index 8706dca..6b21acc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/server/documentation/FARM_FILTER_URL.md b/server/documentation/FARM_FILTER_URL.md index 8d2de2b..cbaf7cf 100644 --- a/server/documentation/FARM_FILTER_URL.md +++ b/server/documentation/FARM_FILTER_URL.md @@ -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. --- diff --git a/server/src/core/definition-provider.ts b/server/src/core/definition-provider.ts index 33243ad..a525037 100644 --- a/server/src/core/definition-provider.ts +++ b/server/src/core/definition-provider.ts @@ -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, @@ -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 diff --git a/server/src/core/diagnostics.ts b/server/src/core/diagnostics.ts new file mode 100644 index 0000000..cbb7781 --- /dev/null +++ b/server/src/core/diagnostics.ts @@ -0,0 +1,141 @@ +/** + * @fileoverview Handles scanning and providing diagnostics regarding an Apache Dispatcher Config file (e.g., duplicate properties). + * @author Darian Benam + */ + +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(); + + 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 = new Map(); + const stringSyntaxNodeOccurrences: Map = 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; +} diff --git a/server/src/core/document-parser-tree-manager.ts b/server/src/core/document-parser-tree-manager.ts index 95e8baa..3f2fd2c 100644 --- a/server/src/core/document-parser-tree-manager.ts +++ b/server/src/core/document-parser-tree-manager.ts @@ -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 { return new Promise(function(resolve: (value: void) => void) { setTimeout(function() { @@ -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, @@ -98,7 +80,6 @@ export class DocumentParserTreeManager { }; } - private getApacheDispatcherConfigTokenSymbolKind(tokenType: ApacheDispatcherConfigToken): SymbolKind { switch (tokenType) { case ApacheDispatcherConfigToken.Function: @@ -122,7 +103,7 @@ export class DocumentParserTreeManager { } const rootSymbols: DocumentSymbol[] = []; - const documentSymbolsStack: ApacheDispatcherConfigDocoumentSymbol[] = [ + const documentSymbolsStack: ApacheDispatcherConfigDocumentSymbol[] = [ { node: syntaxTree.rootNode, parentSymbols: rootSymbols @@ -130,7 +111,7 @@ export class DocumentParserTreeManager { ]; while (documentSymbolsStack.length > 0) { - const documentSymbol: ApacheDispatcherConfigDocoumentSymbol | undefined = documentSymbolsStack.pop(); + const documentSymbol: ApacheDispatcherConfigDocumentSymbol | undefined = documentSymbolsStack.pop(); if (documentSymbol === undefined || documentSymbol.node === undefined) { continue; @@ -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; } } diff --git a/server/src/server.ts b/server/src/server.ts index 0b3f9db..f1550eb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -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"; @@ -16,6 +17,7 @@ import { Connection, DefinitionLink, DefinitionParams, + Diagnostic, DidChangeConfigurationNotification, DocumentSymbol, DocumentSymbolParams, @@ -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 = 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 { console.info("Initializing language server..."); @@ -89,7 +116,7 @@ CONNECTION.onCompletion( async (textDocumentPositionParams: TextDocumentPositionParams): Promise => { return await handleAutoCompletion( DOCUMENT_MANAGER, - documentParserTreeManager, + documentParserTreeManager!, documentationManager, textDocumentPositionParams ); @@ -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 []; @@ -125,19 +152,19 @@ CONNECTION.onHover( async (textDocumentPositionParams: TextDocumentPositionParams): Promise => { return await handleHover( DOCUMENT_MANAGER, - documentParserTreeManager, + documentParserTreeManager!, documentationManager, textDocumentPositionParams ); } ); -DOCUMENT_MANAGER.onDidOpen(function(event: TextDocumentChangeEvent) { - documentParserTreeManager?.updateParseTree(event.document); +DOCUMENT_MANAGER.onDidOpen(function(event: TextDocumentChangeEvent): void { + updateDocumentParseTreeAndSendDiagnostics(event.document, false); }); -DOCUMENT_MANAGER.onDidChangeContent(async function(event: TextDocumentChangeEvent): Promise { - documentParserTreeManager?.updateParseTree(event.document); +DOCUMENT_MANAGER.onDidChangeContent(function(event: TextDocumentChangeEvent): void { + updateDocumentParseTreeAndSendDiagnostics(event.document); }); DOCUMENT_MANAGER.listen(CONNECTION); diff --git a/server/src/utils/crypto.ts b/server/src/utils/crypto.ts new file mode 100644 index 0000000..aa5a84f --- /dev/null +++ b/server/src/utils/crypto.ts @@ -0,0 +1,10 @@ +/** + * @fileoverview Utility functions for dealing with cryptography. + * @author Darian Benam + */ + +import { createHash } from "crypto"; + +export function convertValueToMd5Hash(value: string): string { + return createHash("md5").update(value).digest("hex"); +} diff --git a/server/src/utils/range.ts b/server/src/utils/range.ts new file mode 100644 index 0000000..ca132cc --- /dev/null +++ b/server/src/utils/range.ts @@ -0,0 +1,20 @@ +/** + * @fileoverview Utility functions for dealing with text editor ranges. + * @author Darian Benam + */ + +import { Range } from "vscode-languageserver"; +import Parser = require("web-tree-sitter"); + +export function getSyntaxNodeRange(syntaxNode: Parser.SyntaxNode): Range { + return { + start: { + line: syntaxNode.startPosition.row, + character: syntaxNode.startPosition.column + }, + end: { + line: syntaxNode.endPosition.row, + character: syntaxNode.endPosition.column + } + }; +} diff --git a/server/src/utils/string.ts b/server/src/utils/string.ts new file mode 100644 index 0000000..0d7a16b --- /dev/null +++ b/server/src/utils/string.ts @@ -0,0 +1,20 @@ +/** + * @fileoverview Utility functions for dealing with strings. + * @author Darian Benam + */ + +function isSingleQuoteString(text: string): boolean { + return text.startsWith("'") && text.endsWith("'"); +} + +function isDoubleQuoteString(text: string): boolean { + return text.startsWith("\"") && text.endsWith("\""); +} + +export function removeOuterQuotes(text: string): string { + if (isSingleQuoteString(text) || isDoubleQuoteString(text)) { + return text.substring(1, text.length - 1); + } + + return text; +} diff --git a/test/src/run-tests.ts b/test/src/run-tests.ts index 87ce1b5..3ccbaae 100644 --- a/test/src/run-tests.ts +++ b/test/src/run-tests.ts @@ -11,6 +11,7 @@ async function main(): Promise { // Download Visual Studio Code, unzip it, and run the integration tests await runTests({ + version: "1.86.0", extensionDevelopmentPath, extensionTestsPath }); diff --git a/test/src/suite/autocompletion.test.ts b/test/src/suite/autocompletion.test.ts index a9c3879..5098f1d 100644 --- a/test/src/suite/autocompletion.test.ts +++ b/test/src/suite/autocompletion.test.ts @@ -1,9 +1,9 @@ -import * as assert from "assert"; -import * as vscode from "vscode"; import { activateApacheDispatcherConfigExtension, openDocumentByRelativeUri } from "../utils/extension"; +import * as assert from "assert"; +import * as vscode from "vscode"; /** * @returns The position after the property that was typed in. @@ -107,7 +107,7 @@ async function assertDoesNotContainAutocompletionItem( } suite("Apache Dispatcher Config Language Support for Visual Studio Code Autocompletion Test Suite", () => { - vscode.window.showInformationMessage("Running tests..."); + vscode.window.showInformationMessage("Running Autocompletion Tests..."); setup(async () => { await activateApacheDispatcherConfigExtension(); diff --git a/test/src/suite/diagnostics.test.ts b/test/src/suite/diagnostics.test.ts new file mode 100644 index 0000000..62bd53e --- /dev/null +++ b/test/src/suite/diagnostics.test.ts @@ -0,0 +1,242 @@ +import * as assert from "assert"; +import * as vscode from "vscode"; +import { + activateApacheDispatcherConfigExtension, + openDocumentByRelativeUri +} from "../utils/extension"; +import { sleep } from "../utils/misc"; + +const DIAGNOSTIC_SLEEP_TIMEOUT_MS = 500; + +suite("Apache Dispatcher Config Language Support for Visual Studio Code Diagnostics Test Suite", () => { + vscode.window.showInformationMessage("Running Diagnostics Tests..."); + + setup(async () => { + await activateApacheDispatcherConfigExtension(); + }); + + teardown(async () => { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + }); + + test("2 Properties With Same Name Results in 2 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-properties-1.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 2, true, `Expected exactly 2 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < 2; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/farm1' is already defined in the current scope (Scope ID: c81e728d9d4c2f636f067f89cc14862c)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("Removing 1 Duplicate Property Removes All Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-properties-1.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + let diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnosticsInitial: number = diagnostics.length; + + assert.strictEqual(totalDiagnosticsInitial === 2, true, `Expected exactly 2 diagnostic warnings, found ${totalDiagnosticsInitial}`); + + await textEditor.edit(function(editBuilder: vscode.TextEditorEdit) { + editBuilder.delete(new vscode.Range(new vscode.Position(4, 0), new vscode.Position(4, 13))); + }); + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + diagnostics = vscode.languages.getDiagnostics(document.uri); + const totalDiagnosticsAfterEdit: number = diagnostics.length; + + assert.strictEqual(totalDiagnosticsAfterEdit === 0, true, `Expected exactly 0 diagnostic warnings, found ${totalDiagnosticsInitial}`); + }); + + test("2 Properties With 1 Extra Definition Each Results in 4 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-properties-2.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 4, true, `Expected exactly 4 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < 2; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/farm1' is already defined in the current scope (Scope ID: c81e728d9d4c2f636f067f89cc14862c)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + + for (let i = 2; i < 4; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/farm2' is already defined in the current scope (Scope ID: c81e728d9d4c2f636f067f89cc14862c)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("Duplicate Property Diagnostic Warnings Are Contained In Their Respective Property Scope", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-properties-3.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 6, true, `Expected exactly 6 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < 2; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/url' is already defined in the current scope (Scope ID: c81e728d9d4c2f636f067f89cc14862c)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + + for (let i = 2; i < 4; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/extension' is already defined in the current scope (Scope ID: 1679091c5a880faf6fb5e6087eb1b2dc)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + + for (let i = 4; i < 6; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/type' is already defined in the current scope (Scope ID: d3d9446802a44259755d38e6d163e820)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("6 Duplicate Properties Results in 6 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-properties-4.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 6, true, `Expected exactly 6 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < totalDiagnostics; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/type' is already defined in the current scope (Scope ID: cfcd208495d565ef66e7dff9f98764da)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("25 Duplicate Properties Results in 25 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-properties-5.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 25, true, `Expected exactly 25 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < totalDiagnostics; i++) { + assert.strictEqual(diagnostics[i].message, "The property '/type' is already defined in the current scope (Scope ID: cfcd208495d565ef66e7dff9f98764da)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("Pair of Duplicate Properties/Strings Results in 6 Diagnostic Warnings With 2 Scope IDs", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-property-string-pairs-1.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 6, true, `Expected exactly 6 diagnostic warnings, found ${totalDiagnostics}`); + + const clientHeadersDiagnosticMessage: string = "The property '/clientheaders' is already defined in the current scope (Scope ID: c4ca4238a0b923820dcc509a6f75849b)."; + + assert.strictEqual(diagnostics[0].message, clientHeadersDiagnosticMessage); + assert.strictEqual(diagnostics[0].severity, vscode.DiagnosticSeverity.Warning); + + const alreadyDefinedStringScopeOneDiagnosticMessage: string = "The string '*' is already defined in the current scope (Scope ID: c81e728d9d4c2f636f067f89cc14862c)."; + const alreadyDefinedStringScopeTwoDiagnosticMessage: string = "The string '*' is already defined in the current scope (Scope ID: 1679091c5a880faf6fb5e6087eb1b2dc)."; + + assert.strictEqual(diagnostics[1].message, clientHeadersDiagnosticMessage); + assert.strictEqual(diagnostics[1].severity, vscode.DiagnosticSeverity.Warning); + + assert.strictEqual(diagnostics[2].message, alreadyDefinedStringScopeOneDiagnosticMessage); + assert.strictEqual(diagnostics[2].severity, vscode.DiagnosticSeverity.Warning); + + assert.strictEqual(diagnostics[3].message, alreadyDefinedStringScopeOneDiagnosticMessage); + assert.strictEqual(diagnostics[3].severity, vscode.DiagnosticSeverity.Warning); + + assert.strictEqual(diagnostics[4].message, alreadyDefinedStringScopeTwoDiagnosticMessage); + assert.strictEqual(diagnostics[4].severity, vscode.DiagnosticSeverity.Warning); + + assert.strictEqual(diagnostics[5].message, alreadyDefinedStringScopeTwoDiagnosticMessage); + assert.strictEqual(diagnostics[5].severity, vscode.DiagnosticSeverity.Warning); + }); + + test("2 Strings With Same Value Results in 2 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-strings-1.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 2, true, `Expected exactly 2 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < 2; i++) { + assert.strictEqual(diagnostics[i].message, "The string '${PUBLISH_DEFAULT_HOSTNAME}' is already defined in the current scope (Scope ID: 9c70933aff6b2a6d08c687a6cbb6b765)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("2 Strings With Same Value But With Different Quotation Marks Results in 2 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-strings-2.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 2, true, `Expected exactly 2 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < totalDiagnostics; i++) { + assert.strictEqual(diagnostics[i].message, "The string '${PUBLISH_DEFAULT_HOSTNAME}' is already defined in the current scope (Scope ID: 9c70933aff6b2a6d08c687a6cbb6b765)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("25 Strings With Same Value Results in 25 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("diagnostics/duplicate-strings-3.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 25, true, `Expected exactly 25 diagnostic warnings, found ${totalDiagnostics}`); + + for (let i = 0; i < totalDiagnostics; i++) { + assert.strictEqual(diagnostics[i].message, "The string 'authorization' is already defined in the current scope (Scope ID: 9c70933aff6b2a6d08c687a6cbb6b765)."); + assert.strictEqual(diagnostics[i].severity, vscode.DiagnosticSeverity.Warning); + } + }); + + test("'dispatcher.any' File With 0 Duplicate Properties/Strings Has 0 Diagnostic Warnings", async () => { + const textEditor: vscode.TextEditor = await openDocumentByRelativeUri("generic/dispatcher.any"); + const document: vscode.TextDocument = textEditor.document; + + await sleep(DIAGNOSTIC_SLEEP_TIMEOUT_MS); + + const diagnostics: vscode.Diagnostic[] = vscode.languages.getDiagnostics(document.uri); + const totalDiagnostics: number = diagnostics.length; + + assert.strictEqual(totalDiagnostics === 0, true, `Expected exactly 0 diagnostic warnings, found ${totalDiagnostics}`); + }); +}); diff --git a/test/workspace/diagnostics/duplicate-properties-1.any b/test/workspace/diagnostics/duplicate-properties-1.any new file mode 100644 index 0000000..ddb6901 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-properties-1.any @@ -0,0 +1,6 @@ +/name "internet-server" + +/farms { + /farm1 {} + /farm1 {} +} diff --git a/test/workspace/diagnostics/duplicate-properties-2.any b/test/workspace/diagnostics/duplicate-properties-2.any new file mode 100644 index 0000000..e0e4842 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-properties-2.any @@ -0,0 +1,8 @@ +/name "internet-server" + +/farms { + /farm1 {} + /farm1 {} + /farm2 {} + /farm2 {} +} diff --git a/test/workspace/diagnostics/duplicate-properties-3.any b/test/workspace/diagnostics/duplicate-properties-3.any new file mode 100644 index 0000000..155fb90 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-properties-3.any @@ -0,0 +1,31 @@ +# The content of this file was taken and modified from: https://github.com/adobe/aem-project-archetype/blob/develop/src/main/archetype/dispatcher.ams/src/conf.dispatcher.d/filters/ams_publish_filters.any + +/0001 { /type "deny" /url "*" /url "*" } + +/0010 { /type "allow" /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|pdf|png|svg|swf|ttf|woff|woff2|html)' /path "/content/*" } + +/0011 { /type "allow" /extension "json" /selectors "model" /path "/content/*" /extension "json" } + +/0012 { /type "allow" /method "GET" /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|png|svg|swf|ttf|woff|woff2)' /path "/etc/clientlibs/*" } + +/0013 { /type "allow" /type "allow" /method "GET" /url "/etc.clientlibs/*" } + +/0014 { /type "allow" /method "GET" /url '/libs/granite/csrf/token.json' /extension 'json' } + +/0015 { /type "allow" /method "POST" /url "/content/[.]*.form.html" } + +/0016 { /type "allow" /method "GET" /path "/libs/cq/personalization" } + +/0017 { /type "allow" /method "POST" /path "/content/[.]*.commerce.cart.json" } + +/0100 { /type "deny" /selectors '(feed|rss|pages|languages|blueprint|infinity|tidy|sysview|docview|query|[0-9-]+|jcr:content)' /extension '(json|xml|html|feed)' } + +/0101 { /type "deny" /method "GET" /query "debug=*" } + +/0102 { /type "deny" /method "GET" /query "wcmmode=*" } + +/0103 { /type "deny" /path "/content/ams/healthcheck/*" } + +/0104 { /type "deny" /url "/content/regent.html" } + +/0105 { /type "allow" /extension '(gltf|stl|obj|usdz|glb)' /method "GET" /path "/content/dam/*" } diff --git a/test/workspace/diagnostics/duplicate-properties-4.any b/test/workspace/diagnostics/duplicate-properties-4.any new file mode 100644 index 0000000..b46ee66 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-properties-4.any @@ -0,0 +1,10 @@ +/0001 { + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|pdf|png|svg|swf|ttf|woff|woff2|html)' + /path "/content/*" +} diff --git a/test/workspace/diagnostics/duplicate-properties-5.any b/test/workspace/diagnostics/duplicate-properties-5.any new file mode 100644 index 0000000..1aaacbf --- /dev/null +++ b/test/workspace/diagnostics/duplicate-properties-5.any @@ -0,0 +1,29 @@ +/0001 { + /type "allow" + /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|pdf|png|svg|swf|ttf|woff|woff2|html)' + /path "/content/*" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" + /type "allow" +} diff --git a/test/workspace/diagnostics/duplicate-property-string-pairs-1.any b/test/workspace/diagnostics/duplicate-property-string-pairs-1.any new file mode 100644 index 0000000..60a4254 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-property-string-pairs-1.any @@ -0,0 +1,12 @@ +/farms { + /publish { + /clientheaders { + "*" + "*" + } + /clientheaders { + "*" + "*" + } + } +} diff --git a/test/workspace/diagnostics/duplicate-strings-1.any b/test/workspace/diagnostics/duplicate-strings-1.any new file mode 100644 index 0000000..ecf99f0 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-strings-1.any @@ -0,0 +1,2 @@ +"${PUBLISH_DEFAULT_HOSTNAME}" +"${PUBLISH_DEFAULT_HOSTNAME}" diff --git a/test/workspace/diagnostics/duplicate-strings-2.any b/test/workspace/diagnostics/duplicate-strings-2.any new file mode 100644 index 0000000..0426b3e --- /dev/null +++ b/test/workspace/diagnostics/duplicate-strings-2.any @@ -0,0 +1,2 @@ +"${PUBLISH_DEFAULT_HOSTNAME}" +'${PUBLISH_DEFAULT_HOSTNAME}' diff --git a/test/workspace/diagnostics/duplicate-strings-3.any b/test/workspace/diagnostics/duplicate-strings-3.any new file mode 100644 index 0000000..79721a3 --- /dev/null +++ b/test/workspace/diagnostics/duplicate-strings-3.any @@ -0,0 +1,25 @@ +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" +"authorization" diff --git a/test/workspace/generic/dispatcher.any b/test/workspace/generic/dispatcher.any new file mode 100644 index 0000000..1c61d2e --- /dev/null +++ b/test/workspace/generic/dispatcher.any @@ -0,0 +1,112 @@ +# This file was borrowed from https://github.com/adobe/aem-dispatcher-experiments/blob/main/dispatcher-config-basic/private/etc/apache2/conf/dispatcher.any and modified + +/farms { + /publish { + /clientheaders { + "*" + } + + /virtualhosts { + "aem-publish.local" + } + + /renders { + /rend01 { + /hostname "127.0.0.1" + /port "4503" + } + } + + /filter { + /0000 { /url "/*" /type "allow" } + /0001 { /type "deny" /url '/(system|crx|admin)(/.*)?' } + /0002 { /type "allow" /url "/system/sling/logout*" } + } + + /cache { + /docroot "/Library/WebServer/docroot/publish" + + /statfileslevel "0" + + /allowAuthorized "0" + + /rules { + /0000 { + /glob "*" + /type "deny" + } + + /0005 { + /glob "/content/*" + /type "allow" + } + + /0006 { + /glob "/etc.clientlibs/*" + /type "allow" + } + + /0007 { + /glob "/favicon.ico" + /type "allow" + } + + /0008 { + /glob "/conf/*" + /type "allow" + } + } + + /invalidate { + /0000 { + /glob "*" + /type "deny" + } + + /0001 { + /glob "*.html" + /type "allow" + } + + /0002 { + /glob "/etc/segmentation.segment.js" + /type "allow" + } + + /0003 { + /glob "*/analytics.sitecatalyst.js" + /type "allow" + } + } + + /allowedClients { + /0000 { + /glob "*" + /type "deny" + } + + /0001 { + /glob "127.0.0.1" + /type "allow" + } + } + + /ignoreUrlParams { + /0001 { /glob "*" /type "deny" } + /0002 { /glob "utm_campaign" /type "allow" } + } + } + + /statistics { + /categories { + /html { + /glob "*.html" + } + + /others { + /glob "*" + } + } + } + } +}