vscode-json-languageservice
Version:
Language service for JSON
1,057 lines • 62.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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';
import { extendedRegExp, stringLength } from '../utils/strings';
import { ErrorCode, Diagnostic, DiagnosticSeverity, Range, SchemaDraft } from '../jsonLanguageTypes';
import { URI } from 'vscode-uri';
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 },
'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) {
this.schemaDraft = schemaDraft;
}
}
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) {
if (this.root && schema) {
const validationResult = new ValidationResult();
validate(this.root, schema, validationResult, NoOpSchemaCollector.instance, new EvaluationContext(schemaDraft ?? getSchemaDraft(schema)));
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) {
if (this.root && schema) {
const matchingSchemas = new SchemaCollector(focusOffset, exclude);
const schemaDraft = getSchemaDraft(schema);
const context = new EvaluationContext(schemaDraft);
validate(this.root, schema, new ValidationResult(), matchingSchemas, context);
return matchingSchemas.schemas;
}
return [];
}
}
function getSchemaDraft(schema, fallBack = SchemaDraft.v2020_12) {
let schemaId = schema.$schema;
if (schemaId) {
return getSchemaDraftFromId(schemaId) ?? fallBack;
}
return fallBack;
}
function validate(n, schema, validationResult, matchingSchemas, context) {
if (!n || !matchingSchemas.include(n)) {
return;
}
if (n.type === 'property') {
return validate(n.valueNode, schema, validationResult, matchingSchemas, context);
}
const node = n;
_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 });
function _validateNode() {
function matchesType(type) {
return node.type === type || (type === 'integer' && node.type === 'number' && node.isInteger);
}
if (Array.isArray(schema.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) {
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)) {
for (const subSchemaRef of schema.allOf) {
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, asSchema(subSchemaRef), subValidationResult, subMatchingSchemas, context);
validationResult.merge(subValidationResult);
matchingSchemas.merge(subMatchingSchemas);
}
}
const notSchema = asSchema(schema.not);
if (notSchema) {
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, notSchema, subValidationResult, subMatchingSchemas, context);
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);
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)) {
testAlternatives(schema.anyOf, false);
}
if (Array.isArray(schema.oneOf)) {
testAlternatives(schema.oneOf, true);
}
const testBranch = (schema) => {
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, asSchema(schema), subValidationResult, subMatchingSchemas, context);
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);
matchingSchemas.merge(subMatchingSchemas);
validationResult.mergeProcessedProperties(subValidationResult);
if (!subValidationResult.hasProblems()) {
if (thenSchema) {
testBranch(thenSchema);
}
}
else if (elseSchema) {
testBranch(elseSchema);
}
};
const ifSchema = asSchema(schema.if);
if (ifSchema) {
testCondition(ifSchema, asSchema(schema.then), asSchema(schema.else));
}
if (Array.isArray(schema.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)) {
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);
}
});
}
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)) {
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) {
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) {
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) {
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) {
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) {
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) {
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)) {
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)
});
}
}
if (schema.format) {
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 '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);
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);
validationResult.mergePropertyMatch(itemValidationResult);
validationResult.processedProperties.add(String(index));
}
}
}
const containsSchema = asSchema(schema.contains);
if (containsSchema) {
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);
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) {
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) {
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) {
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);
validationResult.mergePropertyMatch(itemValidationResult);
}
}
validationResult.processedProperties.add(String(i));
validationResult.propertiesValueMatches++;
}
}
if (isNumber(schema.minItems) && node.items.length < schema.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) {
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) {
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)) {
for (const propertyName of schema.required) {
if (!seenKeys[propertyName]) {
const keyNode = node.parent && node.parent.type === 'property' && node.parent.keyNode;
const location = keyNode ? { offset: keyNode.offset, length: keyNode.length } : { offset: node.offset, length: 1 };
validationResult.problems.push({
location: location,
message: l10n.t('Missing property "{0}".', propertyName)
});
}
}
}
const propertyProcessed = (prop) => {
unprocessedProperties.delete(prop);
validationResult.processedProperties.add(prop);
};
if (schema.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);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
if (schema.patternProperties) {
for (const propertyPattern of Object.keys(schema.patternProperties)) {
const regex = extendedRegExp(propertyPattern);
if (regex) {
const processed = [];
for (const propertyName of unprocessedProperties) {
if (regex.test(propertyName)) {
processed.push(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);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
processed.forEach(propertyProcessed);
}
}
}
const additionalProperties = schema.additionalProperties;
if (additionalProperties !== undefined) {
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);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
const unevaluatedProperties = schema.unevaluatedProperties;
if (unevaluatedProperties !== undefined) {
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);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
processed.forEach(propertyProcessed);
}
if (isNumber(schema.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)) {
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.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) {
for (const key in schema.dependentSchemas) {
const prop = seenKeys[key];
const propertyDeps = schema.dependentSchemas[key];
if (prop && isObject(propertyDeps)) {
_validatePropertyDependencies(key, propertyDeps);
}
}
}
if (schema.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) {
for (const f of node.properties) {
const key = f.keyNode;
if (key) {
validate(key, propertyNames, validationResult, matchingSchemas, context);
}
}
}
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);
validationResult.mergePropertyMatch(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) {
con