Files
chesco/node_modules/yaml-language-server/lib/umd/languageservice/services/yamlSchemaService.js
T

1039 lines
53 KiB
JavaScript

/*---------------------------------------------------------------------------------------------
* 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.
*--------------------------------------------------------------------------------------------*/
(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", "../jsonSchema", "../yamlLanguageService", "vscode-json-languageservice/lib/umd/services/jsonSchemaService", "vscode-uri", "@vscode/l10n", "path", "./modelineUtil", "yaml", "jsonc-parser", "ajv", "ajv-draft-04", "ajv/dist/2019", "ajv/dist/2020", "./crdUtil", "../utils/schemaUrls"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YAMLSchemaService = exports.MODIFICATION_ACTIONS = void 0;
const jsonSchema_1 = require("../jsonSchema");
const yamlLanguageService_1 = require("../yamlLanguageService");
const jsonSchemaService_1 = require("vscode-json-languageservice/lib/umd/services/jsonSchemaService");
const vscode_uri_1 = require("vscode-uri");
const l10n = require("@vscode/l10n");
const path = require("path");
const modelineUtil_1 = require("./modelineUtil");
const yaml_1 = require("yaml");
const Json = require("jsonc-parser");
const ajv_1 = require("ajv");
const ajv_draft_04_1 = require("ajv-draft-04");
const _2019_1 = require("ajv/dist/2019");
const _2020_1 = require("ajv/dist/2020");
const crdUtil_1 = require("./crdUtil");
const schemaUrls_1 = require("../utils/schemaUrls");
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 });
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema2019 = require('ajv/dist/refs/json-schema-2019-09/schema.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema2020 = require('ajv/dist/refs/json-schema-2020-12/schema.json');
const schema04Validator = ajv4.compile(jsonSchema04);
const schema07Validator = ajv7.compile(jsonSchema07);
const schema2019Validator = ajv2019.compile(jsonSchema2019);
const schema2020Validator = ajv2020.compile(jsonSchema2020);
const schemaDialectCache = new Map();
const schemaDialectInFlight = new Map();
// 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',
'_baseUrl',
'_dialect',
'$anchor',
'$dynamicAnchor',
'$dynamicRef',
'$recursiveAnchor',
'$recursiveRef',
'definitions',
'$defs',
'$comment',
'title',
'description',
'$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 || (exports.MODIFICATION_ACTIONS = {}));
class YAMLSchemaService extends jsonSchemaService_1.JSONSchemaService {
constructor(requestService, contextService, promiseConstructor, yamlSettings) {
super(requestService, contextService, promiseConstructor);
this.schemaUriToNameAndDescription = new Map();
this.customSchemaProvider = undefined;
this.requestService = requestService;
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 (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);
}
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(l10n.t("Schema '{0}' is not valid: {1}", loc, `expected a JSON Schema object or boolean, got "${got}".`));
return new jsonSchemaService_1.ResolvedSchema({}, resolveErrors);
}
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);
}
return result;
};
/**
* ----------------------------
* Meta-validate a schema node against its dialect's meta-schema
* ----------------------------
*/
const _loadSchema = this.loadSchema.bind(this);
async function _metaValidateSchemaNode(node, hasNestedSchema) {
if (!node || typeof node !== 'object')
return;
const dialect = await pickSchemaDialect(node.$schema, _loadSchema);
dialect && (node._dialect = dialect);
const validator = pickMetaValidator(dialect);
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);
}
if (!validator(toValidate)) {
const errs = [];
for (const err of validator.errors) {
errs.push(`${err.instancePath} : ${err.message}`);
}
resolveErrors.push(l10n.t("Schema '{0}' is not valid: {1}", loc, `\n${errs.join('\n')}`));
}
}
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._baseUrl === 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 this.normalizeId(ref);
};
const _indexSchemaResources = async (root, initialBaseUri) => {
const preOrderStack = [{ node: root, baseUri: 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;
const id = node.$id || node.id;
if (id) {
const normalizedId = _resolveAgainstBase(baseUri, id);
node._baseUrl = normalizedId;
const hashIndex = normalizedId.indexOf('#');
if (hashIndex !== -1 && hashIndex < normalizedId.length - 1) {
// Draft-07 and earlier: $id with fragment defines a plain-name anchor scoped to the resolved base
const frag = normalizedId.slice(hashIndex + 1);
_getResourceIndex(baseUri).fragments.set(frag, { node });
}
else {
// $id without fragment creates a new embedded resource scope
baseUri = normalizedId;
const entry = _getResourceIndex(normalizedId);
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._baseUrl = 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 });
}, 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;
await _indexSchemaResources(schema, schemaURL);
const _findSection = (schemaRoot, refPath, sourceURI) => {
if (!refPath) {
return schemaRoot;
}
// JSON pointer style
if (refPath[0] === '/') {
let current = schemaRoot;
const parts = refPath.substr(1).split('/');
for (const part of parts) {
// in JSON Pointer: ~ must be escaped as ~0, / must be escaped as ~1
current = current?.[part.replace(/~1/g, '/').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];
}
}
return;
}
else {
resolveErrors.push(l10n.t("$ref '{0}' in '{1}' cannot be resolved.", refPath, sourceURI));
}
};
const resolveExternalLink = (node, uri, linkPath, parentSchemaURL, 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._baseUrl)
node._baseUrl = 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 resolvedUri = _resolveAgainstBase(parentSchemaURL, uri);
const embeddedSchema = resourceIndexByUri.get(resolvedUri)?.root;
if (embeddedSchema) {
return _attachResolvedSchema(node, embeddedSchema, resolvedUri, linkPath, parentSchemaDependencies, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope);
}
const referencedHandle = this.getOrAddSchemaHandle(resolvedUri);
return referencedHandle.getUnresolvedSchema().then(async (unresolvedSchema) => {
if (unresolvedSchema.errors.length) {
const loc = linkPath ? resolvedUri + '#' + linkPath : resolvedUri;
resolveErrors.push(l10n.t("Problems loading reference '{0}': {1}", loc, unresolvedSchema.errors[0]));
}
// index resources for the newly loaded schema
await _indexSchemaResources(unresolvedSchema.schema, resolvedUri);
return _attachResolvedSchema(node, unresolvedSchema.schema, resolvedUri, linkPath, parentSchemaDependencies, referencedHandle.dependencies, resolutionStack, recursiveAnchorBase, inheritedDynamicScope);
});
};
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, baseURL: 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, nodeBaseURL, nodeDialect, recursiveAnchorBase, inheritedDynamicScope) => {
const currentDynamicScope = _addResourceDynamicAnchors(inheritedDynamicScope, nodeBaseURL);
this.collectSchemaNodes((entry) => toWalk.push({ node: entry, baseURL: nodeBaseURL, 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();
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;
if (_hasRefSiblings(next)) {
// Draft-07 and earlier: ignore siblings
if (nodeDialect === jsonSchema_1.SchemaDialect.draft04 || nodeDialect === jsonSchema_1.SchemaDialect.draft07) {
_stripRefSiblings(next);
}
else {
// Draft-2019+: support sibling keywords
_rewriteRefWithSiblingsToAllOf(next);
if (Array.isArray(next.allOf)) {
for (const entry of next.allOf) {
if (entry && typeof entry === 'object') {
toWalk.push({
node: entry,
baseURL: nodeBaseURL,
recursiveAnchorBase,
inheritedDynamicScope: currentDynamicScope,
});
}
}
}
return;
}
}
const ref = decodeURIComponent(rawRef);
delete next.$dynamicRef;
delete next.$recursiveRef;
delete next.$ref;
// parse ref into base URI and fragment
const segments = ref.split('#', 2);
const baseUri = segments[0];
const frag = segments.length > 1 ? segments[1] : '';
// Draft-2019+: $recursiveRef
if (isRecursiveRef && (ref === '#' || ref === '')) {
const targetRoot = resourceIndexByUri.get(nodeBaseURL)?.root;
const recursiveBase = targetRoot?.$recursiveAnchor && recursiveAnchorBase ? recursiveAnchorBase : nodeBaseURL;
if (recursiveBase.length > 0) {
if (resolutionStack?.has(recursiveBase) || recursiveBase === nodeBaseURL) {
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, '', nodeBaseURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope));
return;
}
continue;
}
// Draft-2020+: $dynamicRef
else if (isDynamicRef) {
const targetResource = baseUri.length > 0 ? _resolveAgainstBase(nodeBaseURL, baseUri) : nodeBaseURL;
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._baseUrl : targetResource;
if (dynamicTarget && (resolveResource === nodeBaseURL || resolutionStack.has(resolveResource))) {
if (!seenRefs.has(ref)) {
_merge(next, dynamicTarget, resolveResource, '', false);
seenRefs.add(ref);
}
continue;
}
if (baseUri.length > 0 || targetHasDynamicAnchor) {
openPromises.push(resolveExternalLink(next, resolveResource, frag, nodeBaseURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope));
return;
}
}
// normal $ref with external baseUri
else if (baseUri.length > 0) {
// resolve relative to this node's base URL
openPromises.push(resolveExternalLink(next, baseUri, frag, nodeBaseURL, parentSchemaDependencies, resolutionStack, recursiveAnchorBase, currentDynamicScope));
return;
}
// local $ref or $dynamicRef
if (!seenRefs.has(ref)) {
_merge(next, parentSchema, nodeBaseURL, frag, !!currentDynamicScope);
seenRefs.add(ref);
}
}
// recursively process children
this.collectSchemaNodes((entry) => toWalk.push({
node: entry,
baseURL: next._baseUrl || nodeBaseURL,
dialect: nodeDialect,
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, 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 nodeBaseURL = next._baseUrl || item.baseURL;
const nodeDialect = next._dialect || item.dialect;
const nodeRecursiveAnchorBase = item.recursiveAnchorBase ?? (next.$recursiveAnchor ? nodeBaseURL : undefined);
if (seen.has(next))
continue;
seen.add(next);
_handleRef(next, nodeBaseURL, nodeDialect, nodeRecursiveAnchorBase, item.inheritedDynamicScope);
}
return Promise.all(openPromises);
};
const resolutionStack = new Set(); // prevents $ref/$recursiveRef/$dynamicRef loops across schema URIs
const rootResource = schema._baseUrl || schemaURL;
if (rootResource)
resolutionStack.add(rootResource);
await resolveRefs(schema, schema, schemaURL, dependencies, resolutionStack);
return new jsonSchemaService_1.ResolvedSchema(schema, resolveErrors);
}
getSchemaForResource(resource, doc) {
const resolveModelineSchema = () => {
let schemaFromModeline = (0, modelineUtil_1.getSchemaFromModeline)(doc);
if (schemaFromModeline !== undefined) {
if (!schemaFromModeline.startsWith('file:') && !schemaFromModeline.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 (schemaFromModeline.indexOf('#') > 0) {
const segments = schemaFromModeline.split('#', 2);
schemaFromModeline = segments[0];
appendix = segments[1];
}
if (!path.isAbsolute(schemaFromModeline)) {
const resUri = vscode_uri_1.URI.parse(resource);
schemaFromModeline = vscode_uri_1.URI.file(path.resolve(path.parse(resUri.fsPath).dir, schemaFromModeline)).toString();
}
else {
schemaFromModeline = vscode_uri_1.URI.file(schemaFromModeline).toString();
}
if (appendix.length > 0) {
schemaFromModeline += '#' + appendix;
}
}
return schemaFromModeline;
}
};
const resolveSchemaForResource = (schemas) => {
const schemaHandle = super.createCombinedSchema(resource, schemas);
return schemaHandle.getResolvedSchema().then((schema) => {
return this.finalizeResolvedSchema(schema, schemaHandle.url, doc, false);
});
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveSchema = async () => {
const seen = Object.create(null);
const schemas = [];
let k8sAllSchema = undefined;
for (const entry of this.filePatternAssociations) {
if (entry.matchesPattern(resource)) {
for (const schemaId of entry.getURIs()) {
if (!seen[schemaId]) {
if (this.yamlSettings?.kubernetesCRDStoreEnabled && schemaId === schemaUrls_1.KUBERNETES_SCHEMA_URL) {
if (!k8sAllSchema) {
k8sAllSchema = await this.getResolvedSchema(schemaUrls_1.KUBERNETES_SCHEMA_URL);
}
const kubeSchema = (0, crdUtil_1.autoDetectKubernetesSchemaFromDocument)(doc, this.yamlSettings.kubernetesCRDStoreUrl ?? schemaUrls_1.CRD_CATALOG_URL, k8sAllSchema);
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 = resolveModelineSchema();
if (modelineSchema) {
return resolveSchemaForResource([modelineSchema]);
}
if (this.customSchemaProvider) {
return this.customSchemaProvider(resource)
.then((schemaUri) => {
if (Array.isArray(schemaUri)) {
if (schemaUri.length === 0) {
return resolveSchema();
}
return Promise.all(schemaUri.map((schemaUri) => {
return this.resolveCustomSchema(schemaUri, doc);
})).then((schemas) => {
return {
errors: [],
schema: {
allOf: schemas.map((schemaObj) => {
return schemaObj.schema;
}),
},
};
}, () => {
return resolveSchema();
});
}
if (!schemaUri) {
return resolveSchema();
}
return this.resolveCustomSchema(schemaUri, doc);
})
.then((schema) => {
return schema;
}, () => {
return resolveSchema();
});
}
return resolveSchema();
}
finalizeResolvedSchema(schema, schemaUrl, doc, includeErrorsForSequence) {
if (schema.schema && typeof schema.schema === 'object') {
schema.schema.url = schemaUrl;
if (schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex]) {
const selectedSchema = schema.schema.schemaSequence[doc.currentDocIndex];
if (includeErrorsForSequence) {
return new jsonSchemaService_1.ResolvedSchema(selectedSchema, schema.errors);
}
return new jsonSchemaService_1.ResolvedSchema(selectedSchema);
}
}
return schema;
}
// Set the priority of a schema in the schema service
addSchemaPriority(uri, priority) {
let currSchemaArray = this.schemaPriorityMapping.get(uri);
if (currSchemaArray) {
currSchemaArray = currSchemaArray.add(priority);
this.schemaPriorityMapping.set(uri, currSchemaArray);
}
else {
this.schemaPriorityMapping.set(uri, new Set().add(priority));
}
}
/**
* Search through all the schemas and find the ones with the highest priority
*/
highestPrioritySchemas(schemas) {
let highestPrio = 0;
const priorityMapping = new Map();
schemas.forEach((schema) => {
// If the schema does not have a priority then give it a default one of [0]
const priority = this.schemaPriorityMapping.get(schema) || [0];
priority.forEach((prio) => {
if (prio > highestPrio) {
highestPrio = prio;
}
// Build up a mapping of priority to schemas so that we can easily get the highest priority schemas easier
let currPriorityArray = priorityMapping.get(prio);
if (currPriorityArray) {
currPriorityArray = currPriorityArray.concat(schema);
priorityMapping.set(prio, currPriorityArray);
}
else {
priorityMapping.set(prio, [schema]);
}
});
});
return priorityMapping.get(highestPrio) || [];
}
async resolveCustomSchema(schemaUri, doc) {
const unresolvedSchema = await this.loadSchema(schemaUri);
const schema = await this.resolveSchemaContent(unresolvedSchema, schemaUri, []);
return this.finalizeResolvedSchema(schema, schemaUri, doc, true);
}
/**
* Save a schema with schema ID and schema content.
* Overrides previous schemas set for that schema ID.
*/
async saveSchema(schemaId, schemaContent) {
const id = this.normalizeId(schemaId);
this.getOrAddSchemaHandle(id, schemaContent);
this.schemaPriorityMapping.set(id, new Set().add(yamlLanguageService_1.SchemaPriority.Settings));
return Promise.resolve(undefined);
}
/**
* Delete schemas on specific path
*/
async deleteSchemas(deletions) {
deletions.schemas.forEach((s) => {
this.deleteSchema(s);
});
return Promise.resolve(undefined);
}
/**
* Delete a schema with schema ID.
*/
async deleteSchema(schemaId) {
const id = this.normalizeId(schemaId);
if (this.schemasById[id]) {
delete this.schemasById[id];
}
this.schemaPriorityMapping.delete(id);
return Promise.resolve(undefined);
}
/**
* Add content to a specified schema at a specified path
*/
async addContent(additions) {
const schema = await this.getResolvedSchema(additions.schema);
if (schema) {
const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, additions.path);
if (typeof resolvedSchemaLocation === 'object') {
resolvedSchemaLocation[additions.key] = additions.content;
}
await this.saveSchema(additions.schema, schema.schema);
}
}
/**
* Delete content in a specified schema at a specified path
*/
async deleteContent(deletions) {
const schema = await this.getResolvedSchema(deletions.schema);
if (schema) {
const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, deletions.path);
if (typeof resolvedSchemaLocation === 'object') {
delete resolvedSchemaLocation[deletions.key];
}
await this.saveSchema(deletions.schema, schema.schema);
}
}
/**
* Take a JSON Schema and the path that you would like to get to
* @returns the JSON Schema resolved at that specific path
*/
resolveJSONSchemaToSection(schema, paths) {
const splitPathway = paths.split('/');
let resolvedSchemaLocation = schema;
for (const path of splitPathway) {
if (path === '') {
continue;
}
this.resolveNext(resolvedSchemaLocation, path);
resolvedSchemaLocation = resolvedSchemaLocation[path];
}
return resolvedSchemaLocation;
}
/**
* Resolve the next Object if they have compatible types
* @param object a location in the JSON Schema
* @param token the next token that you want to search for
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolveNext(object, token) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (Array.isArray(object) && isNaN(token)) {
throw new Error('Expected a number after the array object');
}
else if (typeof object === 'object' && typeof token !== 'string') {
throw new Error('Expected a string after the object');
}
}
/**
* Everything below here is needed because we're importing from vscode-json-languageservice umd and we need
* to provide a wrapper around the javascript methods we are calling since they have no type
*/
normalizeId(id) {
// The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here
try {
return vscode_uri_1.URI.parse(id).toString();
}
catch (e) {
return id;
}
}
getOrAddSchemaHandle(id, unresolvedSchemaContent) {
return super.getOrAddSchemaHandle(id, unresolvedSchemaContent);
}
loadSchema(schemaUri) {
const requestService = this.requestService;
return super.loadSchema(schemaUri).then(async (unresolvedJsonSchema) => {
// If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
// If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
// contain a number instead of being undefined, so we need to check for that too.
if (unresolvedJsonSchema.errors &&
(unresolvedJsonSchema.schema === undefined || typeof unresolvedJsonSchema.schema === 'number')) {
return requestService(schemaUri).then((content) => {
if (!content) {
const errorMessage = l10n.t("Unable to load schema from '{0}': No content. {1}", toDisplayString(schemaUri), unresolvedJsonSchema.errors);
return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
}
try {
const schemaContent = (0, yaml_1.parse)(content);
return new jsonSchemaService_1.UnresolvedSchema(schemaContent, []);
}
catch (yamlError) {
const errorMessage = l10n.t("Unable to parse content from '{0}': {1}.", toDisplayString(schemaUri), yamlError);
return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
}
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error) => {
let errorMessage = error.toString();
const errorSplit = error.toString().split('Error: ');
if (errorSplit.length > 1) {
// more concise error message, URL and context are attached by caller anyways
errorMessage = errorSplit[1];
}
return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
});
}
unresolvedJsonSchema.uri = schemaUri;
if (this.schemaUriToNameAndDescription.has(schemaUri)) {
const { name, description, versions } = this.schemaUriToNameAndDescription.get(schemaUri);
unresolvedJsonSchema.schema.title = name ?? unresolvedJsonSchema.schema.title;
unresolvedJsonSchema.schema.description = description ?? unresolvedJsonSchema.schema.description;
unresolvedJsonSchema.schema.versions = versions ?? unresolvedJsonSchema.schema.versions;
}
else if (unresolvedJsonSchema.errors && unresolvedJsonSchema.errors.length > 0) {
let errorMessage = unresolvedJsonSchema.errors[0];
if (errorMessage.toLowerCase().indexOf('load') !== -1) {
errorMessage = l10n.t("Unable to load schema from '{0}': No content.", toDisplayString(schemaUri));
}
else if (errorMessage.toLowerCase().indexOf('parse') !== -1) {
const content = await requestService(schemaUri);
const jsonErrors = [];
const schemaContent = Json.parse(content, jsonErrors);
if (jsonErrors.length && schemaContent) {
const { offset } = jsonErrors[0];
const { line, column } = getLineAndColumnFromOffset(content, offset);
errorMessage = l10n.t("Unable to parse content from '{0}': Parse error at line: {1} column: {2}", toDisplayString(schemaUri), line, column);
}
}
return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
}
return unresolvedJsonSchema;
});
}
registerExternalSchema(uri, filePatterns, unresolvedSchema, name, description, versions) {
if (name || description) {
this.schemaUriToNameAndDescription.set(uri, { name, description, versions });
}
return super.registerExternalSchema(uri, filePatterns, unresolvedSchema);
}
clearExternalSchemas() {
super.clearExternalSchemas();
}
setSchemaContributions(schemaContributions) {
super.setSchemaContributions(schemaContributions);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRegisteredSchemaIds(filter) {
return super.getRegisteredSchemaIds(filter);
}
getResolvedSchema(schemaId) {
return super.getResolvedSchema(schemaId);
}
onResourceChange(uri) {
return super.onResourceChange(uri);
}
}
exports.YAMLSchemaService = YAMLSchemaService;
function toDisplayString(url) {
try {
const uri = vscode_uri_1.URI.parse(url);
if (uri.scheme === 'file') {
return uri.fsPath;
}
}
catch (e) {
// ignore
}
return url;
}
function getLineAndColumnFromOffset(text, offset) {
const lines = text.slice(0, offset).split(/\r?\n/);
const line = lines.length; // 1-based line number
const column = lines[lines.length - 1].length + 1; // 1-based column number
return { line, column };
}
function normalizeSchemaUri(uri) {
if (!uri)
return '';
let s;
if (typeof uri === 'string') {
s = uri;
}
else {
s = uri.$id || uri.id || '';
}
s = s.trim();
// strips fragment (# or #/something)
const hash = s.indexOf('#');
s = hash === -1 ? s : s.slice(0, hash);
// normalize http to https (don't normalize custom dialects)
s = s.replace(/^http:\/\/json-schema\.org\//i, 'https://json-schema.org/');
// normalize to no trailing slash
s = s.replace(/\/+$/g, '');
return s;
}
function knownDialectFromSchemaUri(schemaUri) {
if (schemaUri === normalizeSchemaUri(ajv4.defaultMeta()))
return jsonSchema_1.SchemaDialect.draft04;
if (schemaUri === normalizeSchemaUri(ajv7.defaultMeta()))
return jsonSchema_1.SchemaDialect.draft07;
if (schemaUri === normalizeSchemaUri(ajv2019.defaultMeta()))
return jsonSchema_1.SchemaDialect.draft2019;
if (schemaUri === normalizeSchemaUri(ajv2020.defaultMeta()))
return jsonSchema_1.SchemaDialect.draft2020;
return undefined;
}
async function pickSchemaDialect($schema, loadSchema) {
if (!$schema)
return undefined;
const s = normalizeSchemaUri($schema || '');
const dialect = knownDialectFromSchemaUri(s);
if (dialect)
return dialect;
// cache custom dialect result
const cached = schemaDialectCache.get(s);
if (cached) {
return cached;
}
const inflight = schemaDialectInFlight.get(s);
if (inflight) {
return inflight;
}
if (!loadSchema)
return undefined;
// resolve custom dialect: load the dialect meta-schema doc and infer base dialect from its $schema
const promise = (async () => {
const meta = await loadSchema(s);
if (meta.errors?.length)
return undefined;
const metaSchema = meta.schema;
if (!metaSchema || typeof metaSchema !== 'object')
return undefined;
const metaDialect = knownDialectFromSchemaUri(metaSchema.$schema);
if (metaDialect)
return metaDialect;
return undefined;
})();
schemaDialectInFlight.set(s, promise);
try {
const result = await promise;
schemaDialectCache.set(s, result);
return result;
}
finally {
schemaDialectInFlight.delete(s);
}
}
function pickMetaValidator(dialect) {
switch (dialect) {
case jsonSchema_1.SchemaDialect.draft04:
return schema04Validator;
case jsonSchema_1.SchemaDialect.draft07:
return schema07Validator;
case jsonSchema_1.SchemaDialect.draft2019:
return schema2019Validator;
case jsonSchema_1.SchemaDialect.draft2020:
return schema2020Validator;
default:
// don't meta-validate unknown schema URI
return undefined;
}
}
});
//# sourceMappingURL=yamlSchemaService.js.map