From 11d2fdd2a558a518c6d8b1fc3857311563613487 Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 13:37:32 +0200 Subject: [PATCH 1/7] refactor(definition): break down `createDefinitionLink` into smaller functions --- server/src/providers/definition.ts | 106 +++++++++++++++++------------ 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/server/src/providers/definition.ts b/server/src/providers/definition.ts index 2c88530..f9cd300 100644 --- a/server/src/providers/definition.ts +++ b/server/src/providers/definition.ts @@ -1,11 +1,11 @@ import path from 'path'; import { URI } from 'vscode-uri'; +import * as fs from 'fs'; import { DefinitionParams, DefinitionLink, Range } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { JRefSymbol } from '../visitor'; +import { JRefSymbol, SymbolTable } from '../visitor'; import { analyze, ServerContext } from '../utils'; -import * as fs from 'fs'; const defaultTargetRange: Range = { start: { line: 0, character: 0 }, @@ -37,50 +37,72 @@ function createDefinitionLink( document: TextDocument, ref: JRefSymbol, context: ServerContext, -): DefinitionLink[] | undefined { - const { documents, documentSymbols } = context; - const refValueNode = ref.node; - const targetPath = refValueNode.value; - const uri = URI.parse(targetPath); - const currentDir = path.dirname(URI.parse(document.uri).fsPath); - const absolutePath = path.resolve(currentDir, uri.path.slice(1)); - const targetUri = URI.file(absolutePath).toString(); - const targetRange = getTargetRange(targetUri); - - function getTargetRange(targetUri: string): Range { - let targetDocument = documents.get(targetUri); - if (!targetDocument) { - try { - const filePath = URI.parse(targetUri).fsPath; - const content = fs.readFileSync(filePath, 'utf8'); - targetDocument = TextDocument.create(targetUri, 'jref', 1, content); - } catch (e) { - return defaultTargetRange; - } - } - let targetSymbolTable = documentSymbols.get(targetDocument); - if (!targetSymbolTable) { - const { symbols } = analyze(targetDocument.getText()); - documentSymbols.set(targetDocument, symbols); - targetSymbolTable = symbols; - } - const targetSymbol = targetSymbolTable?.get(uri.fragment); - if (!targetSymbol) return defaultTargetRange; - return { - start: targetDocument.positionAt(targetSymbol.node.offset), - end: targetDocument.positionAt(targetSymbol.node.offset + targetSymbol.node.length), - }; - } +): DefinitionLink[] { + const { targetUri, fragment } = resolveTargetUriAndFragment(document.uri, ref.node.value); + + const targetRange = findTargetRange(targetUri, fragment, context); return [ { - originSelectionRange: { - start: document.positionAt(refValueNode.offset + 1), // +1 to skip the opening quote - end: document.positionAt(refValueNode.offset + refValueNode.length - 1), // -1 to skip the closing quote - }, - targetUri: URI.file(absolutePath).toString(), - targetRange: targetRange, + originSelectionRange: createOriginSelectionRange(document, ref), + targetUri, + targetRange, targetSelectionRange: targetRange, }, ]; } + +function resolveTargetUriAndFragment(documentUri: string, targetPath: string) { + const uri = URI.parse(targetPath); + const currentDir = path.dirname(URI.parse(documentUri).fsPath); + const absolutePath = path.resolve(currentDir, uri.path.slice(1)); + const targetUri = URI.file(absolutePath).toString(); + return { targetUri, fragment: uri.fragment }; +} + +function getOrLoadDocument(targetUri: string, context: ServerContext): TextDocument | undefined { + const { documents } = context; + const targetDocument = documents.get(targetUri); + if (targetDocument) return targetDocument; + + try { + const filePath = URI.parse(targetUri).fsPath; + const content = fs.readFileSync(filePath, 'utf8'); + return TextDocument.create(targetUri, 'jref', 1, content); + } catch (e) { + return; + } +} + +function getOrAnalyzeSymbols(targetDocument: TextDocument, context: ServerContext): SymbolTable { + const { documentSymbols } = context; + let targetSymbolTable = documentSymbols.get(targetDocument); + if (targetSymbolTable) return targetSymbolTable; + + const { symbols } = analyze(targetDocument.getText()); + documentSymbols.set(targetDocument, symbols); + return symbols; +} + +function findTargetRange(targetUri: string, fragment: string, context: ServerContext): Range { + const targetDocument = getOrLoadDocument(targetUri, context); + if (!targetDocument) return defaultTargetRange; + + const targetSymbolTable = getOrAnalyzeSymbols(targetDocument, context); + const targetSymbol = targetSymbolTable.get(fragment || ''); // Default to empty string if no fragment + + if (!targetSymbol) return defaultTargetRange; + + return { + start: targetDocument.positionAt(targetSymbol.node.offset), + end: targetDocument.positionAt(targetSymbol.node.offset + targetSymbol.node.length), + }; +} + +function createOriginSelectionRange(document: TextDocument, ref: JRefSymbol): Range { + const refValueNode = ref.node; + return { + start: document.positionAt(refValueNode.offset + 1), // +1 to skip the opening quote + end: document.positionAt(refValueNode.offset + refValueNode.length - 1), // -1 to skip the closing quote + }; +} From 57ebd716120572578789e06cc89c3146230ea7f1 Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 14:00:46 +0200 Subject: [PATCH 2/7] refactor(visitor): move symbol table to a dedicated file --- server/src/providers/definition.ts | 2 +- server/src/server.ts | 2 +- server/src/symbolTable.ts | 10 ++++++++++ server/src/test/definition.test.ts | 3 ++- server/src/test/visitor.test.ts | 3 ++- server/src/utils.ts | 3 ++- server/src/visitor.ts | 7 +------ 7 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 server/src/symbolTable.ts diff --git a/server/src/providers/definition.ts b/server/src/providers/definition.ts index f9cd300..9570975 100644 --- a/server/src/providers/definition.ts +++ b/server/src/providers/definition.ts @@ -4,8 +4,8 @@ import * as fs from 'fs'; import { DefinitionParams, DefinitionLink, Range } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { JRefSymbol, SymbolTable } from '../visitor'; import { analyze, ServerContext } from '../utils'; +import { JRefSymbol, SymbolTable } from '../symbolTable'; const defaultTargetRange: Range = { start: { line: 0, character: 0 }, diff --git a/server/src/server.ts b/server/src/server.ts index e4508c9..fa148d9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -14,7 +14,7 @@ import { ParseError } from 'jsonc-parser'; import { createParseErrorDiagnostic } from './providers/diagnostics'; import { onDefinition } from './providers/definition'; -import { SymbolTable } from './visitor'; +import { SymbolTable } from './symbolTable'; import { handleSemanticTokens, tokenTypes } from './providers/semanticTokens'; import { analyze } from './utils'; diff --git a/server/src/symbolTable.ts b/server/src/symbolTable.ts new file mode 100644 index 0000000..a81cc76 --- /dev/null +++ b/server/src/symbolTable.ts @@ -0,0 +1,10 @@ +import { Node } from 'jsonc-parser'; + +export interface JRefSymbol { + pointer: string; + node: Node; + isReference: boolean; + refersTo: string | null; +} + +export type SymbolTable = Map; diff --git a/server/src/test/definition.test.ts b/server/src/test/definition.test.ts index 833b198..7417d47 100644 --- a/server/src/test/definition.test.ts +++ b/server/src/test/definition.test.ts @@ -3,7 +3,8 @@ import { URI } from 'vscode-uri'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { onDefinition } from '../providers/definition.js'; import { parseTree } from 'jsonc-parser'; -import { SymbolTable, visit } from '../visitor.js'; +import { visit } from '../visitor.js'; +import { SymbolTable } from '../symbolTable.js'; import { DefinitionParams } from 'vscode-languageserver/node'; import path from 'path'; diff --git a/server/src/test/visitor.test.ts b/server/src/test/visitor.test.ts index 6e5b02f..6419152 100644 --- a/server/src/test/visitor.test.ts +++ b/server/src/test/visitor.test.ts @@ -1,6 +1,7 @@ import * as assert from 'assert'; import { parseTree } from 'jsonc-parser'; -import { SymbolTable, visit } from '../visitor.js'; +import { visit } from '../visitor.js'; +import { SymbolTable } from '../symbolTable.js'; suite('Visitor Test Suite', () => { test('Should find a single $ref in a simple object', () => { diff --git a/server/src/utils.ts b/server/src/utils.ts index 6785899..dabe034 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -1,5 +1,6 @@ import { TextDocuments } from 'vscode-languageserver/node'; -import { SymbolTable, visit } from './visitor'; +import { visit } from './visitor'; +import { SymbolTable } from './symbolTable'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { ParseError, parseTree } from 'jsonc-parser'; diff --git a/server/src/visitor.ts b/server/src/visitor.ts index 0208d43..3b52e7e 100644 --- a/server/src/visitor.ts +++ b/server/src/visitor.ts @@ -1,11 +1,6 @@ import { Node } from 'jsonc-parser'; -export interface JRefSymbol { - pointer: string; - node: Node; - isReference: boolean; - refersTo: string | null; -} + export type SymbolTable = Map; From 456e97f4e10ea011244a843a0c31caaad2d24aab Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 14:13:18 +0200 Subject: [PATCH 3/7] refactor(visitor): moved `analyze` to a dedicated file --- server/src/analyzer.ts | 34 ++++++++++++++++++++++++++++++ server/src/providers/definition.ts | 3 ++- server/src/server.ts | 2 +- server/src/test/definition.test.ts | 22 +++++-------------- server/src/test/visitor.test.ts | 31 +++++++-------------------- server/src/utils.ts | 14 ------------ server/src/visitor.ts | 1 - 7 files changed, 50 insertions(+), 57 deletions(-) create mode 100644 server/src/analyzer.ts diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts new file mode 100644 index 0000000..263a652 --- /dev/null +++ b/server/src/analyzer.ts @@ -0,0 +1,34 @@ +import { Node, parseTree, ParseError } from 'jsonc-parser'; +import { SymbolTable, JRefSymbol } from './symbolTable'; +import { visit } from './visitor'; + +export interface DocumentAnalysis { + symbols: SymbolTable; + errors: ParseError[]; +} + +export function analyze(content: string): DocumentAnalysis { + const errors: ParseError[] = []; + const ast = parseTree(content, errors); + const symbols: SymbolTable = new Map(); + visit(ast, symbols); + return { symbols, errors }; +} + +function createJRefSymbol(node: Node, pointer: string): JRefSymbol { + const isReference = isReferenceValue(node); + return { + pointer, + node, + isReference, + refersTo: isReference ? node.value : null, + }; +} + +function isReferenceValue(node: Node): boolean { + return ( + node.type === 'string' && + node.parent?.type === 'property' && + node.parent.children?.[0].value === '$ref' + ); +} diff --git a/server/src/providers/definition.ts b/server/src/providers/definition.ts index 9570975..b836d28 100644 --- a/server/src/providers/definition.ts +++ b/server/src/providers/definition.ts @@ -4,8 +4,9 @@ import * as fs from 'fs'; import { DefinitionParams, DefinitionLink, Range } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { analyze, ServerContext } from '../utils'; import { JRefSymbol, SymbolTable } from '../symbolTable'; +import { ServerContext } from '../utils'; +import { analyze } from '../analyzer'; const defaultTargetRange: Range = { start: { line: 0, character: 0 }, diff --git a/server/src/server.ts b/server/src/server.ts index fa148d9..e7e3b85 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -16,7 +16,7 @@ import { createParseErrorDiagnostic } from './providers/diagnostics'; import { onDefinition } from './providers/definition'; import { SymbolTable } from './symbolTable'; import { handleSemanticTokens, tokenTypes } from './providers/semanticTokens'; -import { analyze } from './utils'; +import { analyze } from './analyzer'; // Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. diff --git a/server/src/test/definition.test.ts b/server/src/test/definition.test.ts index 7417d47..4229aca 100644 --- a/server/src/test/definition.test.ts +++ b/server/src/test/definition.test.ts @@ -2,9 +2,7 @@ import * as assert from 'assert'; import { URI } from 'vscode-uri'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { onDefinition } from '../providers/definition.js'; -import { parseTree } from 'jsonc-parser'; -import { visit } from '../visitor.js'; -import { SymbolTable } from '../symbolTable.js'; +import { analyze } from '../analyzer.js'; import { DefinitionParams } from 'vscode-languageserver/node'; import path from 'path'; @@ -24,10 +22,7 @@ suite('Definition Test Suite', () => { const uri = URI.file(path.resolve('/abs/path/main.jref')).toString(); const doc = TextDocument.create(uri, 'jref', 1, text); - const errors: any[] = []; - const ast = parseTree(text, errors); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); const context = { documents: new MockTextDocuments([doc]) as any, @@ -51,10 +46,7 @@ suite('Definition Test Suite', () => { const uri = URI.file(path.resolve('/abs/path/main.jref')).toString(); const doc = TextDocument.create(uri, 'jref', 1, text); - const errors: any[] = []; - const ast = parseTree(text, errors); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); const context = { documents: new MockTextDocuments([doc]) as any, @@ -79,13 +71,9 @@ suite('Definition Test Suite', () => { const schemaUri = URI.file(path.resolve('/abs/path/schema.jref')).toString(); const schemaDoc = TextDocument.create(schemaUri, 'jref', 1, schemaText); - const mainAst = parseTree(mainText); - const mainSymbols: SymbolTable = new Map(); - visit(mainAst, mainSymbols); + const { symbols: mainSymbols } = analyze(mainText); - const schemaAst = parseTree(schemaText); - const schemaSymbols: SymbolTable = new Map(); - visit(schemaAst, schemaSymbols); + const { symbols: schemaSymbols } = analyze(schemaText); const context = { documents: new MockTextDocuments([mainDoc, schemaDoc]) as any, diff --git a/server/src/test/visitor.test.ts b/server/src/test/visitor.test.ts index 6419152..259d3c3 100644 --- a/server/src/test/visitor.test.ts +++ b/server/src/test/visitor.test.ts @@ -1,14 +1,10 @@ import * as assert from 'assert'; -import { parseTree } from 'jsonc-parser'; -import { visit } from '../visitor.js'; -import { SymbolTable } from '../symbolTable.js'; +import { analyze } from '../analyzer.js'; suite('Visitor Test Suite', () => { test('Should find a single $ref in a simple object', () => { const text = '{"$ref": "path/to/schema.json"}'; - const ast = parseTree(text); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); assert.strictEqual(symbols.get('/$ref')?.node.type, 'string'); assert.strictEqual(symbols.get('/$ref')?.isReference, true); @@ -20,9 +16,7 @@ suite('Visitor Test Suite', () => { "first": { "$ref": "one.json" }, "second": { "inner": { "$ref": "two.json" } } }`; - const ast = parseTree(text); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); assert.strictEqual(symbols.get('/first/$ref')?.node.type, 'string'); assert.strictEqual(symbols.get('/first/$ref')?.isReference, true); @@ -39,9 +33,7 @@ suite('Visitor Test Suite', () => { { "other": "value" }, { "$ref": "item2.json" } ]`; - const ast = parseTree(text); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); assert.strictEqual(symbols.get('/0/$ref')?.node.type, 'string'); assert.strictEqual(symbols.get('/0/$ref')?.isReference, true); @@ -58,9 +50,7 @@ suite('Visitor Test Suite', () => { test('Should NOT pick up $ref if the value is not a string', () => { const text = '{"$ref": 123}'; - const ast = parseTree(text); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); assert.strictEqual(symbols.get('/$ref')?.isReference, false); assert.strictEqual(symbols.get('/$ref')?.refersTo, null); @@ -68,9 +58,7 @@ suite('Visitor Test Suite', () => { test('Should handle empty objects and arrays', () => { const text = '{"obj": {}, "arr": []}'; - const ast = parseTree(text); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); assert.ok(symbols.has('/obj'), 'Should have a symbol for the empty object'); assert.strictEqual(symbols.get('/obj')?.node.type, 'object'); @@ -80,16 +68,13 @@ suite('Visitor Test Suite', () => { }); test('Should handle undefined or null nodes gracefully', () => { - const symbols: SymbolTable = new Map(); - visit(undefined, symbols); + const { symbols } = analyze(''); assert.strictEqual(symbols.size, 0); }); test('Should handle array elements', () => { const text = '{"arr": ["a", "b", "c"]}'; - const ast = parseTree(text); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); + const { symbols } = analyze(text); assert.ok(symbols.has('/arr'), 'Should have a symbol for the array'); assert.strictEqual(symbols.get('/arr')?.node.type, 'array'); diff --git a/server/src/utils.ts b/server/src/utils.ts index dabe034..9d3f110 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -2,22 +2,8 @@ import { TextDocuments } from 'vscode-languageserver/node'; import { visit } from './visitor'; import { SymbolTable } from './symbolTable'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { ParseError, parseTree } from 'jsonc-parser'; export interface ServerContext { documents: TextDocuments; documentSymbols: WeakMap; } - -export interface DocumentAnalysis { - symbols: SymbolTable; - errors: ParseError[]; -} - -export function analyze(content: string): DocumentAnalysis { - const errors: ParseError[] = []; - const ast = parseTree(content, errors); - const symbols: SymbolTable = new Map(); - visit(ast, symbols); - return { symbols, errors }; -} diff --git a/server/src/visitor.ts b/server/src/visitor.ts index 3b52e7e..3159cd6 100644 --- a/server/src/visitor.ts +++ b/server/src/visitor.ts @@ -2,7 +2,6 @@ import { Node } from 'jsonc-parser'; -export type SymbolTable = Map; const visitFunctions: Record void> = { object: visitObject, From eba2630eebbe95c5759a1dddb16b66408a01141b Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 14:21:42 +0200 Subject: [PATCH 4/7] fix(visitor): add missing import --- server/src/visitor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/visitor.ts b/server/src/visitor.ts index 3159cd6..ae25168 100644 --- a/server/src/visitor.ts +++ b/server/src/visitor.ts @@ -1,7 +1,5 @@ import { Node } from 'jsonc-parser'; - - - +import { SymbolTable, JRefSymbol } from './symbolTable'; const visitFunctions: Record void> = { object: visitObject, From 62c7301144596b0a58eccc478d9e3f5998a3d7ff Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 15:04:22 +0200 Subject: [PATCH 5/7] refactor(visitor): decouple visitor and analysis logic --- server/src/analyzer.ts | 9 ++++-- server/src/visitor.ts | 62 ++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 263a652..c77d8b5 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -1,6 +1,6 @@ import { Node, parseTree, ParseError } from 'jsonc-parser'; import { SymbolTable, JRefSymbol } from './symbolTable'; -import { visit } from './visitor'; +import { visit, VisitCallback } from './visitor'; export interface DocumentAnalysis { symbols: SymbolTable; @@ -11,7 +11,12 @@ export function analyze(content: string): DocumentAnalysis { const errors: ParseError[] = []; const ast = parseTree(content, errors); const symbols: SymbolTable = new Map(); - visit(ast, symbols); + + visit(ast, (node, path) => { + const symbol = createJRefSymbol(node, path); + symbols.set(path, symbol); + }); + return { symbols, errors }; } diff --git a/server/src/visitor.ts b/server/src/visitor.ts index ae25168..7b72cee 100644 --- a/server/src/visitor.ts +++ b/server/src/visitor.ts @@ -1,57 +1,43 @@ import { Node } from 'jsonc-parser'; -import { SymbolTable, JRefSymbol } from './symbolTable'; -const visitFunctions: Record void> = { +export type VisitCallback = (node: Node, path: string) => void; + +export function visit(node: Node | undefined, cb: VisitCallback, path: string = '') { + if (!node?.type) { + return; + } + + const isProperty = node.type === 'property'; + + if (!isProperty) { + cb(node, path); + } + + const visitChildren = visitFunctions[node.type]; + visitChildren?.(node, cb, path); +} + +const visitFunctions: Record void> = { object: visitObject, array: visitArray, property: visitProperty, }; -function visitObject(node: Node, acc: SymbolTable, path: string) { +function visitObject(node: Node, cb: VisitCallback, path: string) { node?.children?.forEach((child) => { - visit(child, acc, path); + visit(child, cb, path); }); } -function visitArray(node: Node, acc: SymbolTable, path: string) { +function visitArray(node: Node, cb: VisitCallback, path: string) { node?.children?.forEach((child, index) => { - visit(child, acc, path + '/' + index); + visit(child, cb, path + '/' + index); }); } -function visitProperty(node: Node, acc: SymbolTable, path: string) { +function visitProperty(node: Node, cb: VisitCallback, path: string) { if (node.children?.length !== 2) return; const key = node.children[0].value; const value = node.children[1]; - visit(value, acc, `${path}/${key}`); -} - -export function visit(node: Node | undefined, acc: SymbolTable, path: string = '') { - if (!node?.type) { - console.error('Node type is undefined'); - return; - } - const symbol = createJRefSymbol(node, path); - acc.set(path, symbol); - - const visit = visitFunctions[node.type]; - visit?.(node, acc, path); -} - -function createJRefSymbol(node: Node, pointer: string): JRefSymbol { - const isReference = isReferenceValue(node); - return { - pointer, - node, - isReference, - refersTo: isReference ? node.value : null, - }; -} - -function isReferenceValue(node: Node): boolean { - return ( - node.type === 'string' && - node.parent?.type === 'property' && - node.parent.children?.[0].value === '$ref' - ); + visit(value, cb, `${path}/${key}`); } From 61460b7b1459a4a1d0464f7ff2399f52446cddbe Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 15:07:15 +0200 Subject: [PATCH 6/7] refactor(visitor): reorganize tests to match analyzer and visitor roles --- server/src/test/{visitor.test.ts => analyzer.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename server/src/test/{visitor.test.ts => analyzer.test.ts} (98%) diff --git a/server/src/test/visitor.test.ts b/server/src/test/analyzer.test.ts similarity index 98% rename from server/src/test/visitor.test.ts rename to server/src/test/analyzer.test.ts index 259d3c3..2ae8a58 100644 --- a/server/src/test/visitor.test.ts +++ b/server/src/test/analyzer.test.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import { analyze } from '../analyzer.js'; -suite('Visitor Test Suite', () => { +suite('Analyzer Test Suite', () => { test('Should find a single $ref in a simple object', () => { const text = '{"$ref": "path/to/schema.json"}'; const { symbols } = analyze(text); From a5b842ecf71a4088205ada9c9d2664381f09646a Mon Sep 17 00:00:00 2001 From: Marios Ntoulas Date: Wed, 4 Mar 2026 15:10:43 +0200 Subject: [PATCH 7/7] test(visitor): add visitor traversal unit test --- server/src/test/visitor.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 server/src/test/visitor.test.ts diff --git a/server/src/test/visitor.test.ts b/server/src/test/visitor.test.ts new file mode 100644 index 0000000..be94de8 --- /dev/null +++ b/server/src/test/visitor.test.ts @@ -0,0 +1,19 @@ +import * as assert from 'assert'; +import { parseTree } from 'jsonc-parser'; +import { visit } from '../visitor.js'; + +suite('Visitor Pattern Test Suite', () => { + test('Should call visit callback for each node in correct order', () => { + const text = '{"a": 1, "b": [2]}'; + const ast = parseTree(text); + + const trace: string[] = []; + visit(ast, (node, path) => { + trace.push(`visit: ${path || '/'}`); + }); + + const expected = ['visit: /', 'visit: /a', 'visit: /b', 'visit: /b/0']; + + assert.deepStrictEqual(trace, expected); + }); +});