UNPKG

vscode-json-languageservice

Version:
1,466 lines 72.1 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 { isNumber, equals, isBoolean, isString, isDefined, isObject } from '../utils/objects.js';
import { extendedRegExp, stringLength } from '../utils/strings.js';
import { ErrorCode, Diagnostic, DiagnosticSeverity, Range, SchemaDraft } from '../jsonLanguageTypes.js';
import { URI } from 'vscode-uri';
import { isKeywordEnabled, isFormatAssertionEnabled } from '../services/vocabularies.js';
import * as l10n from '@vscode/l10n';
const formats = {
    'color-hex': { errorMessage: l10n.t('Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.'), pattern: /^#([0-9A-Fa-f]{3,4}|([0-9A-Fa-f]{2}){3,4})$/ },
    'date-time': { errorMessage: l10n.t('String is not a RFC3339 date-time.'), pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i },
    'date': { errorMessage: l10n.t('String is not a RFC3339 date.'), pattern: /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/i },
    'time': { errorMessage: l10n.t('String is not a RFC3339 time.'), pattern: /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))$/i },
    'duration': { errorMessage: l10n.t('String is not an ISO 8601 duration.'), pattern: /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/ },
    'uuid': { errorMessage: l10n.t('String is not a valid UUID.'), pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i },
    'email': { errorMessage: l10n.t('String is not an e-mail address.'), pattern: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}))$/ },
    'hostname': { errorMessage: l10n.t('String is not a hostname.'), pattern: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i },
    'ipv4': { errorMessage: l10n.t('String is not an IPv4 address.'), pattern: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/ },
    'ipv6': { errorMessage: l10n.t('String is not an IPv6 address.'), pattern: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i },
};
export class ASTNodeImpl {
    constructor(parent, offset, length = 0) {
        this.offset = offset;
        this.length = length;
        this.parent = parent;
    }
    get children() {
        return [];
    }
    toString() {
        return 'type: ' + this.type + ' (' + this.offset + '/' + this.length + ')' + (this.parent ? ' parent: {' + this.parent.toString() + '}' : '');
    }
}
export class NullASTNodeImpl extends ASTNodeImpl {
    constructor(parent, offset) {
        super(parent, offset);
        this.type = 'null';
        this.value = null;
    }
}
export class BooleanASTNodeImpl extends ASTNodeImpl {
    constructor(parent, boolValue, offset) {
        super(parent, offset);
        this.type = 'boolean';
        this.value = boolValue;
    }
}
export class ArrayASTNodeImpl extends ASTNodeImpl {
    constructor(parent, offset) {
        super(parent, offset);
        this.type = 'array';
        this.items = [];
    }
    get children() {
        return this.items;
    }
}
export class NumberASTNodeImpl extends ASTNodeImpl {
    constructor(parent, offset) {
        super(parent, offset);
        this.type = 'number';
        this.isInteger = true;
        this.value = Number.NaN;
    }
}
export class StringASTNodeImpl extends ASTNodeImpl {
    constructor(parent, offset, length) {
        super(parent, offset, length);
        this.type = 'string';
        this.value = '';
    }
}
export class PropertyASTNodeImpl extends ASTNodeImpl {
    constructor(parent, offset, keyNode) {
        super(parent, offset);
        this.type = 'property';
        this.colonOffset = -1;
        this.keyNode = keyNode;
    }
    get children() {
        return this.valueNode ? [this.keyNode, this.valueNode] : [this.keyNode];
    }
}
export class ObjectASTNodeImpl extends ASTNodeImpl {
    constructor(parent, offset) {
        super(parent, offset);
        this.type = 'object';
        this.properties = [];
    }
    get children() {
        return this.properties;
    }
}
export function asSchema(schema) {
    if (isBoolean(schema)) {
        return schema ? {} : { "not": {} };
    }
    return schema;
}
export var EnumMatch;
(function (EnumMatch) {
    EnumMatch[EnumMatch["Key"] = 0] = "Key";
    EnumMatch[EnumMatch["Enum"] = 1] = "Enum";
})(EnumMatch || (EnumMatch = {}));
const httpPrefix = `http://json-schema.org/`;
const httpsPrefix = `https://json-schema.org/`;
export function normalizeId(id) {
    // use the https prefix for the old json-schema.org meta schemas
    // See https://github.com/microsoft/vscode/issues/195189
    if (id.startsWith(httpPrefix)) {
        id = httpsPrefix + id.substring(httpPrefix.length);
    }
    // remove trailing '#', normalize drive capitalization
    try {
        return URI.parse(id).toString(true);
    }
    catch (e) {
        return id;
    }
}
export function getSchemaDraftFromId(schemaId) {
    return schemaDraftFromId[normalizeId(schemaId)] ?? undefined;
}
const schemaDraftFromId = {
    'https://json-schema.org/draft-03/schema': SchemaDraft.v3,
    'https://json-schema.org/draft-04/schema': SchemaDraft.v4,
    'https://json-schema.org/draft-06/schema': SchemaDraft.v6,
    'https://json-schema.org/draft-07/schema': SchemaDraft.v7,
    'https://json-schema.org/draft/2019-09/schema': SchemaDraft.v2019_09,
    'https://json-schema.org/draft/2020-12/schema': SchemaDraft.v2020_12
};
class EvaluationContext {
    constructor(schemaDraft, activeVocabularies, explicitSchemaDraft) {
        this.schemaDraft = schemaDraft;
        this.activeVocabularies = activeVocabularies;
        this.explicitSchemaDraft = explicitSchemaDraft;
    }
}
class SchemaCollector {
    constructor(focusOffset = -1, exclude) {
        this.focusOffset = focusOffset;
        this.exclude = exclude;
        this.schemas = [];
    }
    add(schema) {
        this.schemas.push(schema);
    }
    merge(other) {
        Array.prototype.push.apply(this.schemas, other.schemas);
    }
    include(node) {
        return (this.focusOffset === -1 || contains(node, this.focusOffset)) && (node !== this.exclude);
    }
    newSub() {
        return new SchemaCollector(-1, this.exclude);
    }
}
class NoOpSchemaCollector {
    constructor() { }
    get schemas() { return []; }
    add(_schema) { }
    merge(_other) { }
    include(_node) { return true; }
    newSub() { return this; }
}
NoOpSchemaCollector.instance = new NoOpSchemaCollector();
export class ValidationResult {
    constructor() {
        this.problems = [];
        this.propertiesMatches = 0;
        this.processedProperties = new Set();
        this.propertiesValueMatches = 0;
        this.primaryValueMatches = 0;
        this.enumValueMatch = false;
        this.enumValues = undefined;
    }
    hasProblems() {
        return !!this.problems.length;
    }
    merge(validationResult) {
        this.problems = this.problems.concat(validationResult.problems);
        this.propertiesMatches += validationResult.propertiesMatches;
        this.propertiesValueMatches += validationResult.propertiesValueMatches;
        this.mergeProcessedProperties(validationResult);
    }
    mergeEnumValues(validationResult) {
        if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) {
            this.enumValues = this.enumValues.concat(validationResult.enumValues);
        }
    }
    updateEnumMismatchProblemMessages() {
        if (!this.enumValueMatch && this.enumValues) {
            for (const error of this.problems) {
                if (error.code === ErrorCode.EnumValueMismatch) {
                    error.message = l10n.t('Value is not accepted. Valid values: {0}.', this.enumValues.map(v => JSON.stringify(v)).join(', '));
                }
            }
        }
    }
    mergePropertyMatch(propertyValidationResult) {
        this.problems = this.problems.concat(propertyValidationResult.problems);
        this.propertiesMatches++;
        if (propertyValidationResult.enumValueMatch || !propertyValidationResult.hasProblems() && propertyValidationResult.propertiesMatches) {
            this.propertiesValueMatches++;
        }
        if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues && propertyValidationResult.enumValues.length === 1) {
            this.primaryValueMatches++;
        }
    }
    mergeProcessedProperties(validationResult) {
        validationResult.processedProperties.forEach(p => this.processedProperties.add(p));
    }
    compare(other) {
        const hasProblems = this.hasProblems();
        if (hasProblems !== other.hasProblems()) {
            return hasProblems ? -1 : 1;
        }
        if (this.enumValueMatch !== other.enumValueMatch) {
            return other.enumValueMatch ? -1 : 1;
        }
        if (this.primaryValueMatches !== other.primaryValueMatches) {
            return this.primaryValueMatches - other.primaryValueMatches;
        }
        if (this.propertiesValueMatches !== other.propertiesValueMatches) {
            return this.propertiesValueMatches - other.propertiesValueMatches;
        }
        return this.propertiesMatches - other.propertiesMatches;
    }
}
export function newJSONDocument(root, diagnostics = [], comments = []) {
    return new JSONDocument(root, diagnostics, comments);
}
export function getNodeValue(node) {
    return Json.getNodeValue(node);
}
export function getNodePath(node) {
    return Json.getNodePath(node);
}
export function contains(node, offset, includeRightBound = false) {
    return offset >= node.offset && offset < (node.offset + node.length) || includeRightBound && offset === (node.offset + node.length);
}
export class JSONDocument {
    constructor(root, syntaxErrors = [], comments = []) {
        this.root = root;
        this.syntaxErrors = syntaxErrors;
        this.comments = comments;
    }
    getNodeFromOffset(offset, includeRightBound = false) {
        if (this.root) {
            return Json.findNodeAtOffset(this.root, offset, includeRightBound);
        }
        return undefined;
    }
    visit(visitor) {
        if (this.root) {
            const doVisit = (node) => {
                let ctn = visitor(node);
                const children = node.children;
                if (Array.isArray(children)) {
                    for (let i = 0; i < children.length && ctn; i++) {
                        ctn = doVisit(children[i]);
                    }
                }
                return ctn;
            };
            doVisit(this.root);
        }
    }
    validate(textDocument, schema, severity = DiagnosticSeverity.Warning, schemaDraft, activeVocabularies) {
        if (this.root && schema) {
            const validationResult = new ValidationResult();
            const { explicitDraft, effectiveDraft } = resolveDrafts(schema, schemaDraft);
            const context = new EvaluationContext(effectiveDraft, activeVocabularies, explicitDraft);
            const schemaStack = [];
            const schemaRoots = [schema];
            validate(this.root, schema, validationResult, NoOpSchemaCollector.instance, context, schemaStack, schemaRoots);
            return validationResult.problems.map(p => {
                const range = Range.create(textDocument.positionAt(p.location.offset), textDocument.positionAt(p.location.offset + p.location.length));
                return Diagnostic.create(range, p.message, p.severity ?? severity, p.code);
            });
        }
        return undefined;
    }
    getMatchingSchemas(schema, focusOffset = -1, exclude, activeVocabularies) {
        if (this.root && schema) {
            const matchingSchemas = new SchemaCollector(focusOffset, exclude);
            const { explicitDraft, effectiveDraft } = resolveDrafts(schema);
            const context = new EvaluationContext(effectiveDraft, activeVocabularies, explicitDraft);
            const schemaStack = [];
            const schemaRoots = [schema];
            validate(this.root, schema, new ValidationResult(), matchingSchemas, context, schemaStack, schemaRoots);
            return matchingSchemas.schemas;
        }
        return [];
    }
}
/*
 * Resolves the schema draft versions for validation.
 * - explicitDraft: the draft explicitly declared via $schema or passed as an override.
 *   Undefined when no $schema is present and no override is provided.
 * - effectiveDraft: the draft used for keyword gating, defaulting to 2020-12.
 * These are tracked separately so that behaviors like format assertion can
 * distinguish "explicitly 2020-12" (annotation-only per spec) from
 * "no $schema, defaulting to 2020-12" (asserts for backward compatibility).
 */
function resolveDrafts(schema, schemaDraftOverride) {
    const explicitDraft = schemaDraftOverride ?? (schema.$schema ? getSchemaDraftFromId(schema.$schema) : undefined);
    const effectiveDraft = explicitDraft ?? SchemaDraft.v2020_12;
    return { explicitDraft, effectiveDraft };
}
// Keywords introduced in 2019-09 (not available in draft-07 and earlier)
const keywords201909 = new Set([
    'dependentRequired', 'dependentSchemas',
    'unevaluatedProperties', 'unevaluatedItems',
    'minContains', 'maxContains'
]);
function validate(n, schema, validationResult, matchingSchemas, context, schemaStack, schemaRoots) {
    if (!n || !matchingSchemas.include(n)) {
        return;
    }
    if (n.type === 'property') {
        return validate(n.valueNode, schema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
    }
    const node = n;
    if (schema.$recursiveRef) {
        const hasRecursiveAnchor = (s) => s.$recursiveAnchor === true;
        const isSchemaRoot = (s) => s.$id || s.id || s.$originalId;
        // Find nearest schema resource root
        const currentResourceRoot = schemaStack.slice().reverse().find(isSchemaRoot);
        // If current resource has $recursiveAnchor, find first $recursiveAnchor in stack
        // Otherwise use current resource root or fall back to document root
        const targetSchema = (currentResourceRoot && hasRecursiveAnchor(currentResourceRoot))
            ? schemaStack.find(hasRecursiveAnchor)
            : currentResourceRoot ?? schemaRoots[0];
        // Validate against the target schema (avoiding infinite loop).
        // Fall through afterwards so sibling keywords (e.g. unevaluatedProperties)
        // are still evaluated against this node, accumulating processedProperties.
        if (targetSchema && targetSchema !== schema) {
            validate(node, targetSchema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
        }
    }
    // Track schema document roots (schemas with $id or $originalId) if not already the root
    const isNewRoot = (schema.$id || schema.id || schema.$originalId) && !schemaRoots.includes(schema);
    if (isNewRoot) {
        schemaRoots.push(schema);
    }
    // Push current schema to stack for $recursiveRef resolution
    schemaStack.push(schema);
    // 2020-12 $dynamicRef. The initial (static) target and the referenced plain-name
    // fragment were recorded during schema resolution in $dynamicRefInfo (target and
    // name). "Bookending": dynamic resolution applies only when that
    // initial target is itself a $dynamicAnchor of the referenced name; then the
    // reference resolves to the outermost matching $dynamicAnchor currently in the
    // dynamic scope (schemaRoots, ordered outermost-first). Otherwise it behaves
    // like a plain $ref to the initial target. This runs after the schemaRoots push
    // so the resource that contains the $dynamicRef participates in its own
    // dynamic-scope resolution.
    const dynamicRefInfo = schema.$dynamicRefInfo;
    const dynamicRefTarget = dynamicRefInfo?.target;
    if (dynamicRefTarget !== undefined) {
        let targetSchema = dynamicRefTarget;
        const dynamicName = dynamicRefInfo?.name;
        if (dynamicName !== undefined && dynamicRefTarget.$dynamicAnchor === dynamicName) {
            for (const root of schemaRoots) {
                const candidate = root.$anchorMaps?.dynamic.get(dynamicName);
                if (candidate) {
                    targetSchema = candidate;
                    break;
                }
            }
        }
        // Validate the instance against the referenced schema, then fall through so
        // that any sibling keywords on this same node are validated too: per 2020-12
        // $dynamicRef is an applicator that applies alongside its siblings (like $ref),
        // not a redirect that replaces them. The shared cleanup below pops the stack.
        if (targetSchema && targetSchema !== schema) {
            validate(node, targetSchema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
        }
    }
    const enabled = (keyword) => {
        // Draft-based keyword gating: keywords only exist in certain drafts.
        // 'dependencies' was replaced by dependentRequired/dependentSchemas in 2019-09
        if (keyword === 'dependencies') {
            return context.schemaDraft <= SchemaDraft.v7;
        }
        // Keywords introduced in 2019-09 (not available in draft-07 and earlier)
        if (keywords201909.has(keyword)) {
            return context.schemaDraft >= SchemaDraft.v2019_09;
        }
        // 'prefixItems' was introduced in 2020-12
        if (keyword === 'prefixItems') {
            return context.schemaDraft >= SchemaDraft.v2020_12;
        }
        // Vocabulary-based filtering only applies for 2019-09+ (vocabulary is not a concept in older drafts)
        if (context.schemaDraft >= SchemaDraft.v2019_09 && context.activeVocabularies) {
            return isKeywordEnabled(keyword, context.activeVocabularies);
        }
        // No vocabulary info or pre-2019-09: enable all draft-appropriate keywords
        return true;
    };
    _validateNode();
    switch (node.type) {
        case 'object':
            _validateObjectNode(node);
            break;
        case 'array':
            _validateArrayNode(node);
            break;
        case 'string':
            _validateStringNode(node);
            break;
        case 'number':
            _validateNumberNode(node);
            break;
    }
    matchingSchemas.add({ node: node, schema: schema });
    // Pop schema from stack when done
    schemaStack.pop();
    // Pop schema root if we pushed one
    if (isNewRoot) {
        schemaRoots.pop();
    }
    function _validateNode() {
        function matchesType(type) {
            return node.type === type || (type === 'integer' && node.type === 'number' && node.isInteger);
        }
        if (Array.isArray(schema.type) && enabled('type')) {
            if (!schema.type.some(matchesType)) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.errorMessage || l10n.t('Incorrect type. Expected one of {0}.', schema.type.join(', '))
                });
            }
        }
        else if (schema.type && !Array.isArray(schema.type) && enabled('type')) {
            if (!matchesType(schema.type)) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.errorMessage || l10n.t('Incorrect type. Expected "{0}".', schema.type)
                });
            }
        }
        if (Array.isArray(schema.allOf) && enabled('allOf')) {
            for (const subSchemaRef of schema.allOf) {
                const subValidationResult = new ValidationResult();
                const subMatchingSchemas = matchingSchemas.newSub();
                validate(node, asSchema(subSchemaRef), subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
                validationResult.merge(subValidationResult);
                matchingSchemas.merge(subMatchingSchemas);
            }
        }
        const notSchema = asSchema(schema.not);
        if (notSchema && enabled('not')) {
            const subValidationResult = new ValidationResult();
            const subMatchingSchemas = matchingSchemas.newSub();
            validate(node, notSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
            if (!subValidationResult.hasProblems()) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.errorMessage || l10n.t("Matches a schema that is not allowed.")
                });
            }
            for (const ms of subMatchingSchemas.schemas) {
                ms.inverted = !ms.inverted;
                matchingSchemas.add(ms);
            }
        }
        const testAlternatives = (alternatives, maxOneMatch) => {
            const matches = [];
            const alternativesToTest = _tryDiscriminatorOptimization(alternatives) ?? alternatives;
            // remember the best match that is used for error messages
            let bestMatch = undefined;
            for (const subSchemaRef of alternativesToTest) {
                const subSchema = asSchema(subSchemaRef);
                const subValidationResult = new ValidationResult();
                const subMatchingSchemas = matchingSchemas.newSub();
                validate(node, subSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
                if (!subValidationResult.hasProblems()) {
                    matches.push(subSchema);
                }
                if (!bestMatch) {
                    bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
                }
                else {
                    if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) {
                        // no errors, both are equally good matches
                        bestMatch.matchingSchemas.merge(subMatchingSchemas);
                        bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
                        bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
                        bestMatch.validationResult.mergeProcessedProperties(subValidationResult);
                    }
                    else {
                        const compareResult = subValidationResult.compare(bestMatch.validationResult);
                        if (compareResult > 0) {
                            // our node is the best matching so far
                            bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
                        }
                        else if (compareResult === 0) {
                            // there's already a best matching but we are as good
                            bestMatch.matchingSchemas.merge(subMatchingSchemas);
                            bestMatch.validationResult.mergeEnumValues(subValidationResult);
                        }
                    }
                }
            }
            if (matches.length > 1 && maxOneMatch) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: 1 },
                    message: l10n.t("Matches multiple schemas when only one must validate.")
                });
            }
            if (bestMatch) {
                bestMatch.validationResult.updateEnumMismatchProblemMessages();
                validationResult.merge(bestMatch.validationResult);
                matchingSchemas.merge(bestMatch.matchingSchemas);
            }
            return matches.length;
        };
        if (Array.isArray(schema.anyOf) && enabled('anyOf')) {
            testAlternatives(schema.anyOf, false);
        }
        if (Array.isArray(schema.oneOf) && enabled('oneOf')) {
            testAlternatives(schema.oneOf, true);
        }
        const testBranch = (schema) => {
            const subValidationResult = new ValidationResult();
            const subMatchingSchemas = matchingSchemas.newSub();
            validate(node, asSchema(schema), subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
            validationResult.merge(subValidationResult);
            matchingSchemas.merge(subMatchingSchemas);
        };
        const testCondition = (ifSchema, thenSchema, elseSchema) => {
            const subSchema = asSchema(ifSchema);
            const subValidationResult = new ValidationResult();
            const subMatchingSchemas = matchingSchemas.newSub();
            validate(node, subSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
            matchingSchemas.merge(subMatchingSchemas);
            // if passed: merge properties from if;
            // otherwise don't merge properties from if, only from else
            if (!subValidationResult.hasProblems()) {
                validationResult.mergeProcessedProperties(subValidationResult);
                if (thenSchema) {
                    testBranch(thenSchema);
                }
            }
            else if (elseSchema) {
                testBranch(elseSchema);
            }
        };
        const ifSchema = asSchema(schema.if);
        if (ifSchema && enabled('if')) {
            testCondition(ifSchema, asSchema(schema.then), asSchema(schema.else));
        }
        if (Array.isArray(schema.enum) && enabled('enum')) {
            const val = getNodeValue(node);
            let enumValueMatch = false;
            for (const e of schema.enum) {
                if (equals(val, e)) {
                    enumValueMatch = true;
                    break;
                }
            }
            validationResult.enumValues = schema.enum;
            validationResult.enumValueMatch = enumValueMatch;
            if (!enumValueMatch) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    code: ErrorCode.EnumValueMismatch,
                    message: schema.errorMessage || l10n.t('Value is not accepted. Valid values: {0}.', schema.enum.map(v => JSON.stringify(v)).join(', '))
                });
            }
        }
        if (isDefined(schema.const) && enabled('const')) {
            const val = getNodeValue(node);
            if (!equals(val, schema.const)) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    code: ErrorCode.EnumValueMismatch,
                    message: schema.errorMessage || l10n.t('Value must be {0}.', JSON.stringify(schema.const))
                });
                validationResult.enumValueMatch = false;
            }
            else {
                validationResult.enumValueMatch = true;
            }
            validationResult.enumValues = [schema.const];
        }
        let deprecationMessage = schema.deprecationMessage;
        if (deprecationMessage || schema.deprecated) {
            deprecationMessage = deprecationMessage || l10n.t('Value is deprecated');
            let targetNode = node.parent?.type === 'property' ? node.parent : node;
            validationResult.problems.push({
                location: { offset: targetNode.offset, length: targetNode.length },
                severity: DiagnosticSeverity.Warning,
                message: deprecationMessage,
                code: ErrorCode.Deprecated
            });
        }
    }
    function _tryDiscriminatorOptimization(alternatives) {
        if (alternatives.length < 2) {
            return undefined;
        }
        const buildConstMap = (getSchemas) => {
            const constMap = new Map();
            for (let i = 0; i < alternatives.length; i++) {
                const schemas = getSchemas(asSchema(alternatives[i]), i);
                if (!schemas) {
                    return undefined; // Early exit if any alternative can't be processed
                }
                schemas.forEach(([key, schema]) => {
                    if (schema.const !== undefined) {
                        if (!constMap.has(key)) {
                            constMap.set(key, new Map());
                        }
                        const valueMap = constMap.get(key);
                        if (!valueMap.has(schema.const)) {
                            valueMap.set(schema.const, []);
                        }
                        valueMap.get(schema.const).push(i);
                    }
                    else if (schema.enum && Array.isArray(schema.enum)) {
                        for (const enumValue of schema.enum) {
                            if (!constMap.has(key)) {
                                constMap.set(key, new Map());
                            }
                            const valueMap = constMap.get(key);
                            if (!valueMap.has(enumValue)) {
                                valueMap.set(enumValue, []);
                            }
                            valueMap.get(enumValue).push(i);
                        }
                    }
                });
            }
            return constMap;
        };
        const findDiscriminator = (constMap, getValue) => {
            for (const [key, valueMap] of constMap) {
                const coveredAlts = new Set();
                valueMap.forEach(indices => indices.forEach(idx => coveredAlts.add(idx)));
                if (coveredAlts.size === alternatives.length) {
                    const discriminatorValue = getValue(key);
                    const matchingIndices = valueMap.get(discriminatorValue);
                    if (matchingIndices?.length) {
                        return matchingIndices.map(idx => alternatives[idx]);
                    }
                    break; // Found valid discriminator but no match
                }
            }
            return undefined;
        };
        if (node.type === 'object' && node.properties?.length) {
            const constMap = buildConstMap((schema) => schema.properties ? Object.entries(schema.properties).map(([k, v]) => [k, asSchema(v)]) : undefined);
            if (constMap) {
                return findDiscriminator(constMap, (propName) => {
                    const prop = node.properties.find(p => p.keyNode.value === propName);
                    return prop?.valueNode?.type === 'string' ? prop.valueNode.value : undefined;
                });
            }
        }
        else if (node.type === 'array' && node.items?.length) {
            const constMap = buildConstMap((schema) => {
                const itemSchemas = schema.prefixItems || (Array.isArray(schema.items) ? schema.items : undefined);
                return itemSchemas ? itemSchemas.map((item, idx) => [idx, asSchema(item)]) : undefined;
            });
            if (constMap) {
                return findDiscriminator(constMap, (itemIndex) => {
                    const item = node.items[itemIndex];
                    return item?.type === 'string' ? item.value : undefined;
                });
            }
        }
        return undefined;
    }
    function _validateNumberNode(node) {
        const val = node.value;
        function normalizeFloats(float) {
            const parts = /^(-?\d+)(?:\.(\d+))?(?:e([-+]\d+))?$/.exec(float.toString());
            return parts && {
                value: Number(parts[1] + (parts[2] || '')),
                multiplier: (parts[2]?.length || 0) - (parseInt(parts[3]) || 0)
            };
        }
        ;
        if (isNumber(schema.multipleOf) && enabled('multipleOf')) {
            let remainder = -1;
            if (Number.isInteger(schema.multipleOf)) {
                remainder = val % schema.multipleOf;
            }
            else {
                let normMultipleOf = normalizeFloats(schema.multipleOf);
                let normValue = normalizeFloats(val);
                if (normMultipleOf && normValue) {
                    const multiplier = 10 ** Math.abs(normValue.multiplier - normMultipleOf.multiplier);
                    if (normValue.multiplier < normMultipleOf.multiplier) {
                        normValue.value *= multiplier;
                    }
                    else {
                        normMultipleOf.value *= multiplier;
                    }
                    remainder = normValue.value % normMultipleOf.value;
                }
            }
            if (remainder !== 0) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: l10n.t('Value is not divisible by {0}.', schema.multipleOf)
                });
            }
        }
        function getExclusiveLimit(limit, exclusive) {
            if (isNumber(exclusive)) {
                return exclusive;
            }
            if (isBoolean(exclusive) && exclusive) {
                return limit;
            }
            return undefined;
        }
        function getLimit(limit, exclusive) {
            if (!isBoolean(exclusive) || !exclusive) {
                return limit;
            }
            return undefined;
        }
        const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum);
        if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum && enabled('exclusiveMinimum')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('Value is below the exclusive minimum of {0}.', exclusiveMinimum)
            });
        }
        const exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum);
        if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum && enabled('exclusiveMaximum')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('Value is above the exclusive maximum of {0}.', exclusiveMaximum)
            });
        }
        const minimum = getLimit(schema.minimum, schema.exclusiveMinimum);
        if (isNumber(minimum) && val < minimum && enabled('minimum')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('Value is below the minimum of {0}.', minimum)
            });
        }
        const maximum = getLimit(schema.maximum, schema.exclusiveMaximum);
        if (isNumber(maximum) && val > maximum && enabled('maximum')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('Value is above the maximum of {0}.', maximum)
            });
        }
    }
    function _validateStringNode(node) {
        if (isNumber(schema.minLength) && stringLength(node.value) < schema.minLength && enabled('minLength')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('String is shorter than the minimum length of {0}.', schema.minLength)
            });
        }
        if (isNumber(schema.maxLength) && stringLength(node.value) > schema.maxLength && enabled('maxLength')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('String is longer than the maximum length of {0}.', schema.maxLength)
            });
        }
        if (isString(schema.pattern) && enabled('pattern')) {
            const regex = extendedRegExp(schema.pattern);
            if (regex && !(regex.test(node.value))) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.patternErrorMessage || schema.errorMessage || l10n.t('String does not match the pattern of "{0}".', schema.pattern)
                });
            }
        }
        // Only validate format if format-assertion vocabulary is active (not annotation-only)
        if (schema.format && enabled('format') && isFormatAssertionEnabled(context.activeVocabularies, context.explicitSchemaDraft)) {
            switch (schema.format) {
                case 'uri':
                case 'uri-reference':
                    {
                        let errorMessage;
                        if (!node.value) {
                            errorMessage = l10n.t('URI expected.');
                        }
                        else {
                            const match = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/.exec(node.value);
                            if (!match) {
                                errorMessage = l10n.t('URI is expected.');
                            }
                            else if (!match[2] && schema.format === 'uri') {
                                errorMessage = l10n.t('URI with a scheme is expected.');
                            }
                        }
                        if (errorMessage) {
                            validationResult.problems.push({
                                location: { offset: node.offset, length: node.length },
                                message: schema.patternErrorMessage || schema.errorMessage || l10n.t('String is not a URI: {0}', errorMessage)
                            });
                        }
                    }
                    break;
                case 'color-hex':
                case 'date-time':
                case 'date':
                case 'time':
                case 'duration':
                case 'uuid':
                case 'email':
                case 'hostname':
                case 'ipv4':
                case 'ipv6':
                    const format = formats[schema.format];
                    if (!node.value || !format.pattern.exec(node.value)) {
                        validationResult.problems.push({
                            location: { offset: node.offset, length: node.length },
                            message: schema.patternErrorMessage || schema.errorMessage || format.errorMessage
                        });
                    }
                default:
            }
        }
    }
    function _validateArrayNode(node) {
        let prefixItemsSchemas;
        let additionalItemSchema;
        if (context.schemaDraft >= SchemaDraft.v2020_12) {
            prefixItemsSchemas = schema.prefixItems;
            additionalItemSchema = !Array.isArray(schema.items) ? schema.items : undefined;
        }
        else {
            prefixItemsSchemas = Array.isArray(schema.items) ? schema.items : undefined;
            additionalItemSchema = !Array.isArray(schema.items) ? schema.items : schema.additionalItems;
        }
        let index = 0;
        if (prefixItemsSchemas !== undefined) {
            const max = Math.min(prefixItemsSchemas.length, node.items.length);
            for (; index < max; index++) {
                const subSchemaRef = prefixItemsSchemas[index];
                const subSchema = asSchema(subSchemaRef);
                const itemValidationResult = new ValidationResult();
                const item = node.items[index];
                if (item) {
                    validate(item, subSchema, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                    validationResult.mergePropertyMatch(itemValidationResult);
                }
                validationResult.processedProperties.add(String(index));
            }
        }
        if (additionalItemSchema !== undefined && index < node.items.length) {
            if (typeof additionalItemSchema === 'boolean') {
                if (additionalItemSchema === false) {
                    validationResult.problems.push({
                        location: { offset: node.offset, length: node.length },
                        message: l10n.t('Array has too many items according to schema. Expected {0} or fewer.', index)
                    });
                }
                for (; index < node.items.length; index++) {
                    validationResult.processedProperties.add(String(index));
                    validationResult.propertiesValueMatches++;
                }
            }
            else {
                for (; index < node.items.length; index++) {
                    const itemValidationResult = new ValidationResult();
                    validate(node.items[index], additionalItemSchema, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                    validationResult.mergePropertyMatch(itemValidationResult);
                    validationResult.processedProperties.add(String(index));
                }
            }
        }
        const containsSchema = asSchema(schema.contains);
        if (containsSchema && enabled('contains')) {
            let containsCount = 0;
            for (let index = 0; index < node.items.length; index++) {
                const item = node.items[index];
                const itemValidationResult = new ValidationResult();
                validate(item, containsSchema, itemValidationResult, NoOpSchemaCollector.instance, context, schemaStack, schemaRoots);
                if (!itemValidationResult.hasProblems()) {
                    containsCount++;
                    if (context.schemaDraft >= SchemaDraft.v2020_12) {
                        validationResult.processedProperties.add(String(index));
                    }
                }
            }
            if (containsCount === 0 && !isNumber(schema.minContains)) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.errorMessage || l10n.t('Array does not contain required item.')
                });
            }
            if (isNumber(schema.minContains) && containsCount < schema.minContains && enabled('minContains')) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.errorMessage || l10n.t('Array has too few items that match the contains contraint. Expected {0} or more.', schema.minContains)
                });
            }
            if (isNumber(schema.maxContains) && containsCount > schema.maxContains && enabled('maxContains')) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: schema.errorMessage || l10n.t('Array has too many items that match the contains contraint. Expected {0} or less.', schema.maxContains)
                });
            }
        }
        const unevaluatedItems = schema.unevaluatedItems;
        if (unevaluatedItems !== undefined && enabled('unevaluatedItems')) {
            for (let i = 0; i < node.items.length; i++) {
                if (!validationResult.processedProperties.has(String(i))) {
                    if (unevaluatedItems === false) {
                        validationResult.problems.push({
                            location: { offset: node.offset, length: node.length },
                            message: l10n.t('Item does not match any validation rule from the array.')
                        });
                    }
                    else {
                        const itemValidationResult = new ValidationResult();
                        validate(node.items[i], schema.unevaluatedItems, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                        validationResult.mergePropertyMatch(itemValidationResult);
                    }
                }
                validationResult.processedProperties.add(String(i));
                validationResult.propertiesValueMatches++;
            }
        }
        if (isNumber(schema.minItems) && node.items.length < schema.minItems && enabled('minItems')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('Array has too few items. Expected {0} or more.', schema.minItems)
            });
        }
        if (isNumber(schema.maxItems) && node.items.length > schema.maxItems && enabled('maxItems')) {
            validationResult.problems.push({
                location: { offset: node.offset, length: node.length },
                message: l10n.t('Array has too many items. Expected {0} or fewer.', schema.maxItems)
            });
        }
        if (schema.uniqueItems === true && enabled('uniqueItems')) {
            const values = getNodeValue(node);
            function hasDuplicates() {
                for (let i = 0; i < values.length - 1; i++) {
                    const value = values[i];
                    for (let j = i + 1; j < values.length; j++) {
                        if (equals(value, values[j])) {
                            return true;
                        }
                    }
                }
                return false;
            }
            if (hasDuplicates()) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: l10n.t('Array has duplicate items.')
                });
            }
        }
    }
    function _validateObjectNode(node) {
        const seenKeys = Object.create(null);
        const unprocessedProperties = new Set();
        for (const propertyNode of node.properties) {
            const key = propertyNode.keyNode.value;
            seenKeys[key] = propertyNode.valueNode;
            unprocessedProperties.add(key);
        }
        if (Array.isArray(schema.required) && enabled('required')) {
            for (const propertyName of schema.required) {
                if (!seenKeys[propertyName]) {
                    validationResult.problems.push({
                        location: { offset: node.offset, length: node.length },
                        message: l10n.t('Missing property "{0}".', propertyName)
                    });
                }
            }
        }
        const propertyProcessed = (prop) => {
            unprocessedProperties.delete(prop);
            validationResult.processedProperties.add(prop);
        };
        if (schema.properties && enabled('properties')) {
            for (const propertyName of Object.keys(schema.properties)) {
                propertyProcessed(propertyName);
                const propertySchema = schema.properties[propertyName];
                const child = seenKeys[propertyName];
                if (child) {
                    if (isBoolean(propertySchema)) {
                        if (!propertySchema) {
                            const propertyNode = child.parent;
                            validationResult.problems.push({
                                location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
                                message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
                            });
                        }
                        else {
                            validationResult.propertiesMatches++;
                            validationResult.propertiesValueMatches++;
                        }
                    }
                    else {
                        const propertyValidationResult = new ValidationResult();
                        validate(child, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                        validationResult.mergePropertyMatch(propertyValidationResult);
                    }
                }
            }
        }
        if (schema.patternProperties && enabled('patternProperties')) {
            // Collect all properties matched by any pattern first
            // A property must be validated against all patterns it matches
            // Properties in `properties` can also match `patternProperties`, check all keys
            const allPropertyNames = Object.keys(seenKeys);
            const patternPropertyMatches = new Set();
            for (const propertyPattern of Object.keys(schema.patternProperties)) {
                const regex = extendedRegExp(propertyPattern);
                if (regex) {
                    for (const propertyName of allPropertyNames) {
                        if (regex.test(propertyName)) {
                            // Only mark as pattern match if not already in `properties`
                            // for additionalProperties tracking
                            if (unprocessedProperties.has(propertyName)) {
                                patternPropertyMatches.add(propertyName);
                            }
                            const child = seenKeys[propertyName];
                            if (child) {
                                const propertySchema = schema.patternProperties[propertyPattern];
                                if (isBoolean(propertySchema)) {
                                    if (!propertySchema) {
                                        const propertyNode = child.parent;
                                        validationResult.problems.push({
                                            location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
                                            message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
                                        });
                                    }
                                    else {
                                        validationResult.propertiesMatches++;
                                        validationResult.propertiesValueMatches++;
                                    }
                                }
                                else {
                                    const propertyValidationResult = new ValidationResult();
                                    validate(child, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                                    validationResult.mergePropertyMatch(propertyValidationResult);
                                }
                            }
                        }
                    }
                }
            }
            // Mark all matched properties as processed after all patterns have been checked
            patternPropertyMatches.forEach(propertyProcessed);
        }
        const additionalProperties = schema.additionalProperties;
        if (additionalProperties !== undefined && enabled('additionalProperties')) {
            for (const propertyName of unprocessedProperties) {
                propertyProcessed(propertyName);
                const child = seenKeys[propertyName];
                if (child) {
                    if (additionalProperties === false) {
                        const propertyNode = child.parent;
                        validationResult.problems.push({
                            location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
                            message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
                        });
                    }
                    else if (additionalProperties !== true) {
                        const propertyValidationResult = new ValidationResult();
                        validate(child, additionalProperties, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                        validationResult.mergePropertyMatch(propertyValidationResult);
                    }
                }
            }
        }
        if (schema.dependentRequired && enabled('dependentRequired')) {
            for (const key in schema.dependentRequired) {
                const prop = seenKeys[key];
                const propertyDeps = schema.dependentRequired[key];
                if (prop && Array.isArray(propertyDeps)) {
                    _validatePropertyDependencies(key, propertyDeps);
                }
            }
        }
        if (schema.dependentSchemas && enabled('dependentSchemas')) {
            for (const key in schema.dependentSchemas) {
                const prop = seenKeys[key];
                const propertyDeps = schema.dependentSchemas[key];
                if (prop) {
                    _validatePropertyDependencies(key, propertyDeps);
                }
            }
        }
        const unevaluatedProperties = schema.unevaluatedProperties;
        if (unevaluatedProperties !== undefined && enabled('unevaluatedProperties')) {
            const processed = [];
            for (const propertyName of unprocessedProperties) {
                if (!validationResult.processedProperties.has(propertyName)) {
                    processed.push(propertyName);
                    const child = seenKeys[propertyName];
                    if (child) {
                        if (unevaluatedProperties === false) {
                            const propertyNode = child.parent;
                            validationResult.problems.push({
                                location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
                                message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
                            });
                        }
                        else if (unevaluatedProperties !== true) {
                            const propertyValidationResult = new ValidationResult();
                            validate(child, unevaluatedProperties, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                            validationResult.mergePropertyMatch(propertyValidationResult);
                        }
                    }
                }
            }
            processed.forEach(propertyProcessed);
        }
        if (isNumber(schema.maxProperties) && enabled('maxProperties')) {
            if (node.properties.length > schema.maxProperties) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: l10n.t('Object has more properties than limit of {0}.', schema.maxProperties)
                });
            }
        }
        if (isNumber(schema.minProperties) && enabled('minProperties')) {
            if (node.properties.length < schema.minProperties) {
                validationResult.problems.push({
                    location: { offset: node.offset, length: node.length },
                    message: l10n.t('Object has fewer properties than the required number of {0}', schema.minProperties)
                });
            }
        }
        if (schema.dependencies && enabled('dependencies')) {
            for (const key in schema.dependencies) {
                const prop = seenKeys[key];
                if (prop) {
                    _validatePropertyDependencies(key, schema.dependencies[key]);
                }
            }
        }
        const propertyNames = asSchema(schema.propertyNames);
        if (propertyNames && enabled('propertyNames')) {
            for (const f of node.properties) {
                const key = f.keyNode;
                if (key) {
                    validate(key, propertyNames, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
                }
            }
        }
        function _validatePropertyDependencies(key, propertyDep) {
            if (Array.isArray(propertyDep)) {
                for (const requiredProp of propertyDep) {
                    if (!seenKeys[requiredProp]) {
                        validationResult.problems.push({
                            location: { offset: node.offset, length: node.length },
                            message: l10n.t('Object is missing property {0} required by property {1}.', requiredProp, key)
                        });
                    }
                    else {
                        validationResult.propertiesValueMatches++;
                    }
                }
            }
            else {
                const propertySchema = asSchema(propertyDep);
                if (propertySchema) {
                    const propertyValidationResult = new ValidationResult();
                    validate(node, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
                    validationResult.mergePropertyMatch(propertyValidationResult);
                    validationResult.mergeProcessedProperties(propertyValidationResult);
                }
            }
        }
    }
}
export function parse(textDocument, config) {
    const problems = [];
    let lastProblemOffset = -1;
    const text = textDocument.getText();
    const scanner = Json.createScanner(text, false);
    const commentRanges = config && config.collectComments ? [] : undefined;
    function _scanNext() {
        while (true) {
            const token = scanner.scan();
            _checkScanError();
            switch (token) {
                case 12 /* Json.SyntaxKind.LineCommentTrivia */:
                case 13 /* Json.SyntaxKind.BlockCommentTrivia */:
                    if (Array.isArray(commentRanges)) {
                        commentRanges.push(Range.create(textDocument.positionAt(scanner.getTokenOffset()), textDocument.positionAt(scanner.getTokenOffset() + scanner.getTokenLength())));
                    }
                    break;
                case 15 /* Json.SyntaxKind.Trivia */:
                case 14 /* Json.SyntaxKind.LineBreakTrivia */:
                    break;
                default:
                    return token;
            }
        }
    }
    function _accept(token) {
        if (scanner.getToken() === token) {
            _scanNext();
            return true;
        }
        return false;
    }
    function _errorAtRange(message, code, startOffset, endOffset, severity = DiagnosticSeverity.Error) {
        if (problems.length === 0 || startOffset !== lastProblemOffset) {
            const range = Range.create(textDocument.positionAt(startOffset), textDocument.positionAt(endOffset));
            problems.push(Diagnostic.create(range, message, severity, code, textDocument.languageId));
            lastProblemOffset = startOffset;
        }
    }
    function _error(message, code, node = undefined, skipUntilAfter = [], skipUntil = []) {
        let start = scanner.getTokenOffset();
        let end = scanner.getTokenOffset() + scanner.getTokenLength();
        if (start === end && start > 0) {
            start--;
            while (start > 0 && /\s/.test(text.charAt(start))) {
                start--;
            }
            end = start + 1;
        }
        _errorAtRange(message, code, start, end);
        if (node) {
            _finalize(node, false);
        }
        if (skipUntilAfter.length + skipUntil.length > 0) {
            let token = scanner.getToken();
            while (token !== 17 /* Json.SyntaxKind.EOF */) {
                if (skipUntilAfter.indexOf(token) !== -1) {
                    _scanNext();
                    break;
                }
                else if (skipUntil.indexOf(token) !== -1) {
                    break;
                }
                token = _scanNext();
            }
        }
        return node;
    }
    function _checkScanError() {
        switch (scanner.getTokenError()) {
            case 4 /* Json.ScanError.InvalidUnicode */:
                _error(l10n.t('Invalid unicode sequence in string.'), ErrorCode.InvalidUnicode);
                return true;
            case 5 /* Json.ScanError.InvalidEscapeCharacter */:
                _error(l10n.t('Invalid escape character in string.'), ErrorCode.InvalidEscapeCharacter);
                return true;
            case 3 /* Json.ScanError.UnexpectedEndOfNumber */:
                _error(l10n.t('Unexpected end of number.'), ErrorCode.UnexpectedEndOfNumber);
                return true;
            case 1 /* Json.ScanError.UnexpectedEndOfComment */:
                _error(l10n.t('Unexpected end of comment.'), ErrorCode.UnexpectedEndOfComment);
                return true;
            case 2 /* Json.ScanError.UnexpectedEndOfString */:
                _error(l10n.t('Unexpected end of string.'), ErrorCode.UnexpectedEndOfString);
                return true;
            case 6 /* Json.ScanError.InvalidCharacter */:
                _error(l10n.t('Invalid characters in string. Control characters must be escaped.'), ErrorCode.InvalidCharacter);
                return true;
        }
        return false;
    }
    function _finalize(node, scanNext) {
        node.length = scanner.getTokenOffset() + scanner.getTokenLength() - node.offset;
        if (scanNext) {
            _scanNext();
        }
        return node;
    }
    function _parseArray(parent) {
        if (scanner.getToken() !== 3 /* Json.SyntaxKind.OpenBracketToken */) {
            return undefined;
        }
        const node = new ArrayASTNodeImpl(parent, scanner.getTokenOffset());
        _scanNext(); // consume OpenBracketToken
        const count = 0;
        let needsComma = false;
        while (scanner.getToken() !== 4 /* Json.SyntaxKind.CloseBracketToken */ && scanner.getToken() !== 17 /* Json.SyntaxKind.EOF */) {
            if (scanner.getToken() === 5 /* Json.SyntaxKind.CommaToken */) {
                if (!needsComma) {
                    _error(l10n.t('Value expected'), ErrorCode.ValueExpected);
                }
                const commaOffset = scanner.getTokenOffset();
                _scanNext(); // consume comma
                if (scanner.getToken() === 4 /* Json.SyntaxKind.CloseBracketToken */) {
                    if (needsComma) {
                        _errorAtRange(l10n.t('Trailing comma'), ErrorCode.TrailingComma, commaOffset, commaOffset + 1);
                    }
                    continue;
                }
            }
            else if (needsComma) {
                _error(l10n.t('Expected comma'), ErrorCode.CommaExpected);
            }
            const item = _parseValue(node);
            if (!item) {
                _error(l10n.t('Value expected'), ErrorCode.ValueExpected, undefined, [], [4 /* Json.SyntaxKind.CloseBracketToken */, 5 /* Json.SyntaxKind.CommaToken */]);
            }
            else {
                node.items.push(item);
            }
            needsComma = true;
        }
        if (scanner.getToken() !== 4 /* Json.SyntaxKind.CloseBracketToken */) {
            return _error(l10n.t('Expected comma or closing bracket'), ErrorCode.CommaOrCloseBacketExpected, node);
        }
        return _finalize(node, true);
    }
    const keyPlaceholder = new StringASTNodeImpl(undefined, 0, 0);
    function _parseProperty(parent, keysSeen) {
        const node = new PropertyASTNodeImpl(parent, scanner.getTokenOffset(), keyPlaceholder);
        let key = _parseString(node);
        if (!key) {
            if (scanner.getToken() === 16 /* Json.SyntaxKind.Unknown */) {
                // give a more helpful error message
                _error(l10n.t('Property keys must be doublequoted'), ErrorCode.PropertyKeysMustBeDoublequoted);
                const keyNode = new StringASTNodeImpl(node, scanner.getTokenOffset(), scanner.getTokenLength());
                keyNode.value = scanner.getTokenValue();
                key = keyNode;
                _scanNext(); // consume Unknown
            }
            else {
                return undefined;
            }
        }
        node.keyNode = key;
        // For JSON files that forbid code comments, there is a convention to use the key name "//" to add comments.
        // Multiple instances of "//" are okay.
        if (key.value !== "//") {
            const seen = keysSeen[key.value];
            if (seen) {
                _errorAtRange(l10n.t("Duplicate object key"), ErrorCode.DuplicateKey, node.keyNode.offset, node.keyNode.offset + node.keyNode.length, DiagnosticSeverity.Warning);
                if (isObject(seen)) {
                    _errorAtRange(l10n.t("Duplicate object key"), ErrorCode.DuplicateKey, seen.keyNode.offset, seen.keyNode.offset + seen.keyNode.length, DiagnosticSeverity.Warning);
                }
                keysSeen[key.value] = true; // if the same key is duplicate again, avoid duplicate error reporting
            }
            else {
                keysSeen[key.value] = node;
            }
        }
        if (scanner.getToken() === 6 /* Json.SyntaxKind.ColonToken */) {
            node.colonOffset = scanner.getTokenOffset();
            _scanNext(); // consume ColonToken
        }
        else {
            _error(l10n.t('Colon expected'), ErrorCode.ColonExpected);
            if (scanner.getToken() === 10 /* Json.SyntaxKind.StringLiteral */ && textDocument.positionAt(key.offset + key.length).line < textDocument.positionAt(scanner.getTokenOffset()).line) {
                node.length = key.length;
                return node;
            }
        }
        const value = _parseValue(node);
        if (!value) {
            return _error(l10n.t('Value expected'), ErrorCode.ValueExpected, node, [], [2 /* Json.SyntaxKind.CloseBraceToken */, 5 /* Json.SyntaxKind.CommaToken */]);
        }
        node.valueNode = value;
        node.length = value.offset + value.length - node.offset;
        return node;
    }
    function _parseObject(parent) {
        if (scanner.getToken() !== 1 /* Json.SyntaxKind.OpenBraceToken */) {
            return undefined;
        }
        const node = new ObjectASTNodeImpl(parent, scanner.getTokenOffset());
        const keysSeen = Object.create(null);
        _scanNext(); // consume OpenBraceToken
        let needsComma = false;
        while (scanner.getToken() !== 2 /* Json.SyntaxKind.CloseBraceToken */ && scanner.getToken() !== 17 /* Json.SyntaxKind.EOF */) {
            if (scanner.getToken() === 5 /* Json.SyntaxKind.CommaToken */) {
                if (!needsComma) {
                    _error(l10n.t('Property expected'), ErrorCode.PropertyExpected);
                }
                const commaOffset = scanner.getTokenOffset();
                _scanNext(); // consume comma
                if (scanner.getToken() === 2 /* Json.SyntaxKind.CloseBraceToken */) {
                    if (needsComma) {
                        _errorAtRange(l10n.t('Trailing comma'), ErrorCode.TrailingComma, commaOffset, commaOffset + 1);
                    }
                    continue;
                }
            }
            else if (needsComma) {
                _error(l10n.t('Expected comma'), ErrorCode.CommaExpected);
            }
            const property = _parseProperty(node, keysSeen);
            if (!property) {
                _error(l10n.t('Property expected'), ErrorCode.PropertyExpected, undefined, [], [2 /* Json.SyntaxKind.CloseBraceToken */, 5 /* Json.SyntaxKind.CommaToken */]);
            }
            else {
                node.properties.push(property);
            }
            needsComma = true;
        }
        if (scanner.getToken() !== 2 /* Json.SyntaxKind.CloseBraceToken */) {
            return _error(l10n.t('Expected comma or closing brace'), ErrorCode.CommaOrCloseBraceExpected, node);
        }
        return _finalize(node, true);
    }
    function _parseString(parent) {
        if (scanner.getToken() !== 10 /* Json.SyntaxKind.StringLiteral */) {
            return undefined;
        }
        const node = new StringASTNodeImpl(parent, scanner.getTokenOffset());
        node.value = scanner.getTokenValue();
        return _finalize(node, true);
    }
    function _parseNumber(parent) {
        if (scanner.getToken() !== 11 /* Json.SyntaxKind.NumericLiteral */) {
            return undefined;
        }
        const node = new NumberASTNodeImpl(parent, scanner.getTokenOffset());
        if (scanner.getTokenError() === 0 /* Json.ScanError.None */) {
            const tokenValue = scanner.getTokenValue();
            try {
                const numberValue = JSON.parse(tokenValue);
                if (!isNumber(numberValue)) {
                    return _error(l10n.t('Invalid number format.'), ErrorCode.Undefined, node);
                }
                node.value = numberValue;
            }
            catch (e) {
                return _error(l10n.t('Invalid number format.'), ErrorCode.Undefined, node);
            }
            node.isInteger = tokenValue.indexOf('.') === -1;
        }
        return _finalize(node, true);
    }
    function _parseLiteral(parent) {
        let node;
        switch (scanner.getToken()) {
            case 7 /* Json.SyntaxKind.NullKeyword */:
                return _finalize(new NullASTNodeImpl(parent, scanner.getTokenOffset()), true);
            case 8 /* Json.SyntaxKind.TrueKeyword */:
                return _finalize(new BooleanASTNodeImpl(parent, true, scanner.getTokenOffset()), true);
            case 9 /* Json.SyntaxKind.FalseKeyword */:
                return _finalize(new BooleanASTNodeImpl(parent, false, scanner.getTokenOffset()), true);
            default:
                return undefined;
        }
    }
    function _parseValue(parent) {
        return _parseArray(parent) || _parseObject(parent) || _parseString(parent) || _parseNumber(parent) || _parseLiteral(parent);
    }
    let _root = undefined;
    const token = _scanNext();
    if (token !== 17 /* Json.SyntaxKind.EOF */) {
        _root = _parseValue(_root);
        if (!_root) {
            _error(l10n.t('Expected a JSON object, array or literal.'), ErrorCode.Undefined);
        }
        else if (scanner.getToken() !== 17 /* Json.SyntaxKind.EOF */) {
            _error(l10n.t('End of file expected.'), ErrorCode.Undefined);
        }
    }
    return new JSONDocument(_root, problems, commentRanges);
}