UNPKG

vscode-json-languageservice

Version:
1,203 lines 61.5 kB
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
import * as Json from 'jsonc-parser';
import { URI } from 'vscode-uri';
import * as Strings from '../utils/strings.js';
import { asSchema, getSchemaDraftFromId, normalizeId } from '../parser/jsonParser.js';
import { SchemaDraft, ErrorCode } from '../jsonLanguageTypes.js';
import * as l10n from '@vscode/l10n';
import { createRegex } from '../utils/glob.js';
import { isString } from '../utils/objects.js';
import { Range } from 'vscode-languageserver-types';
const hasSchemePattern = /^[A-Za-z][A-Za-z0-9+\-.+]*:\/.*/.source;
const hasSchemeRegex = new RegExp(hasSchemePattern);
const BANG = '!';
const PATH_SEP = '/';
class FilePatternAssociation {
    constructor(pattern, folderUri, uris) {
        this.folderUri = folderUri;
        this.uris = uris;
        this.globWrappers = [];
        try {
            for (let patternString of pattern) {
                const include = patternString[0] !== BANG;
                if (!include) {
                    patternString = patternString.substring(1);
                }
                if (patternString.length > 0) {
                    if (patternString[0] === PATH_SEP) {
                        patternString = patternString.substring(1);
                    }
                    this.globWrappers.push({
                        regexp: createRegex('**/' + patternString, { extended: true, globstar: true }),
                        include: include,
                    });
                }
            }
            ;
            if (folderUri) {
                folderUri = normalizeResourceForMatching(folderUri);
                if (!folderUri.endsWith('/')) {
                    folderUri = folderUri + '/';
                }
                this.folderUri = folderUri;
            }
        }
        catch (e) {
            this.globWrappers.length = 0;
            this.uris = [];
        }
    }
    matchesPattern(fileName) {
        if (this.folderUri && !fileName.startsWith(this.folderUri)) {
            return false;
        }
        let match = false;
        for (const { regexp, include } of this.globWrappers) {
            if (regexp.test(fileName)) {
                match = include;
            }
        }
        return match;
    }
    getURIs() {
        return this.uris;
    }
}
class SchemaHandle {
    constructor(service, uri, unresolvedSchemaContent) {
        this.service = service;
        this.uri = uri;
        this.dependencies = new Set();
        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 => {
                return this.service.resolveSchemaContent(unresolved, this);
            });
        }
        return this.resolvedSchema;
    }
    clearSchema() {
        const hasChanges = !!this.unresolvedSchema;
        this.resolvedSchema = undefined;
        this.unresolvedSchema = undefined;
        this.dependencies.clear();
        this.anchors = undefined;
        return hasChanges;
    }
    setSchemaContent(schemaContent) {
        this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(schemaContent));
        this.resolvedSchema = undefined;
        this.anchors = undefined;
    }
}
export class UnresolvedSchema {
    constructor(schema, errors = []) {
        this.schema = schema;
        this.errors = errors;
    }
}
function toDiagnostic(message, code, relatedURL) {
    const relatedInformation = relatedURL ? [{
            location: { uri: relatedURL, range: Range.create(0, 0, 0, 0) },
            message
        }] : undefined;
    return { message, code, relatedInformation };
}
export class ResolvedSchema {
    constructor(schema, errors = [], warnings = [], schemaDraft, activeVocabularies) {
        this.schema = schema;
        this.errors = errors;
        this.warnings = warnings;
        this.schemaDraft = schemaDraft;
        this.activeVocabularies = activeVocabularies;
    }
    getSection(path) {
        const schemaRef = this.getSectionRecursive(path, this.schema);
        if (schemaRef) {
            return 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;
    }
}
export class JSONSchemaService {
    static traverseSchemaProperties(node, callback) {
        // `$defs`/`definitions` are visited first so that a reusable resource they
        // contain (which may itself carry a `$ref` chain) begins resolving before an
        // `if`/`then`/`else`/`items`/… sibling references it. During asynchronous
        // `$ref` resolution the referenced resource must be fully assembled before a
        // referrer merges it, otherwise the referrer captures a half-resolved snapshot
        // (e.g. a `$dynamicRef` chain reached through `then: { $ref: "numberList" }`).
        const schemaMapProps = ['definitions', '$defs', 'properties', 'patternProperties',
            'dependencies', 'dependentSchemas'];
        const singleSchemaProps = ['additionalItems', 'additionalProperties', 'not', 'contains',
            'propertyNames', 'if', 'then', 'else', 'unevaluatedItems', 'unevaluatedProperties', 'items'];
        const schemaArrayProps = ['anyOf', 'allOf', 'oneOf', 'prefixItems'];
        const visitValue = (value) => {
            if (value) {
                if (Array.isArray(value)) {
                    value.forEach(item => visitValue(item));
                }
                else if (typeof value === 'object') {
                    callback(value);
                }
            }
        };
        for (const prop of schemaMapProps) {
            const map = node[prop];
            if (map && typeof map === 'object') {
                Object.values(map).forEach(visitValue);
            }
        }
        for (const prop of singleSchemaProps) {
            const propValue = node[prop];
            if (propValue) {
                visitValue(propValue);
            }
        }
        for (const prop of schemaArrayProps) {
            const propValue = node[prop];
            if (propValue) {
                visitValue(propValue);
            }
        }
    }
    constructor(requestService, contextService, promiseConstructor) {
        this.contextService = contextService;
        this.requestService = requestService;
        this.promiseConstructor = promiseConstructor || Promise;
        this.callOnDispose = [];
        this.contributionSchemas = {};
        this.contributionAssociations = [];
        this.schemasById = {};
        this.filePatternAssociations = [];
        this.registeredSchemasIds = {};
    }
    getRegisteredSchemaIds(filter) {
        return Object.keys(this.registeredSchemasIds).filter(id => {
            const scheme = URI.parse(id).scheme;
            return scheme !== 'schemaservice' && (!filter || filter(scheme));
        });
    }
    get promise() {
        return this.promiseConstructor;
    }
    dispose() {
        while (this.callOnDispose.length > 0) {
            this.callOnDispose.pop()();
        }
    }
    onResourceChange(uri) {
        // always clear this local cache when a resource changes
        this.cachedSchemaForResource = undefined;
        let hasChanges = false;
        uri = normalizeId(uri);
        const toWalk = [uri];
        const all = Object.keys(this.schemasById).map(key => this.schemasById[key]);
        while (toWalk.length) {
            const curr = toWalk.pop();
            for (let i = 0; i < all.length; i++) {
                const handle = all[i];
                if (handle && (handle.uri === curr || handle.dependencies.has(curr))) {
                    if (handle.uri !== curr) {
                        toWalk.push(handle.uri);
                    }
                    if (handle.clearSchema()) {
                        hasChanges = true;
                    }
                    all[i] = undefined;
                }
            }
        }
        return hasChanges;
    }
    setSchemaContributions(schemaContributions) {
        if (schemaContributions.schemas) {
            const schemas = schemaContributions.schemas;
            for (const id in schemas) {
                const normalizedId = normalizeId(id);
                this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]);
            }
        }
        if (Array.isArray(schemaContributions.schemaAssociations)) {
            const schemaAssociations = schemaContributions.schemaAssociations;
            for (let schemaAssociation of schemaAssociations) {
                const uris = schemaAssociation.uris.map(normalizeId);
                const association = this.addFilePatternAssociation(schemaAssociation.pattern, schemaAssociation.folderUri, uris);
                this.contributionAssociations.push(association);
            }
        }
    }
    addSchemaHandle(id, unresolvedSchemaContent) {
        const schemaHandle = new SchemaHandle(this, id, unresolvedSchemaContent);
        this.schemasById[id] = schemaHandle;
        return schemaHandle;
    }
    getOrAddSchemaHandle(id, unresolvedSchemaContent) {
        return this.schemasById[id] || this.addSchemaHandle(id, unresolvedSchemaContent);
    }
    addFilePatternAssociation(pattern, folderUri, uris) {
        const fpa = new FilePatternAssociation(pattern, folderUri, uris);
        this.filePatternAssociations.push(fpa);
        return fpa;
    }
    registerExternalSchema(config) {
        const id = normalizeId(config.uri);
        this.registeredSchemasIds[id] = true;
        this.cachedSchemaForResource = undefined;
        if (config.fileMatch && config.fileMatch.length) {
            this.addFilePatternAssociation(config.fileMatch, config.folderUri, [id]);
        }
        return config.schema ? this.addSchemaHandle(id, config.schema) : this.getOrAddSchemaHandle(id);
    }
    clearExternalSchemas() {
        this.schemasById = {};
        this.filePatternAssociations = [];
        this.registeredSchemasIds = {};
        this.cachedSchemaForResource = undefined;
        for (const id in this.contributionSchemas) {
            this.schemasById[id] = this.contributionSchemas[id];
            this.registeredSchemasIds[id] = true;
        }
        for (const contributionAssociation of this.contributionAssociations) {
            this.filePatternAssociations.push(contributionAssociation);
        }
    }
    getResolvedSchema(schemaId) {
        const id = normalizeId(schemaId);
        const schemaHandle = this.schemasById[id];
        if (schemaHandle) {
            return schemaHandle.getResolvedSchema();
        }
        return this.promise.resolve(undefined);
    }
    loadSchema(url) {
        if (!this.requestService) {
            const errorMessage = l10n.t('Unable to load schema from \'{0}\'. No schema request service available', toDisplayString(url));
            return this.promise.resolve(new UnresolvedSchema({}, [toDiagnostic(errorMessage, ErrorCode.SchemaResolveError, url)]));
        }
        return this.requestService(url).then(content => {
            if (!content) {
                const errorMessage = l10n.t('Unable to load schema from \'{0}\': No content.', toDisplayString(url));
                return new UnresolvedSchema({}, [toDiagnostic(errorMessage, ErrorCode.SchemaResolveError, url)]);
            }
            const errors = [];
            if (content.charCodeAt(0) === 65279) {
                errors.push(toDiagnostic(l10n.t('Problem reading content from \'{0}\': UTF-8 with BOM detected, only UTF 8 is allowed.', toDisplayString(url)), ErrorCode.SchemaResolveError, url));
                content = content.trimStart();
            }
            let schemaContent = {};
            const jsonErrors = [];
            schemaContent = Json.parse(content, jsonErrors);
            if (jsonErrors.length) {
                errors.push(toDiagnostic(l10n.t('Unable to parse content from \'{0}\': Parse error at offset {1}.', toDisplayString(url), jsonErrors[0].offset), ErrorCode.SchemaResolveError, url));
            }
            return new UnresolvedSchema(schemaContent, errors);
        }, (error) => {
            let { message, code } = error;
            if (typeof message !== 'string') {
                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];
                }
                if (Strings.endsWith(errorMessage, '.')) {
                    errorMessage = errorMessage.substr(0, errorMessage.length - 1);
                }
                message = errorMessage;
            }
            let errorCode = ErrorCode.SchemaResolveError;
            if (typeof code === 'number' && code < 0x10000) {
                errorCode += code;
            }
            const errorMessage = l10n.t('Unable to load schema from \'{0}\': {1}.', toDisplayString(url), message);
            return new UnresolvedSchema({}, [toDiagnostic(errorMessage, errorCode, url)]);
        });
    }
    resolveSchemaContent(schemaToResolve, handle) {
        const resolveErrors = schemaToResolve.errors.slice(0);
        const schema = schemaToResolve.schema;
        // External documents whose embedded $id resources have already been registered
        // as resolvable handles, so we only do it once per referenced document.
        const embeddedRegisteredFor = new Set();
        const schemaDraft = schema.$schema ? getSchemaDraftFromId(schema.$schema) : undefined;
        if (schemaDraft === SchemaDraft.v3) {
            return this.promise.resolve(new ResolvedSchema({}, [toDiagnostic(l10n.t("Draft-03 schemas are not supported."), ErrorCode.SchemaUnsupportedFeature)], [], schemaDraft, undefined));
        }
        let activeVocabularies = undefined;
        const extractVocabularies = (metaschema) => {
            if (!metaschema.$vocabulary || typeof metaschema.$vocabulary !== 'object') {
                return undefined;
            }
            // Both true and false values indicate the vocabulary is active.
            // The boolean indicates whether the vocabulary is required (true) or optional (false),
            // not whether it's in use. All listed vocabularies should be included.
            const vocabs = new Map();
            for (const [uri, required] of Object.entries(metaschema.$vocabulary)) {
                vocabs.set(uri, required);
            }
            return vocabs.size > 0 ? vocabs : undefined;
        };
        const contextService = this.contextService;
        // Attach internal, non-enumerable metadata to a schema node. Hidden so it is
        // invisible to schema traversal, merging and consumers, but available to the
        // validator (e.g. for $recursiveRef/$dynamicRef resolution).
        const setHidden = (obj, key, value) => {
            Object.defineProperty(obj, key, {
                value,
                enumerable: false,
                writable: true,
                configurable: true
            });
        };
        // Get (creating on first use) the hidden $dynamicRefInfo record for a $dynamicRef
        // node. Its fields are populated across two passes — `scope` while collecting
        // anchors, `target`/`name` while resolving the reference — over the same object.
        const dynamicRefInfoOf = (node) => {
            let info = node.$dynamicRefInfo;
            if (!info) {
                info = {};
                setHidden(node, '$dynamicRefInfo', info);
            }
            return info;
        };
        const findSectionByJSONPointer = (schema, path) => {
            path = decodeURIComponent(path);
            let current = schema;
            if (path[0] === '/') {
                path = path.substring(1);
            }
            path.split('/').some((part) => {
                part = part.replace(/~1/g, '/').replace(/~0/g, '~');
                current = current[part];
                // A boolean `false` is a valid schema, not a "missing" section, so only
                // stop on genuinely absent values (undefined/null).
                return current === undefined || current === null;
            });
            return current;
        };
        // Like findSectionByJSONPointer, but also tracks the effective base URI
        // through any $id encountered along the path. This is needed so that
        // $ref values merged from the found section can be resolved against the
        // correct base, not the document root.
        const findSectionAndBase = (schema, path, baseHandle) => {
            path = decodeURIComponent(path);
            let current = schema;
            let currentBaseHandle = baseHandle;
            if (path[0] === '/') {
                path = path.substring(1);
            }
            path.split('/').some((part) => {
                part = part.replace(/~1/g, '/').replace(/~0/g, '~');
                current = current[part];
                // A boolean `false` is a valid schema, not a "missing" section, so only
                // stop on genuinely absent values (undefined/null).
                if (current === undefined || current === null) {
                    return true;
                }
                const id = getSchemaId(current);
                if (isString(id) && id.charAt(0) !== '#') {
                    let resolvedUri = id;
                    if (contextService && !hasSchemeRegex.test(id)) {
                        resolvedUri = contextService.resolveRelativePath(id, currentBaseHandle.uri);
                    }
                    resolvedUri = normalizeId(resolvedUri);
                    currentBaseHandle = this.getOrAddSchemaHandle(resolvedUri);
                }
                return false;
            });
            return { section: current, baseHandle: currentBaseHandle };
        };
        const findSchemaById = (schema, handle, id) => {
            if (!handle.anchors) {
                handle.anchors = collectAnchors(schema);
            }
            return handle.anchors.get(id);
        };
        const getSchemaId = (schema) => schema.$id || schema.id;
        // Fold a source resource's $dynamicAnchor names into a target node's dynamic
        // map, creating it on demand. Existing entries win, so the outermost resource
        // in a dynamic scope retains precedence — this is what lets a $dynamicAnchor in
        // an outer resource override an inner (referenced) one during the validation
        // walk. Only the `dynamic` map is folded: the `local` map must stay per-resource
        // (it resolves an internal $dynamicRef's initial target within its own lexical
        // resource), so a sibling/referenced resource's identically-named anchor must
        // not leak into it.
        const unionAnchorMaps = (target, srcMaps) => {
            if (srcMaps === undefined) {
                return;
            }
            let maps = target.$anchorMaps;
            if (maps === undefined) {
                maps = { dynamic: new Map(), local: new Map() };
                setHidden(target, '$anchorMaps', maps);
            }
            for (const [name, node] of srcMaps.dynamic) {
                if (!maps.dynamic.has(name)) {
                    maps.dynamic.set(name, node);
                }
            }
        };
        const merge = (target, section) => {
            for (const key in section) {
                if (!section.hasOwnProperty(key) || key === 'id' || key === '$id') {
                    continue;
                }
                // Deep merge for properties and patternProperties to combine them
                const shouldDeepMerge = (key === 'properties' || key === 'patternProperties') &&
                    typeof section[key] === 'object' && section[key] !== null &&
                    typeof target[key] === 'object' && target[key] !== null;
                target[key] = shouldDeepMerge
                    ? { ...target[key], ...section[key] }
                    : section[key];
            }
            // Preserve $id as a non-enumerable hidden property for $recursiveRef resolution.
            // This allows $recursiveRef to correctly resolve references within the schema without exposing $id publicly.
            const id = section.$id || section.id;
            if (id) {
                Object.defineProperty(target, '$originalId', {
                    value: id,
                    enumerable: false,
                    writable: true,
                    configurable: true
                });
            }
            // Propagate hidden $dynamicRef metadata. When a $dynamicRef lives directly on
            // a $ref'd schema resource, that resource is a distinct object from the
            // referencing schema and is often resolved standalone first — deleting its
            // $dynamicRef and recording the (non-enumerable, so not copied above)
            // $dynamicRefInfo. Carry it over so the merged schema still resolves
            // dynamically. (When the $dynamicRef lives on a child of the merged resource,
            // that child is shared by reference and already carries its own metadata.)
            const src = section;
            const dst = target;
            if (src.$dynamicRefInfo !== undefined && dst.$dynamicRefInfo === undefined) {
                setHidden(target, '$dynamicRefInfo', src.$dynamicRefInfo);
            }
            // When the merged section is itself a schema resource (it carries anchor
            // maps), fold its $dynamicAnchor / $anchor maps into the target's. The
            // merged node stands in for that resource during validation (it becomes a
            // dynamic-scope root via $originalId), so those anchors must participate in
            // dynamic-scope resolution — this is what lets a $dynamicAnchor defined in
            // an external (or $ref'd sub-) resource override a base default. Existing
            // entries are kept so the outermost resource retains precedence.
            unionAnchorMaps(dst, src.$anchorMaps);
        };
        const hasKeyword = (schema, keyword) => schema[keyword] !== undefined;
        const hasAnyKeyword = (schema, keywords) => keywords.some(keyword => hasKeyword(schema, keyword));
        const objectPropertyKeywords = ['properties', 'patternProperties'];
        const objectEvaluatorKeywords = [
            ...objectPropertyKeywords,
            'additionalProperties',
            'dependentSchemas',
            'allOf',
            'anyOf',
            'oneOf',
            'if',
            'then',
            'else'
        ];
        const arrayEvaluatorKeywords = [
            'items',
            'additionalItems',
            'prefixItems',
            'allOf',
            'anyOf',
            'oneOf',
            'if',
            'then',
            'else'
        ];
        const containsBoundKeywords = ['minContains', 'maxContains'];
        const conditionalBranchKeywords = ['then', 'else'];
        const arrayApplicatorKeywords = ['items', 'prefixItems', 'additionalItems'];
        const uses2020_12ArrayAnnotations = schemaDraft === undefined || schemaDraft >= SchemaDraft.v2020_12;
        // Some keywords only make sense relative to adjacent keywords in the same schema object.
        // If they live behind $ref, sibling keywords on the referencing schema must not be
        // flattened into the same scope.
        const needsScopeIsolation = (referencedSchema, referencingSchema) => {
            const hasSiblingObjectProperties = hasAnyKeyword(referencingSchema, objectPropertyKeywords);
            if (hasKeyword(referencedSchema, 'additionalProperties') && hasSiblingObjectProperties) {
                return true;
            }
            if (hasKeyword(referencedSchema, 'additionalItems') && Array.isArray(referencingSchema.items)) {
                return true;
            }
            const hasSiblingObjectEvaluators = hasAnyKeyword(referencingSchema, objectEvaluatorKeywords);
            if (hasKeyword(referencedSchema, 'unevaluatedProperties') && hasSiblingObjectEvaluators) {
                return true;
            }
            const hasSiblingArrayEvaluators = hasAnyKeyword(referencingSchema, arrayEvaluatorKeywords) ||
                (uses2020_12ArrayAnnotations && hasKeyword(referencingSchema, 'contains'));
            if (hasKeyword(referencedSchema, 'unevaluatedItems') && hasSiblingArrayEvaluators) {
                return true;
            }
            // Reverse of the above: the *referencing* schema declares unevaluatedItems
            // while the referenced schema constrains items positionally and may contain
            // internal self-references (`$ref: "#"`). Flattening would pull the referenced
            // items into the referencing scope, so those self-references would inherit the
            // referencing unevaluatedItems. Isolate so the referenced resource keeps its
            // own annotation scope.
            if (hasKeyword(referencingSchema, 'unevaluatedItems') && hasAnyKeyword(referencedSchema, arrayApplicatorKeywords)) {
                return true;
            }
            if (hasKeyword(referencedSchema, 'contains') && hasAnyKeyword(referencingSchema, containsBoundKeywords)) {
                return true;
            }
            // Both the referenced and referencing schemas constrain array items
            // (tuple `items`, or 2020-12 `items`/`prefixItems`). A flatten-merge can
            // only keep one `items`, silently dropping the other; isolate into an
            // allOf so both position constraints apply and item-evaluation
            // annotations (for unevaluatedItems) accumulate across both.
            if (Array.isArray(referencedSchema.items) && Array.isArray(referencingSchema.items)) {
                return true;
            }
            if (hasAnyKeyword(referencedSchema, containsBoundKeywords) && hasKeyword(referencingSchema, 'contains')) {
                return true;
            }
            if (hasKeyword(referencedSchema, 'if') && hasAnyKeyword(referencingSchema, conditionalBranchKeywords)) {
                return true;
            }
            if (hasAnyKeyword(referencedSchema, conditionalBranchKeywords) && hasKeyword(referencingSchema, 'if')) {
                return true;
            }
            return uses2020_12ArrayAnnotations &&
                hasKeyword(referencedSchema, 'items') &&
                hasKeyword(referencingSchema, 'prefixItems');
        };
        const mergeRef = (target, sourceRoot, sourceHandle, refSegment) => {
            let section;
            let sectionBaseHandle = sourceHandle;
            if (refSegment === undefined || refSegment.length === 0) {
                section = sourceRoot;
            }
            else if (refSegment.charAt(0) === '/') {
                // A $ref to a JSON Pointer (i.e #/definitions/foo)
                // Track $id base changes along the path so inner $refs resolve correctly.
                ({ section, baseHandle: sectionBaseHandle } = findSectionAndBase(sourceRoot, refSegment, sourceHandle));
            }
            else {
                // A $ref to a sub-schema with an $id (i.e #hello)
                section = findSchemaById(sourceRoot, sourceHandle, refSegment);
            }
            // A boolean is a valid JSON Schema: `true` accepts everything ({}) and
            // `false` rejects everything ({ not: {} }). Normalize so it merges like any
            // other schema (and isn't mistaken for an unresolved section below).
            if (typeof section === 'boolean') {
                section = section ? {} : { not: {} };
            }
            if (section) {
                // If the found section contains a $ref that needs to be resolved
                // relative to a different base (e.g. it's inside a schema with $id),
                // pre-resolve it now so the merged target carries the correct base.
                // Clone before rewriting to avoid mutating the cached source schema.
                if (section.$ref && sectionBaseHandle !== sourceHandle) {
                    const innerRef = section.$ref;
                    const innerSegments = innerRef.split('#', 2);
                    if (innerSegments[0].length > 0 && contextService && !hasSchemeRegex.test(innerSegments[0])) {
                        section = {
                            ...section,
                            $ref: contextService.resolveRelativePath(innerSegments[0], sectionBaseHandle.uri) +
                                (innerSegments[1] !== undefined ? '#' + innerSegments[1] : '')
                        };
                    }
                }
                const reservedKeys = new Set(['$ref', '$defs', 'definitions', '$schema', '$id', 'id']);
                // Keywords that must stay on the wrapper rather than move into the allOf
                // sibling. Scope-anchor keywords ($recursiveAnchor/$dynamicAnchor) keep this
                // resource discoverable as a $recursiveRef/$dynamicRef bookending target.
                // The unevaluated* annotations must observe the results of *all* in-place
                // applicators (including the $ref'd schema in allOf[0]); leaving them on the
                // wrapper lets them see the aggregated annotations rather than only the
                // sibling's own evaluations.
                const keepOnWrapperKeys = new Set([
                    '$recursiveAnchor', '$dynamicAnchor', 'unevaluatedItems', 'unevaluatedProperties'
                ]);
                // In JSON Schema draft-04 through draft-07, $ref completely overrides any sibling keywords.
                // Starting in 2019-09, sibling keywords are processed alongside $ref.
                // Only strip siblings when schema explicitly declares a pre-2019-09 draft via $schema.
                const isPreDraft201909 = schemaDraft !== undefined && schemaDraft < SchemaDraft.v2019_09;
                if (isPreDraft201909) {
                    // Clear all sibling keywords from target - $ref takes precedence
                    for (const key in target) {
                        if (target.hasOwnProperty(key) && !reservedKeys.has(key)) {
                            delete target[key];
                        }
                    }
                    merge(target, section);
                }
                else if (needsScopeIsolation(section, target)) {
                    // In JSON Schema 2019-09 or greater, $ref creates a new scope when sibling
                    // keywords would otherwise change the meaning of same-object-dependent keywords
                    // from the referenced schema.
                    // To achieve this, we wrap the $ref in an allOf when needed.
                    const siblingSchema = {};
                    const refSchema = { ...section };
                    // Move all existing properties from target to siblingSchema, except
                    // keywords that must remain on the wrapper resource.
                    for (const key in target) {
                        if (target.hasOwnProperty(key) && !reservedKeys.has(key) && !keepOnWrapperKeys.has(key)) {
                            const k = key;
                            siblingSchema[k] = target[k];
                            delete target[k];
                        }
                    }
                    // Create allOf with the $ref'd schema and sibling schema
                    target.allOf = [refSchema, siblingSchema];
                }
                else {
                    merge(target, section);
                }
            }
            else {
                const message = l10n.t('$ref \'{0}\' in \'{1}\' can not be resolved.', refSegment || '', sourceHandle.uri);
                resolveErrors.push(toDiagnostic(message, ErrorCode.SchemaResolveError));
            }
        };
        const getExternalSchemaHandle = (uri, parentHandle) => {
            if (contextService && !hasSchemeRegex.test(uri)) {
                uri = contextService.resolveRelativePath(uri, parentHandle.uri);
            }
            uri = normalizeId(uri);
            return this.getOrAddSchemaHandle(uri);
        };
        const resolveExternalLink = (node, uri, refSegment, parentHandle) => {
            const referencedHandle = getExternalSchemaHandle(uri, parentHandle);
            uri = referencedHandle.uri;
            return referencedHandle.getUnresolvedSchema().then(unresolvedSchema => {
                parentHandle.dependencies.add(uri);
                if (unresolvedSchema.errors.length) {
                    const error = unresolvedSchema.errors[0];
                    const loc = refSegment ? uri + '#' + refSegment : uri;
                    const errorMessage = refSegment ? l10n.t('Problems loading reference \'{0}\': {1}', refSegment, error.message) : error.message;
                    resolveErrors.push(toDiagnostic(errorMessage, error.code, uri));
                }
                // A referenced document may itself embed subschemas with their own
                // absolute $id. Register those as resolvable handles (once per document)
                // so a nested $ref by that $id resolves to the embedded resource instead
                // of being (mis)loaded as a standalone document. registerEmbeddedSchemas
                // only runs for the root document otherwise.
                if (!embeddedRegisteredFor.has(uri)) {
                    embeddedRegisteredFor.add(uri);
                    registerEmbeddedSchemas(unresolvedSchema.schema, uri);
                }
                // 2020-12: collect the referenced document's own resource anchor maps
                // (once) before merging, so its $dynamicAnchor declarations can join the
                // dynamic scope and its $dynamicRef nodes get their lexical resource
                // recorded — mirroring what is done eagerly for the root document, before
                // $ref merging flattens resource boundaries.
                if (unresolvedSchema.schema.$anchorMaps === undefined) {
                    collectDynamicAnchors(unresolvedSchema.schema);
                }
                mergeRef(node, unresolvedSchema.schema, referencedHandle, refSegment);
                // Referencing a fragment of another resource (e.g. "other#/$defs/x")
                // still *enters* that resource's dynamic scope, so its $dynamicAnchor
                // declarations must join `node`'s scope even though only the sub-schema
                // was merged. (For a whole-document ref the merge above already did this,
                // since the merged section is the resource root; this is a no-op then.)
                unionAnchorMaps(node, unresolvedSchema.schema.$anchorMaps);
                return resolveRefs(node, unresolvedSchema.schema, referencedHandle);
            });
        };
        const resolveDynamicRef = (schema, newBase, newBaseHandle, currentBaseHandle) => {
            // 2020-12 $dynamicRef. Its *initial* target is resolved statically, exactly
            // like a plain $ref, and kept as hidden metadata (info.target) instead of
            // being merged into `schema`, so `schema`'s own keywords are preserved. The
            // referenced plain-name fragment is recorded as info.name. The "bookending"
            // decision (does the initial target name a $dynamicAnchor?) and the
            // dynamic-scope walk are both deferred to validation time, where the (possibly
            // asynchronously resolved) target is fully populated. Returns the external
            // resolution promise, if one is needed.
            const info = dynamicRefInfoOf(schema);
            const ref = schema.$dynamicRef;
            const segments = ref.split('#', 2);
            delete schema.$dynamicRef;
            // A JSON-pointer fragment ("#/…") or a missing fragment can never name a
            // $dynamicAnchor, so such a $dynamicRef always behaves like a plain $ref.
            const fragment = segments[1];
            const dynamicName = (isString(fragment) && fragment.length > 0 && fragment.charAt(0) !== '/') ? fragment : undefined;
            if (dynamicName !== undefined) {
                info.name = dynamicName;
            }
            if (segments[0].length > 0) {
                // External / relative reference: resolve the target document into a
                // throwaway container so `schema`'s own keywords are preserved.
                const target = {};
                info.target = target;
                const refBase = (newBase === schema) ? currentBaseHandle : newBaseHandle;
                return resolveExternalLink(target, segments[0], segments[1], refBase);
            }
            // Internal reference. Resolve #name within the *lexical* schema resource
            // (recorded as info.scope before $ref merging flattened resource boundaries)
            // so a sibling resource's identically-named anchor cannot leak in.
            let target;
            if (dynamicName !== undefined && info.scope?.$anchorMaps) {
                target = info.scope.$anchorMaps.local.get(dynamicName);
            }
            if (target === undefined) {
                // JSON-pointer fragment, or no lexical anchor found: fall back to a plain
                // static $ref resolution against the current base.
                const container = {};
                mergeRef(container, newBase, newBaseHandle, segments[1]);
                target = container;
            }
            info.target = target;
            return undefined;
        };
        const resolveRefs = (node, parentSchema, parentHandle) => {
            const prefetchExternalSchema = (uri, parentHandle) => {
                // Ordered traversal reuses this cached request before mutating schema state.
                getExternalSchemaHandle(uri, parentHandle).getUnresolvedSchema();
            };
            const prefetchExternalRefs = (schema, currentBase, currentBaseHandle, seen) => {
                if (!schema || typeof schema !== 'object' || seen.has(schema)) {
                    return;
                }
                seen.add(schema);
                const id = getSchemaId(schema);
                let newBase = currentBase;
                let newBaseHandle = currentBaseHandle;
                if (isString(id) && id.charAt(0) !== '#') {
                    let resolvedUri = id;
                    if (contextService && !hasSchemeRegex.test(id)) {
                        resolvedUri = contextService.resolveRelativePath(id, currentBaseHandle.uri);
                    }
                    newBase = schema;
                    newBaseHandle = this.getOrAddSchemaHandle(normalizeId(resolvedUri));
                }
                const refBase = (newBase === schema) ? currentBaseHandle : newBaseHandle;
                if (schema.$ref) {
                    const segments = schema.$ref.split('#', 2);
                    if (segments[0].length > 0) {
                        prefetchExternalSchema(segments[0], refBase);
                    }
                    if (schemaDraft !== undefined && schemaDraft < SchemaDraft.v2019_09) {
                        return;
                    }
                }
                if (schema.$dynamicRef && (schemaDraft === undefined || schemaDraft >= SchemaDraft.v2020_12)) {
                    const segments = schema.$dynamicRef.split('#', 2);
                    if (segments[0].length > 0) {
                        prefetchExternalSchema(segments[0], refBase);
                    }
                }
                JSONSchemaService.traverseSchemaProperties(schema, childSchema => {
                    prefetchExternalRefs(childSchema, newBase, newBaseHandle, seen);
                });
            };
            prefetchExternalRefs(node, parentSchema, parentHandle, new Set());
            // Traversal that tracks the current base schema for internal refs.
            // When we encounter a schema with its own $id, that becomes the new base
            // for resolving fragment refs (#...) in its descendants
            const traverseWithBaseTracking = (schema, currentBase, currentBaseHandle, isRoot, seen) => {
                if (!schema || typeof schema !== 'object' || seen.has(schema)) {
                    return this.promise.resolve(undefined);
                }
                seen.add(schema);
                // A schema with its own $id defines a new base URI scope for everything
                // inside it. Resolve the id (relative to the current base) and use that
                // handle as the base going forward.
                //
                // We do this even when `isRoot` is true so that a recursive call from
                // `resolveExternalLink` — which passes the external schema's handle as
                // `currentBaseHandle` — still uses *this* node's own $id as its base
                // when re-traversing it. Without this, nested $id values inside the
                // original schema would be resolved against the external schema's URI.
                const id = getSchemaId(schema);
                let newBase = currentBase;
                let newBaseHandle = currentBaseHandle;
                if (isString(id) && id.charAt(0) !== '#') {
                    let resolvedUri = id;
                    if (contextService && !hasSchemeRegex.test(id)) {
                        resolvedUri = contextService.resolveRelativePath(id, currentBaseHandle.uri);
                    }
                    resolvedUri = normalizeId(resolvedUri);
                    newBase = schema;
                    newBaseHandle = this.getOrAddSchemaHandle(resolvedUri);
                    // Register the schema under its id if we haven't already.
                    if (!this.schemasById[resolvedUri]) {
                        this.addSchemaHandle(resolvedUri, schema);
                    }
                }
                // Process refs in this schema
                const seenRefs = new Set();
                while (schema.$ref) {
                    const ref = schema.$ref;
                    const segments = ref.split('#', 2);
                    delete schema.$ref;
                    if (segments[0].length > 0) {
                        // This is a reference to an external schema (like "foo.json" or "foo.json#/bar")
                        // Per JSON Schema spec, $ref is resolved against the current base URI.
                        // If this schema has its own $id (sibling case), the $ref should resolve
                        // against the parent's base, not the sibling $id. Otherwise, use the
                        // nearest ancestor's base (newBaseHandle).
                        const refBase = (newBase === schema) ? currentBaseHandle : newBaseHandle;
                        return resolveExternalLink(schema, segments[0], segments[1], refBase).then(() => undefined);
                    }
                    else {
                        // This is an internal reference (like "#/definitions/foo")
                        // Internal refs are resolved within the current document
                        // If this schema has its own $id, it is a new document, so use newBase
                        // Otherwise, use the parent's base (currentBase)
                        if (!seenRefs.has(ref)) {
                            const refId = segments[1];
                            mergeRef(schema, newBase, newBaseHandle, refId);
                            seenRefs.add(ref);
                        }
                    }
                }
                // Apply child schemas in keyword order so references never observe
                // partially resolved state from an earlier schema.
                const traverseChildren = () => {
                    let result = this.promise.resolve(undefined);
                    JSONSchemaService.traverseSchemaProperties(schema, childSchema => {
                        result = result.then(() => traverseWithBaseTracking(childSchema, newBase, newBaseHandle, false, seen));
                    });
                    return result;
                };
                // $dynamicRef is a 2020-12 core keyword. In earlier drafts it is an
                // unknown keyword, so leave it untouched and unresolved (ignored) there.
                if (schema.$dynamicRef && (schemaDraft === undefined || schemaDraft >= SchemaDraft.v2020_12)) {
                    const dynamicRefPromise = resolveDynamicRef(schema, newBase, newBaseHandle, currentBaseHandle);
                    if (dynamicRefPromise) {
                        return dynamicRefPromise.then(() => traverseChildren());
                    }
                }
                return traverseChildren();
            };
            return traverseWithBaseTracking(node, parentSchema, parentHandle, true, new Set());
        };
        const collectAnchors = (root) => {
            const result = new Map();
            const seen = new Set();
            // Use the schema's own $schema to determine draft, so that an external
            // schema referenced from a doesn't inherit the parent's anchor rules.
            const draft = root.$schema ? getSchemaDraftFromId(root.$schema) : schemaDraft;
            // Traversal that stops at sub-schemas with their own $id
            // because those create a new URI scope for anchors
            const traverseForAnchors = (node, isRoot) => {
                if (!node || typeof node !== 'object' || seen.has(node)) {
                    return;
                }
                seen.add(node);
                // If this node has its own $id, and it's not the root, it's a new URI scope
                const id = getSchemaId(node);
                if (!isRoot && isString(id) && id.charAt(0) !== '#') {
                    return;
                }
                // Collect anchor from this node
                // In draft-04/06/07, anchors are defined via $id/#fragment (e.g., "$id": "#myanchor")
                // In 2019-09+, $id fragments are no longer anchors; $anchor is used instead
                // In 2020-12, $dynamicAnchor also defines a plain-name fragment that a
                // (static) $ref can resolve to, just like $anchor.
                const fragmentAnchor = (draft === undefined || draft < SchemaDraft.v2019_09) && isString(id) && id.charAt(0) === '#' ? id.substring(1) : undefined;
                const dollarAnchor = (draft === undefined || draft >= SchemaDraft.v2019_09) ? node.$anchor : undefined;
                const dynamicAnchor = (draft === undefined || draft >= SchemaDraft.v2020_12) ? node.$dynamicAnchor : undefined;
                const registerAnchor = (anchor) => {
                    if (!anchor) {
                        return;
                    }
                    if (result.has(anchor) && result.get(anchor) !== node) {
                        resolveErrors.push(toDiagnostic(l10n.t('Duplicate anchor declaration: \'{0}\'', anchor), ErrorCode.SchemaResolveError));
                    }
                    else {
                        result.set(anchor, node);
                    }
                };
                registerAnchor(fragmentAnchor ?? dollarAnchor);
                registerAnchor(dynamicAnchor);
                // Continue traversing child schemas
                JSONSchemaService.traverseSchemaProperties(node, (childSchema) => {
                    traverseForAnchors(childSchema, false);
                });
            };
            traverseForAnchors(root, true);
            return result;
        };
        // 2020-12: group $anchor / $dynamicAnchor declarations by the schema resource
        // ($id scope) that contains them, and attach each resource's name→node maps to
        // its resource-root node as hidden metadata ($anchorMaps):
        //   - $anchorMaps.dynamic: only $dynamicAnchor names, used by the validator to
        //     walk the dynamic scope (outermost resource first).
        //   - $anchorMaps.local: both $anchor and $dynamicAnchor names, used to resolve
        //     an internal $dynamicRef's initial target within its own resource.
        // Each $dynamicRef node also gets its lexical resource recorded as
        // $dynamicRefInfo.scope. This must run on the original schema, before $ref
        // resolution merges (and thereby flattens) resource boundaries; node identities
        // are preserved through in-place resolution, so the attached metadata stays valid.
        const collectDynamicAnchors = (root) => {
            // Respect the resource's own $schema (like collectAnchors) so a referenced
            // document with a different, pre-2020-12 dialect is not given $dynamicAnchor
            // semantics just because the root document is 2020-12.
            const draft = root && typeof root === 'object' && root.$schema ? getSchemaDraftFromId(root.$schema) : schemaDraft;
            if (!(draft === undefined || draft >= SchemaDraft.v2020_12)) {
                return;
            }
            const seen = new Set();
            const visit = (node, resourceRoot) => {
                if (!node || typeof node !== 'object' || seen.has(node)) {
                    return;
                }
                seen.add(node);
                // A node with its own $id (or the document root) starts a new resource.
                let currentRoot = resourceRoot;
                const id = getSchemaId(node);
                if (node === root || (isString(id) && id.charAt(0) !== '#')) {
                    currentRoot = node;
                    if (!currentRoot.$anchorMaps) {
                        const maps = { dynamic: new Map(), local: new Map() };
                        setHidden(currentRoot, '$anchorMaps', maps);
                    }
                }
                const maps = currentRoot.$anchorMaps;
                if (isString(node.$dynamicAnchor)) {
                    if (!maps.dynamic.has(node.$dynamicAnchor)) {
                        maps.dynamic.set(node.$dynamicAnchor, node);
                    }
                    if (!maps.local.has(node.$dynamicAnchor)) {
                        maps.local.set(node.$dynamicAnchor, node);
                    }
                }
                if (isString(node.$anchor) && !maps.local.has(node.$anchor)) {
                    maps.local.set(node.$anchor, node);
                }
                if (isString(node.$dynamicRef)) {
                    dynamicRefInfoOf(node).scope = currentRoot;
                }
                JSONSchemaService.traverseSchemaProperties(node, (childSchema) => {
                    visit(childSchema, currentRoot);
                });
            };
            visit(root, root);
        };
        // Collect and register embedded schemas with $id so they can be resolved as external refs
        // This traversal tracks the current base URI so nested $id values are resolved correctly
        const registerEmbeddedSchemas = (root, baseUri) => {
            const seen = new Set();
            const resolveId = (id, currentBaseUri) => {
                if (contextService && !hasSchemeRegex.test(id)) {
                    return normalizeId(contextService.resolveRelativePath(id, currentBaseUri));
                }
                return normalizeId(id);
            };
            const visit = (node, currentBaseUri) => {
                if (!node || typeof node !== 'object' || seen.has(node)) {
                    return;
                }
                seen.add(node);
                // Check if this node has its own $id that changes the base URI
                const id = getSchemaId(node);
                let newBaseUri = currentBaseUri;
                if (isString(id) && id.charAt(0) !== '#') {
                    const resolvedUri = resolveId(id, currentBaseUri);
                    const existingHandle = this.schemasById[resolvedUri];
                    if (!existingHandle) {
                        this.addSchemaHandle(resolvedUri, node);
                    }
                    else {
                        // Update existing handle with embedded schema content
                        // This ensures embedded schemas take precedence over external schemas
                        existingHandle.setSchemaContent(node);
                    }
                    newBaseUri = resolvedUri;
                }
                // Visit child schemas
                JSONSchemaService.traverseSchemaProperties(node, (childSchema) => {
                    visit(childSchema, newBaseUri);
                });
            };
            visit(root, baseUri);
        };
        // Register embedded schemas before resolving refs
        registerEmbeddedSchemas(schema, handle.uri);
        // Collect anchors eagerly before $ref resolution mutates the schema.
        // resolveRefs merges referenced nodes (including $anchor) into $ref targets,
        // so a lazy collectAnchors call could see duplicates from merged copies.
        handle.anchors = collectAnchors(schema);
        // Collect $dynamicAnchor maps eagerly too, for the same reason: resource
        // boundaries must be read before $ref resolution flattens them.
        collectDynamicAnchors(schema);
        // Resolve meta-schema to extract vocabularies if present
        const resolveMetaschemaVocabularies = () => {
            if (!schema.$schema || typeof schema.$schema !== 'string') {
                return this.promise.resolve(undefined);
            }
            let metaschemaUri = schema.$schema;
            if (contextService && !hasSchemeRegex.test(metaschemaUri)) {
                metaschemaUri = contextService.resolveRelativePath(metaschemaUri, handle.uri);
            }
            const normalizedMetaschemaUri = normalizeId(metaschemaUri);
            const metaschemaHandle = this.getOrAddSchemaHandle(normalizedMetaschemaUri);
            return metaschemaHandle.getUnresolvedSchema().then(unresolvedMetaschema => {
                // Only extract vocabularies if the meta-schema has a $vocabulary property
                // or if it's draft 2019-09 or later which support vocabularies.
                const metaschemaDraft = unresolvedMetaschema.schema.$schema ? getSchemaDraftFromId(unresolvedMetaschema.schema.$schema) : undefined;
                const isDraft2019OrLater = metaschemaDraft && metaschemaDraft >= SchemaDraft.v2019_09;
                const hasVocabulary = unresolvedMetaschema.schema.$vocabulary && typeof unresolvedMetaschema.schema.$vocabulary === 'object';
                if (hasVocabulary || isDraft2019OrLater) {
                    activeVocabularies = extractVocabularies(unresolvedMetaschema.schema);
                }
                return undefined;
            }, () => {
                // If we can't load the meta-schema, proceed without vocabulary info
                return undefined;
            });
        };
        return resolveMetaschemaVocabularies().then(() => {
            return resolveRefs(schema, schema, handle).then(_ => {
                return new ResolvedSchema(schema, resolveErrors, [], schemaDraft, activeVocabularies);
            });
        });
    }
    ;
    getSchemaFromProperty(resource, document) {
        if (document.root?.type === 'object') {
            for (const p of document.root.properties) {
                if (p.keyNode.value === '$schema' && p.valueNode?.type === 'string') {
                    let schemaId = p.valueNode.value;
                    if (this.contextService && !/^\w[\w\d+.-]*:/.test(schemaId)) { // has scheme
                        schemaId = this.contextService.resolveRelativePath(schemaId, resource);
                    }
                    return schemaId;
                }
            }
        }
        return undefined;
    }
    getAssociatedSchemas(resource) {
        const seen = Object.create(null);
        const schemas = [];
        const normalizedResource = normalizeResourceForMatching(resource);
        for (const entry of this.filePatternAssociations) {
            if (entry.matchesPattern(normalizedResource)) {
                for (const schemaId of entry.getURIs()) {
                    if (!seen[schemaId]) {
                        schemas.push(schemaId);
                        seen[schemaId] = true;
                    }
                }
            }
        }
        return schemas;
    }
    getSchemaURIsForResource(resource, document) {
        let schemeId = document && this.getSchemaFromProperty(resource, document);
        if (schemeId) {
            return [schemeId];
        }
        return this.getAssociatedSchemas(resource);
    }
    getSchemaForResource(resource, document) {
        if (document) {
            // first use $schema if present
            let schemeId = this.getSchemaFromProperty(resource, document);
            if (schemeId) {
                const id = normalizeId(schemeId);
                return this.getOrAddSchemaHandle(id).getResolvedSchema();
            }
        }
        if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) {
            return this.cachedSchemaForResource.resolvedSchema;
        }
        const schemas = this.getAssociatedSchemas(resource);
        const resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined);
        this.cachedSchemaForResource = { resource, resolvedSchema };
        return resolvedSchema;
    }
    createCombinedSchema(resource, schemaIds) {
        if (schemaIds.length === 1) {
            return this.getOrAddSchemaHandle(schemaIds[0]);
        }
        else {
            const combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(resource);
            const combinedSchema = {
                allOf: schemaIds.map(schemaId => ({ $ref: schemaId }))
            };
            return this.addSchemaHandle(combinedSchemaId, combinedSchema);
        }
    }
    getMatchingSchemas(document, jsonDocument, schema) {
        if (schema) {
            const id = schema.id || ('schemaservice://untitled/matchingSchemas/' + idCounter++);
            const handle = this.addSchemaHandle(id, schema);
            return handle.getResolvedSchema().then(resolvedSchema => {
                return jsonDocument.getMatchingSchemas(resolvedSchema.schema).filter(s => !s.inverted);
            });
        }
        return this.getSchemaForResource(document.uri, jsonDocument).then(schema => {
            if (schema) {
                return jsonDocument.getMatchingSchemas(schema.schema).filter(s => !s.inverted);
            }
            return [];
        });
    }
}
let idCounter = 0;
function normalizeResourceForMatching(resource) {
    // remove queries and fragments, normalize drive capitalization
    try {
        return URI.parse(resource).with({ fragment: null, query: null }).toString(true);
    }
    catch (e) {
        return resource;
    }
}
function toDisplayString(url) {
    try {
        const uri = URI.parse(url);
        if (uri.scheme === 'file') {
            return uri.fsPath;
        }
    }
    catch (e) {
        // ignore
    }
    return url;
}