Files
archify-vscode-ext/src/jsonPointer.ts
T
root-at-skic adc5cdbe97 Suggest brand IDs after brand: and place unknown-brand errors on their line
- Completion for brand values in .archify (YAML) and Archify JSON sources,
  from the bundled brand-marks/catalog.json (107 marks); aliases and
  product names match, so "kafka" offers apache-kafka
- brand/unknown diagnostics carry their JSON pointer only at the start of
  the message; use it when subject.path is missing, so the error lands on
  the brand line instead of the top of the file
- Unit tests for catalog loading, value detection and pointer fallback;
  integration checks for completion and diagnostic placement

Closes #5
2026-10-04 22:05:30 +03:00

88 lines
3.6 KiB
TypeScript

import { findNodeAtLocation, Node, parseTree } from 'jsonc-parser';
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
export interface OffsetRange {
start: number;
end: number;
}
/**
* The JSON pointer an Archify diagnostic is about. Most diagnostics carry it
* in subject.path; brand diagnostics only start their message with it.
*/
export function diagnosticPointer(diagnostic: { message: string; subject?: { path?: string } }): string | undefined {
return diagnostic.subject?.path ?? /^(\/[^\s"]+)\s/.exec(diagnostic.message)?.[1];
}
/** Splits an RFC 6901 JSON pointer into path segments. */
export function pointerSegments(pointer: string): (string | number)[] {
if (!pointer || pointer === '/') return [];
return pointer
.replace(/^\//, '')
.split('/')
.map((raw) => raw.replace(/~1/g, '/').replace(/~0/g, '~'))
.map((segment) => (/^(0|[1-9]\d*)$/.test(segment) ? Number(segment) : segment));
}
/**
* Maps a JSON pointer to a text range. Objects and arrays resolve to their
* property key (or opening bracket) so a "missing property" error does not
* underline a whole block. Unresolvable paths fall back to the deepest
* ancestor that exists.
*/
export function rangeForPointer(text: string, pointer: string | undefined): OffsetRange {
const root = parseTree(text, [], { allowTrailingComma: true });
if (!root) return { start: 0, end: 0 };
const segments = pointerSegments(pointer ?? '');
let node: Node | undefined;
for (let length = segments.length; length >= 0 && !node; length -= 1) {
node = findNodeAtLocation(root, segments.slice(0, length));
}
if (!node) return { start: 0, end: 0 };
if (node.type === 'object' || node.type === 'array') {
const key = node.parent?.type === 'property' ? node.parent.children?.[0] : undefined;
if (key) return { start: key.offset, end: key.offset + key.length };
return { start: node.offset, end: node.offset + 1 };
}
return { start: node.offset, end: node.offset + node.length };
}
/** The same mapping for YAML text (`.archify` files). */
export function rangeForPointerYaml(text: string, pointer: string | undefined): OffsetRange {
const doc = parseDocument(text, { keepSourceTokens: false, uniqueKeys: false });
let node: unknown = doc.contents;
let keyRange: [number, number] | undefined;
for (const segment of pointerSegments(pointer ?? '')) {
if (isMap(node)) {
const pair = node.items.find((item) => isScalar(item.key) && String(item.key.value) === String(segment));
if (!pair) break;
keyRange = isScalar(pair.key) && pair.key.range ? [pair.key.range[0], pair.key.range[1]] : undefined;
node = pair.value;
} else if (isSeq(node) && typeof segment === 'number' && segment < node.items.length) {
keyRange = undefined;
node = node.items[segment];
} else {
break;
}
}
if ((isMap(node) || isSeq(node)) && keyRange) return { start: keyRange[0], end: keyRange[1] };
const range = (node as { range?: [number, number, number] } | null)?.range;
if (!range) return { start: 0, end: 0 };
if (isMap(node) || isSeq(node)) return { start: range[0], end: Math.min(range[0] + 1, range[1]) };
return { start: range[0], end: range[1] };
}
export interface SyntaxProblem extends OffsetRange {
message: string;
}
/** YAML syntax errors with their offsets (none for valid YAML). */
export function yamlSyntaxProblems(text: string): SyntaxProblem[] {
const doc = parseDocument(text, { uniqueKeys: false });
return doc.errors.map((error) => ({
start: error.pos[0],
end: Math.max(error.pos[1], error.pos[0] + 1),
message: error.message.split('\n')[0],
}));
}