UNPKG

yaml-language-server

Version:
955 lines (954 loc) 81.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Red Hat, Inc. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "path", "@vscode/l10n", "ajv", "ajv/dist/2019", "ajv/dist/2020", "ajv-draft-04", "ajv-i18n", "jsonc-parser", "picomatch", "vscode-languageserver-types", "vscode-uri", "yaml", "./dollarUtils", "./modelineUtil", "../jsonLanguageTypes", "../parser/schemaValidation/baseValidator", "../yamlLanguageService", "./k8sSchemaUtil", "../utils/schemaUrls", "../utils/strings"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.YAMLSchemaService = exports.ResolvedSchema = exports.UnresolvedSchema = exports.SchemaHandle = exports.FilePatternAssociation = exports.MODIFICATION_ACTIONS = void 0; const path = __importStar(require("path")); const l10n = __importStar(require("@vscode/l10n")); const ajv_1 = __importDefault(require("ajv")); const _2019_1 = __importDefault(require("ajv/dist/2019")); const _2020_1 = __importDefault(require("ajv/dist/2020")); const ajv_draft_04_1 = __importDefault(require("ajv-draft-04")); const ajvLocalizers = __importStar(require("ajv-i18n")); const Json = __importStar(require("jsonc-parser")); const picomatch_1 = __importDefault(require("picomatch")); const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const vscode_uri_1 = require("vscode-uri"); const yaml_1 = require("yaml"); const dollarUtils_1 = require("./dollarUtils"); const modelineUtil_1 = require("./modelineUtil"); const jsonLanguageTypes_1 = require("../jsonLanguageTypes"); const baseValidator_1 = require("../parser/schemaValidation/baseValidator"); const yamlLanguageService_1 = require("../yamlLanguageService"); const k8sSchemaUtil_1 = require("./k8sSchemaUtil"); const schemaUrls_1 = require("../utils/schemaUrls"); const Strings = __importStar(require("../utils/strings")); const ajv4 = new ajv_draft_04_1.default({ allErrors: true }); const ajv7 = new ajv_1.default({ allErrors: true }); const ajv2019 = new _2019_1.default({ allErrors: true }); const ajv2020 = new _2020_1.default({ allErrors: true }); const schema04Validator = getDefaultMetaSchemaValidator(ajv4); const schema07Validator = getDefaultMetaSchemaValidator(ajv7); const schema2019Validator = getDefaultMetaSchemaValidator(ajv2019); const schema2020Validator = getDefaultMetaSchemaValidator(ajv2020); const schemaDraftCache = new Map(); const schemaDraftInFlight = new Map(); const AJV_LOCALE_ALIASES = new Map([ ['zh-cn', 'zh'], ['zh-tw', 'zh-TW'], ]); const PATH_SEP = '/'; // metadata/keywords that don't add constraints and thus don't count as $ref siblings const REF_SIBLING_NONCONSTRAINT_KEYS = new Set([ '$ref', '_$ref', '$schema', '$id', 'id', '_baseUri', '_sourceUri', '_schemaDraft', '$anchor', '$dynamicAnchor', '$dynamicRef', '$recursiveAnchor', '$recursiveRef', 'definitions', '$defs', '$comment', 'title', 'description', 'markdownDescription', '$vocabulary', 'examples', 'default', 'url', 'closestTitle', 'unevaluatedProperties', 'unevaluatedItems', ]); var MODIFICATION_ACTIONS; (function (MODIFICATION_ACTIONS) { MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["delete"] = 0] = "delete"; MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["add"] = 1] = "add"; MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["deleteAll"] = 2] = "deleteAll"; })(MODIFICATION_ACTIONS || (exports.MODIFICATION_ACTIONS = MODIFICATION_ACTIONS = {})); class FilePatternAssociation { constructor(pattern, folderUri, uris) { this.folderUri = folderUri; this.uris = uris; try { // strip leading / and add **/ prefix const processedPatterns = pattern .map((p) => { let patternString = p; if (patternString[0] === PATH_SEP) { patternString = patternString.substring(1); } return '**/' + patternString; }) .filter((p) => p.length > 0); this.isMatch = (0, picomatch_1.default)(processedPatterns, { bash: true, noglobstar: false, }); if (folderUri) { folderUri = normalizeResourceForMatching(folderUri); if (!folderUri.endsWith(PATH_SEP)) { folderUri = folderUri + PATH_SEP; } this.folderUri = folderUri; } } catch { this.isMatch = () => false; this.uris = []; } } matchesPattern(fileName) { if (this.folderUri && !fileName.startsWith(this.folderUri)) { return false; } return this.isMatch(fileName); } getURIs() { return this.uris; } } exports.FilePatternAssociation = FilePatternAssociation; class SchemaHandle { constructor(service, uri, unresolvedSchemaContent) { this.service = service; this.uri = uri; this.dependencies = {}; this.anchors = undefined; if (unresolvedSchemaContent) { this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent)); } } getUnresolvedSchema() { if (!this.unresolvedSchema) { this.unresolvedSchema = this.service.loadSchema(this.uri); } return this.unresolvedSchema; } getResolvedSchema() { if (!this.resolvedSchema) { this.resolvedSchema = this.getUnresolvedSchema().then((unresolved) => this.service.resolveSchemaContent(unresolved, this.uri, this.dependencies)); } return this.resolvedSchema; } clearSchema() { const hasChanges = !!this.unresolvedSchema; this.resolvedSchema = undefined; this.unresolvedSchema = undefined; this.dependencies = {}; this.anchors = undefined; return hasChanges; } } exports.SchemaHandle = SchemaHandle; class UnresolvedSchema { constructor(schema, errors = []) { this.schema = schema; this.errors = errors; } } exports.UnresolvedSchema = UnresolvedSchema; function toDiagnostic(message, code, relatedURL) { const relatedInformation = relatedURL ? [ { location: { uri: relatedURL, range: vscode_languageserver_types_1.Range.create(0, 0, 0, 0) }, message, }, ] : undefined; return { message, code, relatedInformation }; } class ResolvedSchema { constructor(schema, errors = [], warnings = [], schemaDraft) { this.schema = schema; this.errors = errors; this.warnings = warnings; this.schemaDraft = schemaDraft; } getSection(path) { const schemaRef = this.getSectionRecursive(path, this.schema); if (schemaRef) { return (0, baseValidator_1.asSchema)(schemaRef); } return undefined; } getSectionRecursive(path, schema) { if (!schema || typeof schema === 'boolean' || path.length === 0) { return schema; } const next = path.shift(); if (schema.properties && typeof schema.properties[next]) { return this.getSectionRecursive(path, schema.properties[next]); } else if (schema.patternProperties) { for (const pattern of Object.keys(schema.patternProperties)) { const regex = Strings.extendedRegExp(pattern); if (regex?.test(next)) { return this.getSectionRecursive(path, schema.patternProperties[pattern]); } } } else if (typeof schema.additionalProperties === 'object') { return this.getSectionRecursive(path, schema.additionalProperties); } else if (next.match('[0-9]+')) { if (Array.isArray(schema.items)) { const index = parseInt(next, 10); if (!isNaN(index) && schema.items[index]) { return this.getSectionRecursive(path, schema.items[index]); } } else if (schema.items) { return this.getSectionRecursive(path, schema.items); } } return undefined; } } exports.ResolvedSchema = ResolvedSchema; class YAMLSchemaService { constructor(requestService, contextService, promiseConstructor, yamlSettings) { this.schemaUriToNameAndDescription = new Map(); this.contextService = contextService; this.requestService = requestService; this.promiseConstructor = promiseConstructor || Promise; this.callOnDispose = []; this.contributionSchemas = {}; this.contributionAssociations = []; this.schemasById = {}; this.filePatternAssociations = []; this.registeredSchemasIds = {}; this.customSchemaProvider = undefined; this.schemaPriorityMapping = new Map(); this.yamlSettings = yamlSettings; } registerCustomSchemaProvider(customSchemaProvider) { this.customSchemaProvider = customSchemaProvider; } getAllSchemas() { const result = []; const schemaUris = new Set(); for (const filePattern of this.filePatternAssociations) { const schemaUri = filePattern.uris[0]; if (schemaUri === schemaUrls_1.EMPTY_SCHEMA_URL || schemaUris.has(schemaUri)) { continue; } schemaUris.add(schemaUri); const schemaHandle = { uri: schemaUri, fromStore: false, usedForCurrentFile: false, }; if (this.schemaUriToNameAndDescription.has(schemaUri)) { const { name, description, versions } = this.schemaUriToNameAndDescription.get(schemaUri); schemaHandle.name = name; schemaHandle.description = description; schemaHandle.fromStore = true; schemaHandle.versions = versions; } result.push(schemaHandle); } return result; } collectSchemaNodes(push, ...values) { const collect = (value) => { if (!value || typeof value !== 'object') return; if (Array.isArray(value)) { for (const entry of value) { collect(entry); } return; } push(value); }; for (const value of values) { collect(value); } } schemaMapValues(map) { if (!map || typeof map !== 'object') return undefined; return Object.values(map); } resolveSchemaRef(resource, schemaRef) { if (!schemaRef.startsWith('file:') && !schemaRef.startsWith('http')) { // If path contains a fragment and it is left intact, "#" will be // considered part of the filename and converted to "%23" by // path.resolve() -> take it out and add back after path.resolve let appendix = ''; if (schemaRef.indexOf('#') > 0) { const segments = schemaRef.split('#', 2); schemaRef = segments[0]; appendix = segments[1]; } if (!path.isAbsolute(schemaRef)) { const resUri = vscode_uri_1.URI.parse(resource); schemaRef = vscode_uri_1.URI.file(path.resolve(path.parse(resUri.fsPath).dir, schemaRef)).toString(); } else { schemaRef = vscode_uri_1.URI.file(schemaRef).toString(); } if (appendix.length > 0) { schemaRef += '#' + appendix; } } return schemaRef; } resolveModelineSchema(resource, doc) { const schemaFromModeline = (0, modelineUtil_1.getSchemaFromModeline)(doc); if (schemaFromModeline !== undefined) { if (schemaFromModeline.trim().toLowerCase() === 'none') { return 'none'; } return this.resolveSchemaRef(resource, schemaFromModeline); } } resolveDollarSchema(resource, doc) { const dollarSchema = (0, dollarUtils_1.getDollarSchema)(doc); if (dollarSchema !== undefined) { return this.resolveSchemaRef(resource, dollarSchema); } } async getSchemaIdsForResource(resource, doc) { const modelineSchema = this.resolveModelineSchema(resource, doc); if (modelineSchema) { if (modelineSchema === 'none') { return []; } return [modelineSchema]; } const dollarSchema = this.resolveDollarSchema(resource, doc); if (dollarSchema) { return [dollarSchema]; } if (this.customSchemaProvider) { try { const schemaUri = await this.customSchemaProvider(resource); if (Array.isArray(schemaUri)) { if (schemaUri.length > 0) { return schemaUri; } } else if (schemaUri) { return [schemaUri]; } } catch { // Fall back to configured schemas } } const seen = Object.create(null); const schemas = []; for (const entry of this.filePatternAssociations) { if (entry.matchesPattern(resource)) { for (const schemaId of entry.getURIs()) { if (!seen[schemaId]) { schemas.push(schemaId); seen[schemaId] = true; } } } } return schemas.length > 0 ? this.highestPrioritySchemas(schemas) : []; } async getSchemaDescriptionsForResource(resource, doc) { const schemaIds = await this.getSchemaIdsForResource(resource, doc); const result = []; for (const schemaId of schemaIds) { if (schemaId === schemaUrls_1.EMPTY_SCHEMA_URL) { continue; } const metadata = this.schemaUriToNameAndDescription.get(schemaId); let schema; if (!metadata) { try { const unresolvedSchema = await this.loadSchema(schemaId); if (unresolvedSchema.schema && typeof unresolvedSchema.schema === 'object') { schema = unresolvedSchema.schema; } } catch { // Keep the schema URI visible even when its content cannot be loaded. } } result.push({ uri: schemaId, name: metadata?.name ?? schema?.title, description: metadata?.description ?? schema?.description, versions: metadata?.versions ?? schema?.versions, }); } return result; } async resolveSchemaContent(schemaToResolve, schemaURL, dependencies) { const resolveErrors = schemaToResolve.errors.slice(0); const loc = toDisplayString(schemaURL); const raw = schemaToResolve.schema; if (raw === null || Array.isArray(raw) || (typeof raw !== 'object' && typeof raw !== 'boolean')) { const got = raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw; resolveErrors.push(toDiagnostic(l10n.t("Schema '{0}' is not valid: {1}", loc, `expected a JSON Schema object or boolean, got "${got}".`), jsonLanguageTypes_1.ErrorCode.SchemaResolveError, schemaURL)); return new ResolvedSchema({}, resolveErrors); } const _setSourceUri = (node, sourceUri) => { if (!node || typeof node !== 'object' || !sourceUri) return; node._sourceUri = sourceUri; }; const _cloneSchema = (value, seen, stopCondition) => { // primitives and null if (value === null || typeof value !== 'object') return value; if (stopCondition) { const replacement = stopCondition(value, seen.size); if (replacement !== undefined) return replacement; } // already cloned if (seen.has(value)) return seen.get(value); // clone arrays if (Array.isArray(value)) { const arr = []; seen.set(value, arr); for (const item of value) { arr.push(_cloneSchema(item, seen, stopCondition)); } return arr; } // clone objects const result = {}; seen.set(value, result); for (const prop in value) { result[prop] = _cloneSchema(value[prop], seen, stopCondition); } _setSourceUri(result, value._sourceUri); return result; }; /** * ---------------------------- * Meta-validate a schema node against its dialect's meta-schema * ---------------------------- */ const _loadSchema = this.loadSchema.bind(this); const ajvErrorLocale = this.yamlSettings?.locale; async function _metaValidateSchemaNode(node, hasNestedSchema) { if (!node || typeof node !== 'object') return; const schemaDraft = await pickSchemaDraft(node.$schema, _loadSchema); if (schemaDraft) { node._schemaDraft = schemaDraft; } const validator = pickMetaValidator(schemaDraft); if (!validator) return; let toValidate = node; if (hasNestedSchema) { // clone for meta-validation: stop at dialect boundaries abd replace with {} const stopAtDialectBoundary = (val, seenSize) => { if (seenSize !== 0 && val && typeof val === 'object' && val.$schema) return {}; return undefined; }; toValidate = _cloneSchema(node, new Map(), stopAtDialectBoundary); } let valid = false; try { valid = validator(toValidate); } catch (e) { // AJV overflows on recursive/cyclic schemas; attempt to degrade gracefully console.warn(l10n.t("Schema '{0}' could not be fully validated: {1}", loc, e.message)); return; } if (!valid) { localizeAjvErrors(validator.errors, ajvErrorLocale); const errs = []; for (const err of validator.errors) { errs.push(`${err.instancePath} : ${err.message}`); } resolveErrors.push(toDiagnostic(l10n.t("Schema '{0}' is not valid: {1}", loc, `\n${errs.join('\n')}`), jsonLanguageTypes_1.ErrorCode.SchemaResolveError, schemaURL)); } } const resourceIndexByUri = new Map(); const _getResourceIndex = (resourceUri) => { let entry = resourceIndexByUri.get(resourceUri); if (!entry) { entry = { fragments: new Map() }; resourceIndexByUri.set(resourceUri, entry); } return entry; }; /** * Adds a resource's dynamic anchors to the inherited scope from parent resources * * Draft 2020-12: For $dynamicRef resolution, when schema A references schema B, * B's dynamic anchors are added to A's scope. This builds a chain where $dynamicRef * looks for the outermost (first) matching anchor. */ const _addResourceDynamicAnchors = (scope, resourceUri) => { const entry = resourceIndexByUri.get(resourceUri); if (!entry || entry.fragments.size === 0) return scope; let result = scope; for (const [name, entryItem] of entry.fragments) { if (!entryItem.dynamic) continue; const current = result?.get(name) ?? []; if (current.some((existing) => existing._baseUri === resourceUri)) continue; // clone map on first modification if (result === scope) result = scope ? new Map(scope) : new Map(); result.set(name, current.concat(entryItem.node)); } return result; }; // resolve relative URI against base URI // e.g. resolve "./foo.json" against "http://example.com/bar.json" => "http://example.com/foo.json" const _resolveAgainstBase = (baseUri, ref) => { if (this.contextService) return this.contextService.resolveRelativePath(ref, baseUri); return normalizeId(ref); }; const _indexSchemaResources = async (root, initialBaseUri) => { const preOrderStack = [{ node: root, baseUri: initialBaseUri, sourceUri: initialBaseUri }]; const postOrderStack = []; const childListByNode = new WeakMap(); const seen = new Set(); while (preOrderStack.length) { const current = preOrderStack.pop(); if (!current) continue; const node = current.node; if (!node || typeof node !== 'object' || seen.has(node)) continue; seen.add(node); let baseUri = current.baseUri; _setSourceUri(node, current.sourceUri); const id = node.$id || node.id; if (id) { const resolvedBaseUri = _resolveAgainstBase(baseUri, id); node._baseUri = resolvedBaseUri; const hashIndex = resolvedBaseUri.indexOf('#'); if (hashIndex !== -1 && hashIndex < resolvedBaseUri.length - 1) { // Draft-07 and earlier: $id with fragment defines a plain-name anchor scoped to the resolved base const frag = resolvedBaseUri.slice(hashIndex + 1); _getResourceIndex(baseUri).fragments.set(frag, { node }); } else { // $id without fragment creates a new embedded resource scope baseUri = resolvedBaseUri; const entry = _getResourceIndex(resolvedBaseUri); if (!entry.root) { entry.root = node; } } } // Draft 2019-09+: $anchor keyword if (node.$anchor) { _getResourceIndex(baseUri).fragments.set(node.$anchor, { node }); } // Draft 2020-12+: $dynamicAnchor keyword if (node.$dynamicAnchor) { node._baseUri = baseUri; _getResourceIndex(baseUri).fragments.set(node.$dynamicAnchor, { node, dynamic: true }); } const children = []; childListByNode.set(node, children); // collect all child schemas this.collectSchemaNodes((entry) => { children.push(entry); preOrderStack.push({ node: entry, baseUri, sourceUri: current.sourceUri }); }, node.not, node.if, node.then, node.else, node.contains, node.propertyNames, node.additionalProperties, node.items, node.additionalItems, node.prefixItems, this.schemaMapValues(node.properties), this.schemaMapValues(node.patternProperties), this.schemaMapValues(node.definitions), this.schemaMapValues(node.$defs), this.schemaMapValues(node.dependentSchemas), this.schemaMapValues(node.dependencies), node.allOf, node.anyOf, node.oneOf, node.schemaSequence); postOrderStack.push(node); } const hasNestedSchema = new WeakMap(); while (postOrderStack.length) { const node = postOrderStack.pop(); let hasNested = false; for (const child of childListByNode.get(node)) { if (child.$schema || hasNestedSchema.get(child)) { hasNested = true; break; } } hasNestedSchema.set(node, hasNested); if (node === root || node.$schema) await _metaValidateSchemaNode(node, hasNested); } }; let schema = raw; const schemaBaseURL = schemaToResolve.uri ?? schemaURL; await _indexSchemaResources(schema, schemaBaseURL); const _findSection = (schemaRoot, refPath, sourceURI) => { if (!refPath) { return schemaRoot; } // JSON pointer style if (refPath[0] === PATH_SEP) { let current = schemaRoot; const parts = refPath.substring(1).split(PATH_SEP); for (const part of parts) { // in JSON Pointer: ~ must be escaped as ~0, / must be escaped as ~1 current = current?.[part.replace(/~1/g, PATH_SEP).replace(/~0/g, '~')]; if (current === null) return undefined; } return current; } // plain-name fragment ($anchor or $id#fragment) -> lookup in collected fragments return _getResourceIndex(sourceURI).fragments.get(refPath).node; }; const _merge = (target, sourceRoot, sourceURI, refPath, clone = false) => { const section = _findSection(sourceRoot, refPath, sourceURI); if (typeof section === 'boolean') { if (!section) target.not = {}; return; } if (typeof section === 'object' && section) { const source = clone ? _cloneSchema(section, new Map()) : section; for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key) && !Object.prototype.hasOwnProperty.call(target, key)) { target[key] = source[key]; } } _setSourceUri(target, source._sourceUri); return; } else { resolveErrors.push(toDiagnostic(l10n.t("$ref '{0}' in '{1}' cannot be resolved.", refPath, sourceURI), jsonLanguageTypes_1.ErrorCode.SchemaResolveError, sourceURI)); } }; const _resolveRefUri = (parentSchemaURL, refUri) => { const resolvedAgainstParent = _resolveAgainstBase(parentSchemaURL, refUri); if (!refUri.startsWith(PATH_SEP)) return resolvedAgainstParent; const parentResource = resourceIndexByUri.get(parentSchemaURL)?.root; const parentResourceId = parentResource?.$id || parentResource?.id; if (!parentResourceId) return resolvedAgainstParent; const resolvedParentId = _resolveAgainstBase(parentSchemaURL, parentResourceId); if (!resolvedParentId.startsWith('http://') && !resolvedParentId.startsWith('https://')) return resolvedAgainstParent; return _resolveAgainstBase(resolvedParentId, refUri); }; const _resolveLocalSiblingFromRemoteUri = (parentSchemaURL, resolvedRefUri) => { try { const parentUri = vscode_uri_1.URI.parse(parentSchemaURL); const targetUri = vscode_uri_1.URI.parse(resolvedRefUri); if (parentUri.scheme !== 'file') return undefined; if (targetUri.scheme !== 'http' && targetUri.scheme !== 'https') return undefined; const localFileName = path.posix.basename(targetUri.path); if (!localFileName) return undefined; const localDir = path.posix.dirname(parentUri.path); const localPath = path.posix.join(localDir, localFileName); return parentUri.with({ path: localPath, query: targetUri.query, fragment: targetUri.fragment }).toString(); } catch { return undefined; } }; const resolveExternalLink = (node, uri, linkPath, parentSchemaURL, parentSchemaSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { const _attachResolvedSchema = (node, schemaRoot, schemaUri, linkPath, parentSchemaDependencies, resolveRefDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { parentSchemaDependencies[schemaUri] = true; _merge(node, schemaRoot, schemaUri, linkPath, !!inheritedDynamicScope || !!recursiveAnchorBase); if (!recursiveAnchorBase || !node._baseUri) node._baseUri = schemaUri; node.url = schemaUri; const nextStack = new Set(resolutionStack); nextStack.add(schemaUri); // eslint-disable-next-line @typescript-eslint/no-use-before-define return resolveRefs(node, schemaRoot, schemaUri, resolveRefDependencies, nextStack, recursiveAnchorBase, inheritedDynamicScope); }; const _resolveByUri = async (targetUris, index = 0) => { const targetUri = targetUris[index]; const embeddedSchema = resourceIndexByUri.get(targetUri)?.root; if (embeddedSchema) { return _attachResolvedSchema(node, embeddedSchema, targetUri, linkPath, parentSchemaDependencies, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope); } const referencedHandle = this.getOrAddSchemaHandle(targetUri); const unresolvedSchema = await Promise.resolve(referencedHandle.getUnresolvedSchema()); if (unresolvedSchema.errors?.some((error) => error.message.toLowerCase().includes('unable to load schema from')) && index + 1 < targetUris.length) { return _resolveByUri(targetUris, index + 1); } if (unresolvedSchema.errors.length) { const schemaError = unresolvedSchema.errors[0]; const loc = linkPath ? targetUri + '#' + linkPath : targetUri; resolveErrors.push(toDiagnostic(l10n.t("Problems loading reference '{0}': {1}", loc, schemaError.message), schemaError.code, targetUri)); } // index resources for the newly loaded schema await _indexSchemaResources(unresolvedSchema.schema, targetUri); return await _attachResolvedSchema(node, unresolvedSchema.schema, targetUri, linkPath, parentSchemaDependencies, referencedHandle.dependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope); }; const resolvedUri = _resolveRefUri(parentSchemaURL, uri); const embeddedTarget = resourceIndexByUri.get(resolvedUri)?.root; const localSiblingUri = embeddedTarget ? undefined : _resolveLocalSiblingFromRemoteUri(parentSchemaSourceUri, resolvedUri); const targetUris = localSiblingUri && localSiblingUri !== resolvedUri ? [localSiblingUri, resolvedUri] : [resolvedUri]; return _resolveByUri(targetUris); }; const resolveRefs = async (node, parentSchema, parentSchemaURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { if (!node || typeof node !== 'object') { return null; } const toWalk = [ { node, baseUri: parentSchemaURL, sourceUri: node._sourceUri ?? parentSchemaURL, recursiveAnchorBase, inheritedDynamicScope, }, ]; const seen = new WeakSet(); // prevents re-walking the same schema object graph // eslint-disable-next-line @typescript-eslint/no-explicit-any const openPromises = []; // handle $ref with siblings based on dialect const _handleRef = (next, nodeBaseUri, nodeSourceUri, nodeSchemaDraft, recursiveAnchorBase, inheritedDynamicScope, siblingRefCycleKeys) => { const currentDynamicScope = _addResourceDynamicAnchors(inheritedDynamicScope, nodeBaseUri); this.collectSchemaNodes((entry) => toWalk.push({ node: entry, baseUri: nodeBaseUri, sourceUri: nodeSourceUri, recursiveAnchorBase, inheritedDynamicScope: currentDynamicScope, }), this.schemaMapValues(next.definitions || next.$defs)); // checks if a node with $ref has other constraint keywords const _hasRefSiblings = (node) => { for (const k of Object.keys(node)) { if (REF_SIBLING_NONCONSTRAINT_KEYS.has(k)) continue; return true; } return false; }; /** * For Draft-2019+: * { $ref: "...", <siblings...> } * becomes * { allOf: [ { $ref: "..." }, <siblings...> ] } */ const _rewriteRefWithSiblingsToAllOf = (node) => { const siblings = {}; for (const k of Object.keys(node)) { if (!REF_SIBLING_NONCONSTRAINT_KEYS.has(k)) { siblings[k] = node[k]; delete node[k]; } } const refValue = node.$dynamicRef ?? node.$recursiveRef ?? node.$ref; if (typeof refValue !== 'string') return; node.allOf = [ { [node.$dynamicRef ? '$dynamicRef' : node.$recursiveRef ? '$recursiveRef' : '$ref']: refValue }, siblings, ]; delete node.$dynamicRef; delete node.$recursiveRef; delete node.$ref; }; const _stripRefSiblings = (node) => { for (const k of Object.keys(node)) { if (!REF_SIBLING_NONCONSTRAINT_KEYS.has(k)) delete node[k]; } }; const seenRefs = new Set(); const _mergeIfResourceAlreadyInResolutionStack = (ref, resolvedResource, frag) => { if (!resolutionStack.has(resolvedResource)) return false; if (!seenRefs.has(ref)) { const source = resourceIndexByUri.get(resolvedResource)?.root; if (source && typeof source === 'object') { _merge(next, source, resolvedResource, frag, !!recursiveAnchorBase); } seenRefs.add(ref); } return true; }; while (next.$dynamicRef || next.$recursiveRef || next.$ref) { const isDynamicRef = typeof next.$dynamicRef === 'string'; const isRecursiveRef = !isDynamicRef && typeof next.$recursiveRef === 'string'; const rawRef = next.$dynamicRef ?? next.$recursiveRef ?? next.$ref; if (typeof rawRef !== 'string') break; next._$ref = rawRef; // parse ref into base URI and fragment const ref = decodeURIComponent(rawRef); const segments = ref.split('#', 2); const baseUri = segments[0]; const frag = segments.length > 1 ? segments[1] : ''; const resolvedRefKey = `${baseUri ? _resolveAgainstBase(nodeBaseUri, baseUri) : nodeBaseUri}#${frag}`; if (_hasRefSiblings(next)) { // Draft-07 and earlier: ignore siblings if (nodeSchemaDraft === jsonLanguageTypes_1.SchemaDraft.v4 || nodeSchemaDraft === jsonLanguageTypes_1.SchemaDraft.v7) { _stripRefSiblings(next); } else { if (siblingRefCycleKeys?.has(resolvedRefKey)) break; // Draft-2019+: support sibling keywords _rewriteRefWithSiblingsToAllOf(next); if (Array.isArray(next.allOf)) { for (let i = 0; i < next.allOf.length; i++) { const entry = next.allOf[i]; if (entry && typeof entry === 'object') { let nextSiblingRefCycleKeys; if (i === 0) { nextSiblingRefCycleKeys = new Set(siblingRefCycleKeys); nextSiblingRefCycleKeys.add(resolvedRefKey); } toWalk.push({ node: entry, baseUri: nodeBaseUri, sourceUri: nodeSourceUri, recursiveAnchorBase, inheritedDynamicScope: currentDynamicScope, siblingRefCycleKeys: nextSiblingRefCycleKeys, }); } } } return; } } delete next.$dynamicRef; delete next.$recursiveRef; delete next.$ref; // Draft-2019+: $recursiveRef if (isRecursiveRef && (ref === '#' || ref === '')) { const targetRoot = resourceIndexByUri.get(nodeBaseUri)?.root; const recursiveBase = targetRoot?.$recursiveAnchor && recursiveAnchorBase ? recursiveAnchorBase : nodeBaseUri; if (recursiveBase.length > 0) { if (resolutionStack?.has(recursiveBase) || recursiveBase === nodeBaseUri) { const sourceRoot = resourceIndexByUri.get(recursiveBase)?.root ?? parentSchema; if (!seenRefs.has(ref)) { _merge(next, sourceRoot, recursiveBase, '', false); seenRefs.add(ref); } continue; } openPromises.push(resolveExternalLink(next, recursiveBase, '', nodeBaseUri, nodeSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope)); return; } continue; } // Draft-2020+: $dynamicRef else if (isDynamicRef) { const targetResource = baseUri.length > 0 ? _resolveAgainstBase(nodeBaseUri, baseUri) : nodeBaseUri; const targetFragments = resourceIndexByUri.get(targetResource)?.fragments; const targetHasDynamicAnchor = frag.length > 0 && targetFragments?.get(frag)?.dynamic; const dynamicTarget = targetHasDynamicAnchor ? currentDynamicScope?.get(frag)?.[0] : undefined; const resolveResource = dynamicTarget ? dynamicTarget._baseUri : targetResource; if (dynamicTarget && (resolveResource === nodeBaseUri || resolutionStack.has(resolveResource))) { if (!seenRefs.has(ref)) { _merge(next, dynamicTarget, resolveResource, '', false); seenRefs.add(ref); } continue; } if (baseUri.length > 0 || targetHasDynamicAnchor) { if (_mergeIfResourceAlreadyInResolutionStack(ref, resolveResource, frag)) continue; openPromises.push(resolveExternalLink(next, resolveResource, frag, nodeBaseUri, nodeSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope)); return; } } // normal $ref with external baseUri else if (baseUri.length > 0) { const resolvedBaseUri = _resolveAgainstBase(nodeBaseUri, baseUri); if (_mergeIfResourceAlreadyInResolutionStack(ref, resolvedBaseUri, frag)) continue; // resolve relative to this node's base URL openPromises.push(resolveExternalLink(next, baseUri, frag, nodeBaseUri, nodeSourceUri, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope)); return; } // local $ref or $dynamicRef if (!seenRefs.has(ref)) { _merge(next, parentSchema, nodeBaseUri, frag, isDynamicRef && !!currentDynamicScope); seenRefs.add(ref); } } // recursively process children this.collectSchemaNodes((entry) => toWalk.push({ node: entry, baseUri: next._baseUri || nodeBaseUri, sourceUri: next._sourceUri || nodeSourceUri, schemaDraft: nodeSchemaDraft, recursiveAnchorBase, inheritedDynamicScope: currentDynamicScope, }), next.not, next.if, next.then, next.else, next.contains, next.propertyNames, next.additionalProperties, next.items, next.additionalItems, next.prefixItems, this.schemaMapValues(next.properties), this.schemaMapValues(next.patternProperties), this.schemaMapValues(next.dependentSchemas), this.schemaMapValues(next.dependencies), next.allOf, next.anyOf, next.oneOf, next.schemaSequence); }; // handle file path with fragments if (parentSchemaURL.indexOf('#') > 0) { const segments = parentSchemaURL.split('#', 2); if (segments[0].length > 0 && segments[1].length > 0) { const newSchema = {}; await resolveExternalLink(newSchema, segments[0], segments[1], parentSchemaURL, schema._sourceUri ?? parentSchemaURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope);