yaml-language-server
Version:
1,029 lines (1,028 loc) • 74.5 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* 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 };
};
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);
for (const key in schema) {
if (key === 'required') {
continue;
}
if (Object.prototype.hasOwnProperty.call(schema, key) && !Object.prototype.hasOwnProperty.call(newSchema, key)) {
newSchema[key] = schema[key];
}
}
schema = newSchema;
}
}
while (toWalk.length) {
const item = toWalk.pop();
const next = item.node;
const nodeBaseUri = next._baseUri || item.baseUri;
const nodeSourceUri = next._sourceUri || nodeBaseUri;
const nodeSchemaDraft = next._schemaDraft || item.schemaDraft;
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseUri : undefined);
if (seen.has(next))
continue;
seen.add(next);
_handleRef(next, nodeBaseUri, nodeSourceUri, nodeSchemaDraft, nodeRecursiveAnchorBase, item.inheritedDynamicScope, item.siblingRefCycleKeys);
}
return Promise.all(openPromises);
};
const resolutionStack = new Set(); // prevents $ref/$recursiveRef/$dynamicRef loops across schema URIs
const rootResource = schema._baseUri || schemaURL;
if (rootResource)
resolutionStack.add(rootResource);
await resolveRefs(schema, schema, schemaURL, dependencies, resolutionStack);
return new ResolvedSchema(schema, resolveErrors);
}
async getSchemaForResource(resource, doc) {
const resolveSchemaForResource = async (schemas) => {
const schemaHandle = this.createCombinedSchema(resource, schemas);
const schema = await Promise.resolve(schemaHandle.getResolvedSchema());
return this.finalizeResolvedSchema(schema, schemaHandle.uri, doc, false);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveSchema = async () => {
const seen = Object.create(null);
const schemas = [];
let k8sAllSchema = undefined;
let k8sSchemaUrl = undefined;
for (const entry of this.filePatternAssociations) {
if (entry.matchesPattern(resource)) {
for (const schemaId of entry.getURIs()) {
if (!seen[schemaId]) {
if (this.yamlSettings?.kubernetesCRDStoreEnabled && (0, schemaUrls_1.isKubernetes)(schemaId)) {
if (!k8sAllSchema) {
k8sSchemaUrl = schemaId;
k8sAllSchema = await this.getResolvedSchema(schemaId);
}
const kubeSchema = (0, k8sSchemaUtil_1.autoDetectKubernetesSchema)(doc, k8sAllSchema, k8sSchemaUrl ?? schemaId, this.yamlSettings.kubernetesCRDStoreUrl ?? schemaUrls_1.CRD_CATALOG_URL);
if (kubeSchema) {
schemas.push(kubeSchema);
seen[schemaId] = true;
}
else {
schemas.push(schemaId);
seen[schemaId] = true;
}
}
else {
schemas.push(schemaId);
seen[schemaId] = true;
}
}
}
}
}
if (schemas.length > 0) {
// Join all schemas with the highest priority.
const highestPrioSchemas = this.highestPrioritySchemas(schemas);
return resolveSchemaForResource(highestPrioSchemas);
}
return Promise.resolve(null);
};
const modelineSchema = this.resolveModelineSchema(resource, doc);
if (modelineSchema) {
if (modelineSchema === 'none') {
return Promise.resolve(null);
}