yaml-language-server
Version:
913 lines • 73 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "../../utils/objects", "../../utils/schemaUtils", "../../utils/arrUtils", "../../utils/strings", "../../utils/filePatternAssociation", "../../utils/math", "../../jsonLanguageTypes", "@vscode/l10n", "vscode-uri", "vscode-languageserver-types", "../../utils/astNodeUtils", "./validatorFactory"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseValidator = exports.ValidationResult = exports.formats = exports.ProblemTypeMessages = exports.ProblemType = exports.YAML_SOURCE = void 0;
exports.asSchema = asSchema;
const objects_1 = require("../../utils/objects");
const schemaUtils_1 = require("../../utils/schemaUtils");
const arrUtils_1 = require("../../utils/arrUtils");
const strings_1 = require("../../utils/strings");
const filePatternAssociation_1 = require("../../utils/filePatternAssociation");
const math_1 = require("../../utils/math");
const jsonLanguageTypes_1 = require("../../jsonLanguageTypes");
const l10n = __importStar(require("@vscode/l10n"));
const vscode_uri_1 = require("vscode-uri");
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const astNodeUtils_1 = require("../../utils/astNodeUtils");
const validatorFactory_1 = require("./validatorFactory");
exports.YAML_SOURCE = 'YAML';
const YAML_SCHEMA_PREFIX = 'yaml-schema: ';
var ProblemType;
(function (ProblemType) {
ProblemType["missingRequiredPropWarning"] = "missingRequiredPropWarning";
ProblemType["typeMismatchWarning"] = "typeMismatchWarning";
ProblemType["constWarning"] = "constWarning";
})(ProblemType || (exports.ProblemType = ProblemType = {}));
exports.ProblemTypeMessages = {
[ProblemType.missingRequiredPropWarning]: 'Missing property "{0}".',
[ProblemType.typeMismatchWarning]: 'Incorrect type. Expected "{0}".',
[ProblemType.constWarning]: 'Value must be {0}.',
};
class SchemaCollector {
constructor(focusOffset = -1, exclude = null) {
this.focusOffset = focusOffset;
this.exclude = exclude;
this.schemas = [];
}
add(schema) {
this.schemas.push(schema);
}
merge(other) {
this.schemas.push(...other.schemas);
}
include(node) {
return (this.focusOffset === -1 || (0, astNodeUtils_1.contains)(node, this.focusOffset)) && node !== this.exclude;
}
newSub() {
return new SchemaCollector(-1, this.exclude);
}
}
class NoOpSchemaCollector {
constructor() {
// ignore
}
get schemas() {
return [];
}
add() {
// ignore
}
merge() {
// ignore
}
include() {
return true;
}
newSub() {
return this;
}
}
NoOpSchemaCollector.instance = new NoOpSchemaCollector();
exports.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-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
},
ipv4: {
errorMessage: l10n.t('String does not match IPv4 format.'),
pattern: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/,
},
ipv6: {
errorMessage: l10n.t('String does not match IPv6 format.'),
pattern: /^([0-9a-f]|:){1,4}(:([0-9a-f]{0,4})*){1,7}$/i,
},
};
class ValidationResult {
constructor(isKubernetes) {
this.problems = [];
this.propertiesMatches = 0;
this.propertiesValueMatches = 0;
this.primaryValueMatches = 0;
this.enumValueMatch = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.enumValues = null;
if (isKubernetes)
this.enumValues = [];
}
getEvaluatedItems(node) {
this.evaluatedItemsByNode ?? (this.evaluatedItemsByNode = new Map());
let evaluated = this.evaluatedItemsByNode.get(node);
if (!evaluated) {
evaluated = new Set();
this.evaluatedItemsByNode.set(node, evaluated);
}
return evaluated;
}
hasProblems() {
return this.problems.length > 0;
}
merge(other) {
this.problems = this.problems.concat(other.problems);
// Merge evaluatedProperties/evaluatedItems if present (needed for unevaluatedProperties/unevaluatedItems)
if (other.evaluatedProperties) {
this.evaluatedProperties ?? (this.evaluatedProperties = new Set());
for (const p of other.evaluatedProperties)
this.evaluatedProperties.add(p);
}
if (other.evaluatedItemsByNode) {
this.evaluatedItemsByNode ?? (this.evaluatedItemsByNode = new Map());
for (const [node, set] of other.evaluatedItemsByNode) {
const target = this.evaluatedItemsByNode.get(node);
if (target) {
for (const i of set)
target.add(i);
}
else {
this.evaluatedItemsByNode.set(node, new Set(set));
}
}
}
}
mergeEnumValues(other) {
if (!this.enumValueMatch && !other.enumValueMatch && this.enumValues && other.enumValues) {
this.enumValues = this.enumValues.concat(other.enumValues);
for (const err of this.problems) {
if (err.code === jsonLanguageTypes_1.ErrorCode.EnumValueMismatch) {
err.message = l10n.t('Value is not accepted. Valid values: {0}.', [...new Set(this.enumValues)].map((v) => JSON.stringify(v)).join(', '));
}
}
}
}
mergeWarningGeneric(sub, problemTypesToMerge) {
if (!this.problems?.length)
return;
for (const problemType of problemTypesToMerge) {
const bestResults = this.problems.filter((p) => p.problemType === problemType);
for (const bestResult of bestResults) {
const mergingResult = sub.problems?.find((p) => p.problemType === problemType &&
bestResult.location.offset === p.location.offset &&
(problemType !== ProblemType.missingRequiredPropWarning || (0, arrUtils_1.isArrayEqual)(p.problemArgs, bestResult.problemArgs)));
if (!mergingResult)
continue;
if (mergingResult.problemArgs?.length) {
mergingResult.problemArgs
.filter((p) => !bestResult.problemArgs.includes(p))
.forEach((p) => bestResult.problemArgs.push(p));
if (bestResult.problemType) {
bestResult.message = getWarningMessage(bestResult.problemType, bestResult.problemArgs ?? []);
}
}
this.mergeSources(mergingResult, bestResult);
}
}
}
mergePropertyMatch(propertyValidationResult, mergeEvaluated = true) {
if (mergeEvaluated) {
this.merge(propertyValidationResult);
}
else {
this.problems = this.problems.concat(propertyValidationResult.problems);
}
this.propertiesMatches++;
if (propertyValidationResult.enumValueMatch ||
(!propertyValidationResult.hasProblems() && propertyValidationResult.propertiesMatches)) {
this.propertiesValueMatches++;
}
if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues) {
this.primaryValueMatches++;
}
}
mergeSources(mergingResult, bestResult) {
const mergingSource = (mergingResult.source ?? '').replace(YAML_SCHEMA_PREFIX, '');
if (mergingSource && bestResult.source && !bestResult.source.includes(mergingSource)) {
bestResult.source = bestResult.source + ' | ' + mergingSource;
}
if (bestResult.schemaUri && mergingResult.schemaUri && !bestResult.schemaUri.includes(mergingResult.schemaUri[0])) {
bestResult.schemaUri = bestResult.schemaUri.concat(mergingResult.schemaUri);
}
}
compareGeneric(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.propertiesValueMatches !== other.propertiesValueMatches)
return this.propertiesValueMatches - other.propertiesValueMatches;
if (this.primaryValueMatches !== other.primaryValueMatches)
return this.primaryValueMatches - other.primaryValueMatches;
return this.propertiesMatches - other.propertiesMatches;
}
compareKubernetes(other) {
const hasProblems = this.hasProblems();
if (this.propertiesMatches !== other.propertiesMatches)
return this.propertiesMatches - other.propertiesMatches;
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;
if (hasProblems !== other.hasProblems())
return hasProblems ? -1 : 1;
return this.propertiesMatches - other.propertiesMatches;
}
}
exports.ValidationResult = ValidationResult;
class BaseValidator {
collectSeenKeys(node) {
const seenKeys = Object.create(null);
const unprocessedNodes = [...node.properties];
while (unprocessedNodes.length > 0) {
const propertyNode = unprocessedNodes.pop();
if (!propertyNode)
continue;
const key = propertyNode.keyNode.value;
// YAML merge key "<<"
if (key === '<<' && propertyNode.valueNode) {
const valueNode = propertyNode.valueNode;
switch (valueNode.type) {
case 'object':
unprocessedNodes.push(...valueNode.properties);
break;
case 'array':
valueNode.items.forEach((sequenceNode) => {
if (sequenceNode && sequenceNode.type === 'object' && (0, objects_1.isIterable)(sequenceNode.properties)) {
unprocessedNodes.push(...sequenceNode.properties);
}
});
break;
default:
break;
}
}
else {
seenKeys[key] = propertyNode.valueNode;
}
}
return seenKeys;
}
validateDocument(root, textDocument, schema, options) {
const validationResult = new ValidationResult(options.isKubernetes);
this.validateNode(root, schema, schema, validationResult, NoOpSchemaCollector.instance, options);
return validationResult.problems.map((p) => {
const range = vscode_languageserver_types_1.Range.create(textDocument.positionAt(p.location.offset), textDocument.positionAt(p.location.offset + p.location.length));
const diagnostic = vscode_languageserver_types_1.Diagnostic.create(range, p.message, p.severity, p.code ? p.code : jsonLanguageTypes_1.ErrorCode.Undefined, p.source);
diagnostic.data = { schemaUri: p.schemaUri, ...p.data };
return diagnostic;
});
}
getMatchingSchemas(root, schema, options, focusOffset, exclude) {
const matchingSchemas = new SchemaCollector(focusOffset, exclude);
this.validateNode(root, schema, schema, new ValidationResult(options.isKubernetes), matchingSchemas, options);
return matchingSchemas.schemas;
}
getNoOpCollector() {
return NoOpSchemaCollector.instance;
}
getSchemaSource(schema, originalSchema) {
let label;
if (schema.title) {
label = schema.title;
}
else if (schema.closestTitle) {
label = schema.closestTitle;
}
else if (originalSchema.closestTitle) {
label = originalSchema.closestTitle;
}
else {
const uriString = schema.url ?? originalSchema.url;
if (uriString)
label = vscode_uri_1.URI.parse(uriString).toString();
}
return label ? `${YAML_SCHEMA_PREFIX}${label}` : exports.YAML_SOURCE;
}
getSchemaUri(schema, originalSchema) {
const uriString = schema.url ?? originalSchema.url;
return uriString ? [uriString] : [];
}
matchesSchemaType(node, schemaType) {
const type = node.customTagReturnType ?? node.type;
if (schemaType === 'integer') {
return type === 'integer' || (type === 'number' && node.isInteger);
}
if (schemaType === 'number') {
return type === 'number' || type === 'integer';
}
return type === schemaType;
}
// ---------------- core traversal ----------------
validateNode(node, schema, originalSchema, validationResult, matchingSchemas, options) {
if (!node)
return;
if (!schema || typeof schema !== 'object')
return;
// Draft 2020-12 Compound Schema Document behavior: check if this node explicitly declares a different draft
if (schema._schemaDraft) {
const subSchemaDraft = schema._schemaDraft;
const currentSchemaDraft = this.getCurrentSchemaDraft();
if (subSchemaDraft !== currentSchemaDraft) {
const subValidator = (0, validatorFactory_1.getValidator)(subSchemaDraft);
subValidator.validateNode(node, schema, originalSchema, validationResult, matchingSchemas, options);
return;
}
}
if (!schema.url)
schema.url = originalSchema.url;
schema.closestTitle = schema.title || originalSchema.closestTitle;
switch (node.type) {
case 'object':
this.validateObjectNode(node, schema, originalSchema, validationResult, matchingSchemas, options);
break;
case 'array':
this.validateArrayNode(node, schema, originalSchema, validationResult, matchingSchemas, options);
break;
case 'string':
this.validateStringNode(node, schema, originalSchema, validationResult);
break;
case 'number':
this.validateNumberNode(node, schema, originalSchema, validationResult);
break;
case 'property':
this.validateNode(node.valueNode, schema, schema, validationResult, matchingSchemas, options);
break;
default:
break;
}
this.validateGenericNode(node, schema, originalSchema, validationResult, matchingSchemas, options);
// used by completion/hover schema collection
matchingSchemas.add({ node, schema });
// draft-specific post-processing hooks (no-op in base)
this.applyUnevaluatedItems(node, schema, originalSchema, validationResult, matchingSchemas, options);
// unevaluatedProperties must run AFTER validateGenericNode() (allOf/$ref/anyOf/oneOf/if/then/else/not)
if (node.type === 'object') {
const seenKeys = this.collectSeenKeys(node);
this.applyUnevaluatedProperties(node, schema, originalSchema, validationResult, matchingSchemas, options, seenKeys);
}
}
// ---------------- shared generic keywords ----------------
validateGenericNode(node, schema, originalSchema, validationResult, matchingSchemas, options) {
const { isKubernetes, callFromAutoComplete } = options;
const matchesType = (type) => this.matchesSchemaType(node, type);
const mergeEvaluated = (target, source) => {
if (source.evaluatedProperties) {
target.evaluatedProperties ?? (target.evaluatedProperties = new Set());
for (const p of source.evaluatedProperties)
target.evaluatedProperties.add(p);
}
if (source.evaluatedItemsByNode) {
target.evaluatedItemsByNode ?? (target.evaluatedItemsByNode = new Map());
for (const [nodeRef, set] of source.evaluatedItemsByNode) {
const targetSet = target.evaluatedItemsByNode.get(nodeRef);
if (targetSet) {
for (const i of set)
targetSet.add(i);
}
else {
target.evaluatedItemsByNode.set(nodeRef, new Set(set));
}
}
}
};
// type
if (Array.isArray(schema.type)) {
if (!schema.type.some(matchesType)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.errorMessage || l10n.t('Incorrect type. Expected one of {0}.', schema.type.join(', ')),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
else if (schema.type) {
if (!matchesType(schema.type)) {
const schemaType = schema.type === 'object' ? (0, schemaUtils_1.getSchemaTypeName)(schema) : schema.type;
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.errorMessage || getWarningMessage(ProblemType.typeMismatchWarning, [schemaType]),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
problemType: ProblemType.typeMismatchWarning,
problemArgs: [schemaType],
});
}
}
// allOf
if (Array.isArray(schema.allOf)) {
for (const subSchemaRef of schema.allOf) {
const subSchema = asSchema(subSchemaRef);
const subValidationResult = new ValidationResult(isKubernetes);
const subMatchingSchemas = matchingSchemas.newSub();
this.validateNode(node, subSchema, schema, subValidationResult, subMatchingSchemas, options);
validationResult.merge(subValidationResult);
validationResult.propertiesMatches += subValidationResult.propertiesMatches;
validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
matchingSchemas.merge(subMatchingSchemas);
}
}
// not
const notSchema = asSchema(schema.not);
if (notSchema) {
const subValidationResult = new ValidationResult(isKubernetes);
const subMatchingSchemas = matchingSchemas.newSub();
this.validateNode(node, notSchema, schema, subValidationResult, subMatchingSchemas, options);
if (!subValidationResult.hasProblems()) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Matches a schema that is not allowed.'),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
for (const ms of subMatchingSchemas.schemas) {
ms.inverted = !ms.inverted;
matchingSchemas.add(ms);
}
}
const testAlternatives = (alternatives, maxOneMatch) => {
const subMatches = [];
const noPropertyMatches = [];
const validResults = [];
let bestMatch = null;
for (const subSchemaRef of alternatives) {
const subSchema = { ...asSchema(subSchemaRef) };
const subValidationResult = new ValidationResult(isKubernetes);
const subMatchingSchemas = matchingSchemas.newSub();
this.validateNode(node, subSchema, schema, subValidationResult, subMatchingSchemas, options);
if (!subValidationResult.hasProblems() || callFromAutoComplete) {
subMatches.push(subSchema);
if (subValidationResult.propertiesMatches === 0)
noPropertyMatches.push(subSchema);
if (subSchema.format)
subMatches.pop();
}
if (!subValidationResult.hasProblems()) {
validResults.push(subValidationResult);
}
if (!bestMatch) {
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
}
else if (isKubernetes) {
bestMatch = this.alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas);
}
else {
bestMatch = this.genericComparison(node, maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas);
}
}
if (subMatches.length > 1 && (subMatches.length > 1 || noPropertyMatches.length === 0) && maxOneMatch) {
validationResult.problems.push({
location: { offset: node.offset, length: 1 },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Matches multiple schemas when only one must validate.'),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if (bestMatch) {
validationResult.merge(bestMatch.validationResult);
validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches;
validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches;
validationResult.enumValueMatch = validationResult.enumValueMatch || bestMatch.validationResult.enumValueMatch;
if (bestMatch.validationResult.enumValues?.length) {
validationResult.enumValues = (validationResult.enumValues || []).concat(bestMatch.validationResult.enumValues);
}
matchingSchemas.merge(bestMatch.matchingSchemas);
}
if (validResults.length > 0) {
for (const result of validResults) {
mergeEvaluated(validationResult, result);
}
}
};
if (Array.isArray(schema.anyOf)) {
testAlternatives(schema.anyOf, false);
}
if (Array.isArray(schema.oneOf)) {
testAlternatives(schema.oneOf, true);
}
// if / then / else (plus filePatternAssociation extension)
const testBranch = (branchSchema, original) => {
const subValidationResult = new ValidationResult(isKubernetes);
const subMatchingSchemas = matchingSchemas.newSub();
this.validateNode(node, asSchema(branchSchema), original, subValidationResult, subMatchingSchemas, options);
validationResult.merge(subValidationResult);
validationResult.propertiesMatches += subValidationResult.propertiesMatches;
validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
matchingSchemas.merge(subMatchingSchemas);
};
const testCondition = (ifSchemaRef, original, thenSchema, elseSchema) => {
const subSchema = asSchema(ifSchemaRef);
const subValidationResult = new ValidationResult(isKubernetes);
const subMatchingSchemas = matchingSchemas.newSub();
this.validateNode(node, subSchema, original, subValidationResult, subMatchingSchemas, options);
if (!callFromAutoComplete) {
matchingSchemas.merge(subMatchingSchemas);
}
const filePatternAssociation = subSchema?.filePatternAssociation;
if (filePatternAssociation) {
const association = new filePatternAssociation_1.FilePatternAssociation(filePatternAssociation);
if (!association.matchesPattern(options.uri)) {
subValidationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t("filePatternAssociation '{0}' does not match with doc uri '{1}'", filePatternAssociation, options.uri),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
if (!subValidationResult.hasProblems()) {
mergeEvaluated(validationResult, subValidationResult);
if (thenSchema)
testBranch(thenSchema, original);
}
else if (elseSchema) {
testBranch(elseSchema, original);
}
};
const ifSchema = asSchema(schema.if);
if (ifSchema) {
testCondition(ifSchema, schema, asSchema(schema.then), asSchema(schema.else));
}
// enum
if (Array.isArray(schema.enum)) {
const val = (0, astNodeUtils_1.getNodeValue)(node);
let enumValueMatch = false;
for (const e of schema.enum) {
if ((0, objects_1.equals)(val, e, node.type) || isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, e)) {
enumValueMatch = true;
break;
}
}
validationResult.enumValues = schema.enum;
validationResult.enumValueMatch = enumValueMatch;
if (!enumValueMatch) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
code: jsonLanguageTypes_1.ErrorCode.EnumValueMismatch,
message: schema.errorMessage ||
l10n.t('Value is not accepted. Valid values: {0}.', (schema.enum ?? []).map((v) => JSON.stringify(v)).join(', ')),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
data: { values: schema.enum },
});
}
}
// const
if ((0, objects_1.isDefined)(schema.const)) {
const val = (0, astNodeUtils_1.getNodeValue)(node);
const c = schema.const;
if (!(0, objects_1.equals)(val, c, node.type) && !isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, c)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
code: jsonLanguageTypes_1.ErrorCode.EnumValueMismatch,
problemType: ProblemType.constWarning,
message: schema.errorMessage || getWarningMessage(ProblemType.constWarning, [JSON.stringify(c)]),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
problemArgs: [JSON.stringify(c)],
data: { values: [c] },
});
validationResult.enumValueMatch = false;
}
else {
validationResult.enumValueMatch = true;
}
validationResult.enumValues = [c];
}
// deprecationMessage
if (schema.deprecationMessage && node.parent) {
validationResult.problems.push({
location: { offset: node.parent.offset, length: node.parent.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.deprecationMessage,
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
// ---------------- shared leaf validations ----------------
validateStringNode(node, schema, originalSchema, validationResult) {
const value = node.value;
if ((0, objects_1.isNumber)(schema.minLength) && value.length < (schema.minLength ?? 0)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('String is shorter than the minimum length of {0}.', schema.minLength),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if ((0, objects_1.isNumber)(schema.maxLength) && value.length > (schema.maxLength ?? 0)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('String is longer than the maximum length of {0}.', schema.maxLength),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if ((0, objects_1.isString)(schema.pattern)) {
const regex = (0, strings_1.safeCreateUnicodeRegExp)(schema.pattern);
if (!regex) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Invalid pattern: "{0}"', schema.pattern),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
else if (!regex.test(value)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.patternErrorMessage ||
schema.errorMessage ||
l10n.t('String does not match the pattern of "{0}".', schema.pattern),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
if (schema.format) {
switch (schema.format) {
case 'uri':
case 'uri-reference': {
let errorMessage;
if (!value) {
errorMessage = l10n.t('URI expected.');
}
else {
try {
const uri = vscode_uri_1.URI.parse(value);
if (!uri.scheme && schema.format === 'uri') {
errorMessage = l10n.t('URI with a scheme is expected.');
}
}
catch (e) {
errorMessage = e instanceof Error ? e.message : String(e);
}
}
if (errorMessage) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.patternErrorMessage || schema.errorMessage || l10n.t('String is not a URI: {0}', errorMessage),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
break;
}
case 'color-hex':
case 'date-time':
case 'date':
case 'time':
case 'email':
case 'ipv4':
case 'ipv6': {
const format = exports.formats[schema.format];
if (!value || !format.pattern.test(value)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.patternErrorMessage || schema.errorMessage || l10n.t(format.errorMessage),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
break;
}
default:
break;
}
}
}
validateNumberNode(node, schema, originalSchema, validationResult) {
const val = node.value;
if ((0, objects_1.isNumber)(schema.multipleOf)) {
if ((0, math_1.floatSafeRemainder)(val, schema.multipleOf ?? 0) !== 0) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Value is not divisible by {0}.', schema.multipleOf),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
const limits = this.getNumberLimits(schema);
if ((0, objects_1.isNumber)(limits.exclusiveMinimum) && val <= limits.exclusiveMinimum) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Value is below the exclusive minimum of {0}.', limits.exclusiveMinimum),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if ((0, objects_1.isNumber)(limits.exclusiveMaximum) && val >= limits.exclusiveMaximum) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Value is above the exclusive maximum of {0}.', limits.exclusiveMaximum),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if ((0, objects_1.isNumber)(limits.minimum) && val < limits.minimum) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Value is below the minimum of {0}.', limits.minimum),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if ((0, objects_1.isNumber)(limits.maximum) && val > limits.maximum) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Value is above the maximum of {0}.', limits.maximum),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
// ---------------- array validation (draft hotspot) ----------------
validateArrayNode(node, schema, originalSchema, validationResult, matchingSchemas, options) {
// Draft-07 default: tuple arrays via items: [] and additionalItems
const { isKubernetes } = options;
const items = node.items ?? [];
const evaluatedItems = schema.items ? validationResult.getEvaluatedItems(node) : undefined;
if (Array.isArray(schema.items)) {
const subSchemas = schema.items;
for (let index = 0; index < subSchemas.length; index++) {
const subSchema = asSchema(subSchemas[index]);
const itemValidationResult = new ValidationResult(isKubernetes);
const item = items[index];
if (item) {
this.validateNode(item, subSchema, schema, itemValidationResult, matchingSchemas, options);
validationResult.mergePropertyMatch(itemValidationResult, false);
validationResult.mergeEnumValues(itemValidationResult);
evaluatedItems?.add(index);
}
else if (items.length >= subSchemas.length) {
validationResult.propertiesValueMatches++;
}
}
if (items.length > subSchemas.length) {
const additional = schema.additionalItems;
if (additional === false) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Array has too many items according to schema. Expected {0} or fewer.', subSchemas.length),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
for (let i = subSchemas.length; i < items.length; i++) {
evaluatedItems?.add(i);
}
}
else if (typeof additional === 'object') {
for (let i = subSchemas.length; i < items.length; i++) {
const itemValidationResult = new ValidationResult(isKubernetes);
this.validateNode(items[i], additional, schema, itemValidationResult, matchingSchemas, options);
validationResult.mergePropertyMatch(itemValidationResult, false);
validationResult.mergeEnumValues(itemValidationResult);
evaluatedItems?.add(i);
}
}
else if (additional === true) {
// additionalItems is true => allowed by default (treat as {}), so mark remaining as evaluated
for (let i = subSchemas.length; i < items.length; i++) {
evaluatedItems?.add(i);
}
}
}
}
else {
const itemSchema = asSchema(schema.items);
if (itemSchema) {
const currentEvaluatedItems = validationResult.getEvaluatedItems(node);
for (let i = 0; i < items.length; i++) {
const item = items[i];
const itemValidationResult = new ValidationResult(isKubernetes);
this.validateNode(item, itemSchema, schema, itemValidationResult, matchingSchemas, options);
validationResult.mergePropertyMatch(itemValidationResult, false);
validationResult.mergeEnumValues(itemValidationResult);
currentEvaluatedItems.add(i);
}
}
}
this.applyContains(node, schema, originalSchema, validationResult, matchingSchemas, options);
this.applyArrayLength(node, schema, originalSchema, validationResult, options);
this.applyUniqueItems(node, schema, originalSchema, validationResult);
}
applyContains(node, schema, originalSchema, validationResult, _matchingSchemas, options) {
// Draft-07 default: contains must match at least one item
const { isKubernetes } = options;
const containsSchema = asSchema(schema.contains);
if (!containsSchema)
return;
const doesContain = (node.items ?? []).some((item) => {
const itemValidationResult = new ValidationResult(isKubernetes);
this.validateNode(item, containsSchema, schema, itemValidationResult, NoOpSchemaCollector.instance, options);
return !itemValidationResult.hasProblems();
});
if (!doesContain) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: schema.errorMessage || l10n.t('Array does not contain required item.'),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
}
applyArrayLength(node, schema, originalSchema, validationResult,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_options) {
if ((0, objects_1.isNumber)(schema.minItems) && node.items.length < (schema.minItems ?? 0)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
message: l10n.t('Array has too few items. Expected {0} or more.', schema.minItems),
source: this.getSchemaSource(schema, originalSchema),
schemaUri: this.getSchemaUri(schema, originalSchema),
});
}
if ((0, objects_1.isNumber)(schema.maxItems) && node.items.length > (schema.maxItems ?? 0)) {
validationResult.problems.push({
location: { offset: node.offset, lengt