UNPKG

@redocly/openapi-core

Version:

See https://github.com/Redocly/redocly-cli

302 lines 15.1 kB
import { Location, isRef } from './ref-utils.js'; import { isNamedType, SpecExtension } from './types/index.js'; import { YamlParseError } from './errors/yaml-parse-error.js'; import { makeRefId } from './utils/make-ref-id.js'; import { pushStack, popStack } from './utils/stack.js'; import { getOwn } from './utils/get-own.js'; function collectParents(ctx) { const parents = {}; while (ctx.parent) { parents[ctx.parent.type.name] = ctx.parent.activatedOn?.value.node; ctx = ctx.parent; } return parents; } function collectParentsLocations(ctx) { const locations = {}; while (ctx.parent) { if (ctx.parent.activatedOn?.value.location) { locations[ctx.parent.type.name] = ctx.parent.activatedOn?.value.location; } ctx = ctx.parent; } return locations; } export function walkDocument(opts) { const { document, rootType, normalizedVisitors, resolvedRefMap, ctx } = opts; const seenNodesPerType = {}; const ignoredNodes = new Set(); walkNode(document.parsed, rootType, new Location(document.source, '#/'), undefined, ''); function walkNode(node, type, location, parent, key) { const resolve = (ref, from = currentLocation.source.absoluteRef) => { if (!isRef(ref)) return { location, node: ref }; const refId = makeRefId(from, ref.$ref); const resolvedRef = resolvedRefMap.get(refId); if (!resolvedRef) { return { location: undefined, node: undefined, }; } const { resolved, node, document, nodePointer, error } = resolvedRef; const newLocation = resolved ? new Location(document.source, nodePointer) : error instanceof YamlParseError ? new Location(error.source, '') : undefined; return { location: newLocation, node, error }; }; const rawLocation = location; let currentLocation = location; const { node: resolvedNode, location: resolvedLocation, error } = resolve(node); const enteredContexts = new Set(); if (isRef(node)) { const refEnterVisitors = normalizedVisitors.ref.enter; for (const { visit: visitor, ruleId, severity, message, context } of refEnterVisitors) { enteredContexts.add(context); const report = reportFn.bind(undefined, ruleId, severity, message); visitor(node, { report, resolve, rawNode: node, rawLocation, location, type, parent, key, parentLocations: {}, specVersion: ctx.specVersion, config: ctx.config, getVisitorData: getVisitorDataFn.bind(undefined, ruleId), }, { node: resolvedNode, location: resolvedLocation, error }); if (resolvedLocation?.source.absoluteRef && ctx.refTypes) { ctx.refTypes.set(resolvedLocation?.source.absoluteRef, type); } } } if (resolvedNode !== undefined && resolvedLocation && type.name !== 'scalar') { currentLocation = resolvedLocation; const isNodeSeen = seenNodesPerType[type.name]?.has?.(resolvedNode); let visitedBySome = false; const anyEnterVisitors = normalizedVisitors.any.enter; const currentEnterVisitors = anyEnterVisitors.concat(normalizedVisitors[type.name]?.enter || []); const activatedContexts = []; for (const { context, visit, skip, ruleId, severity, message } of currentEnterVisitors) { if (ignoredNodes.has(`${currentLocation.absolutePointer}${currentLocation.pointer}`)) break; if (context.isSkippedLevel) { if (context.parent.activatedOn && !context.parent.activatedOn.value.nextLevelTypeActivated && !context.seen.has(node)) { // TODO: test for walk through duplicated $ref-ed node context.seen.add(node); visitedBySome = true; activatedContexts.push(context); } } else { if ((context.parent && // if nested context.parent.activatedOn && context.activatedOn?.value.withParentNode !== context.parent.activatedOn.value.node && // do not enter if visited by parent children (it works thanks because deeper visitors are sorted before) context.parent.activatedOn.value.nextLevelTypeActivated?.value !== type) || (!context.parent && !isNodeSeen) // if top-level visit each node just once ) { activatedContexts.push(context); const activatedOn = { node: resolvedNode, location: resolvedLocation, nextLevelTypeActivated: null, withParentNode: context.parent?.activatedOn?.value.node, skipped: (context.parent?.activatedOn?.value.skipped || skip?.(resolvedNode, key, { location, rawLocation, resolve, rawNode: node, })) ?? false, }; context.activatedOn = pushStack(context.activatedOn, activatedOn); let ctx = context.parent; while (ctx) { ctx.activatedOn.value.nextLevelTypeActivated = pushStack(ctx.activatedOn.value.nextLevelTypeActivated, type); ctx = ctx.parent; } if (!activatedOn.skipped) { visitedBySome = true; enteredContexts.add(context); visitWithContext(visit, resolvedNode, node, context, ruleId, severity, message); } } } } if (visitedBySome || !isNodeSeen) { seenNodesPerType[type.name] = seenNodesPerType[type.name] || new Set(); seenNodesPerType[type.name].add(resolvedNode); if (Array.isArray(resolvedNode)) { const itemsType = type.items; if (itemsType !== undefined) { const isTypeAFunction = typeof itemsType === 'function'; for (let i = 0; i < resolvedNode.length; i++) { let itemType = isTypeAFunction ? itemsType(resolvedNode[i], resolvedLocation.child([i]).absolutePointer) : itemsType; let itemValue = resolvedNode[i]; if (itemType?.directResolveAs) { itemType = itemType.directResolveAs; itemValue = { $ref: itemValue }; } if (isNamedType(itemType)) { walkNode(itemValue, itemType, resolvedLocation.child([i]), resolvedNode, i); } } } } else if (typeof resolvedNode === 'object' && resolvedNode !== null) { // visit in order from type-tree first const props = Object.keys(type.properties); if (type.additionalProperties) { props.push(...Object.keys(resolvedNode).filter((k) => !props.includes(k))); } else if (type.extensionsPrefix) { props.push(...Object.keys(resolvedNode).filter((k) => k.startsWith(type.extensionsPrefix))); } if (isRef(node)) { props.push(...Object.keys(node).filter((k) => k !== '$ref' && !props.includes(k))); // properties on the same level as $ref } for (const propName of props) { let value = resolvedNode[propName]; let loc = resolvedLocation; if (value === undefined) { value = node[propName]; loc = location; // properties on the same level as $ref should resolve against original location, not target } let propType = getOwn(type.properties, propName); if (propType === undefined) propType = type.additionalProperties; if (typeof propType === 'function') propType = propType(value, propName); if (propType === undefined && type.extensionsPrefix && propName.startsWith(type.extensionsPrefix)) { propType = SpecExtension; } if (!isNamedType(propType) && propType?.directResolveAs) { propType = propType.directResolveAs; value = { $ref: value }; } if (propType && propType.name === undefined && propType.resolvable !== false) { propType = { name: 'scalar', properties: {} }; } if (isRef(node[propName]) && propType?.name === 'scalar') { walkNode(node[propName], propType, location.child([propName]), node, propName); continue; } if (!isNamedType(propType) || (propType.name === 'scalar' && !isRef(value))) { continue; } walkNode(value, propType, loc.child([propName]), resolvedNode, propName); } } } const anyLeaveVisitors = normalizedVisitors.any.leave; const currentLeaveVisitors = (normalizedVisitors[type.name]?.leave || []).concat(anyLeaveVisitors); for (const context of activatedContexts.reverse()) { if (context.isSkippedLevel) { context.seen.delete(resolvedNode); } else { context.activatedOn = popStack(context.activatedOn); if (context.parent) { let ctx = context.parent; while (ctx) { ctx.activatedOn.value.nextLevelTypeActivated = popStack(ctx.activatedOn.value.nextLevelTypeActivated); ctx = ctx.parent; } } } } for (const { context, visit, ruleId, severity, message } of currentLeaveVisitors) { if (!context.isSkippedLevel && enteredContexts.has(context)) { visitWithContext(visit, resolvedNode, node, context, ruleId, severity, message); } } } currentLocation = location; if (isRef(node)) { const refLeaveVisitors = normalizedVisitors.ref.leave; for (const { visit: visitor, ruleId, severity, context, message } of refLeaveVisitors) { if (enteredContexts.has(context)) { const report = reportFn.bind(undefined, ruleId, severity, message); visitor(node, { report, resolve, rawNode: node, rawLocation, location, type, parent, key, parentLocations: {}, specVersion: ctx.specVersion, config: ctx.config, getVisitorData: getVisitorDataFn.bind(undefined, ruleId), }, { node: resolvedNode, location: resolvedLocation, error }); } } } // returns true ignores all the next visitors on the specific node function visitWithContext(visit, resolvedNode, node, context, ruleId, severity, customMessage) { const report = reportFn.bind(undefined, ruleId, severity, customMessage); visit(resolvedNode, { report, resolve, rawNode: node, location: currentLocation, rawLocation, type, parent, key, parentLocations: collectParentsLocations(context), specVersion: ctx.specVersion, config: ctx.config, ignoreNextVisitorsOnNode: () => { ignoredNodes.add(`${currentLocation.absolutePointer}${currentLocation.pointer}`); }, getVisitorData: getVisitorDataFn.bind(undefined, ruleId), }, collectParents(context), context); } function reportFn(ruleId, severity, customMessage, opts) { const normalizedLocation = opts.location ? Array.isArray(opts.location) ? opts.location : [opts.location] : [{ ...currentLocation, reportOnKey: false }]; const location = normalizedLocation.map((l) => ({ ...currentLocation, reportOnKey: false, ...l, })); const ruleSeverity = opts.forceSeverity || severity; if (ruleSeverity !== 'off') { ctx.problems.push({ ruleId: opts.ruleId || ruleId, severity: ruleSeverity, ...opts, message: customMessage ? customMessage.replace('{{message}}', opts.message) : opts.message, suggest: opts.suggest || [], location, }); } } function getVisitorDataFn(ruleId) { ctx.visitorsData[ruleId] = ctx.visitorsData[ruleId] || {}; return ctx.visitorsData[ruleId]; } } } //# sourceMappingURL=walk.js.map