import { GetLocationForJsonPath, ILocation, JsonPath, Optional } from '@stoplight/types'; import { SpecialMappingKeys } from './consts'; import { lineForPosition } from './lineForPosition'; import { Kind, YAMLMapping, YAMLNode, YamlParserResult } from './types'; import { isObject } from './utils'; export const getLocationForJsonPath: GetLocationForJsonPath> = ( { ast, lineMap, metadata }, path, closest = false, ) => { const node = findNodeAtPath(ast, path, { closest, mergeKeys: metadata !== undefined && metadata.mergeKeys === true }); if (node === void 0) return; return getLoc(lineMap, { start: getStartPosition(node, lineMap.length > 0 ? lineMap[0] : 0), end: getEndPosition(node), }); }; function getStartPosition(node: YAMLNode, offset: number): number { if (node.parent && node.parent.kind === Kind.MAPPING) { // the parent is a mapping with no value, let's default to the end of node if (node.parent.value === null) { return node.parent.endPosition; } if (node.kind !== Kind.SCALAR) { return node.parent.key.endPosition + 1; // offset for colon } } if (node.parent === null && offset - node.startPosition === 0) { return 0; } return node.startPosition; } function getEndPosition(node: YAMLNode): number { switch (node.kind) { case Kind.SEQ: const { items } = node; if (items.length !== 0) { const lastItem = items[items.length - 1]; if (lastItem !== null) { return getEndPosition(lastItem); } } break; case Kind.MAPPING: if (node.value !== null) { return getEndPosition(node.value); } break; case Kind.MAP: if (node.value !== null && node.mappings.length !== 0) { return getEndPosition(node.mappings[node.mappings.length - 1]); } break; case Kind.SCALAR: // the parent is a mapping with no value, let's default to the end of node if (node.parent !== null && node.parent.kind === Kind.MAPPING && node.parent.value === null) { return node.parent.endPosition; } break; } return node.endPosition; } function findNodeAtPath( node: YAMLNode, path: JsonPath, { closest, mergeKeys }: { closest: boolean; mergeKeys: boolean }, ) { pathLoop: for (const segment of path) { if (!isObject(node)) { return closest ? node : void 0; } switch (node.kind) { case Kind.MAP: const mappings = getMappings(node.mappings, mergeKeys); // we iterate from the last to first to be compliant with JSONish mode // in other words, iterating from the last to first guarantees we choose the last node that might override other matching nodes for (let i = mappings.length - 1; i >= 0; i--) { const item = mappings[i]; if (item.key.value === segment) { if (item.value === null) { node = item.key; } else { node = item.value; } continue pathLoop; } } return closest ? node : void 0; case Kind.SEQ: for (let i = 0; i < node.items.length; i++) { if (i === Number(segment)) { const item = node.items[i]; if (item === null) { break; } node = item; continue pathLoop; } } return closest ? node : void 0; default: return closest ? node : void 0; } } return node; } function getMappings(mappings: YAMLMapping[], mergeKeys: boolean): YAMLMapping[] { if (!mergeKeys) return mappings; return mappings.reduce((mergedMappings, mapping) => { if (isObject(mapping)) { if (mapping.key.value === SpecialMappingKeys.MergeKey) { mergedMappings.push(...reduceMergeKeys(mapping.value)); } else { mergedMappings.push(mapping); } } return mergedMappings; }, []); } function reduceMergeKeys(node: Optional): YAMLMapping[] { if (!isObject(node)) return []; switch (node.kind) { case Kind.SEQ: return node.items.reduceRight((items, item) => { items.push(...reduceMergeKeys(item)); return items; }, []); case Kind.MAP: return node.mappings; case Kind.ANCHOR_REF: return reduceMergeKeys(node.value); default: return []; } } const getLoc = (lineMap: number[], { start = 0, end = 0 }): ILocation => { const startLine = lineForPosition(start, lineMap); const endLine = lineForPosition(end, lineMap); return { range: { start: { line: startLine, character: start - (startLine === 0 ? 0 : lineMap[startLine - 1]), }, end: { line: endLine, character: end - (endLine === 0 ? 0 : lineMap[endLine - 1]), }, }, }; };