1360 lines
70 KiB
JavaScript
1360 lines
70 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Red Hat, Inc. All rights reserved.
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
(function (factory) {
|
|
if (typeof module === "object" && typeof module.exports === "object") {
|
|
var v = factory(require, exports);
|
|
if (v !== undefined) module.exports = v;
|
|
}
|
|
else if (typeof define === "function" && define.amd) {
|
|
define(["require", "exports", "../utils/objects", "../utils/schemaUtils", "vscode-json-languageservice", "vscode-nls", "vscode-uri", "vscode-languageserver-types", "../utils/arrUtils", "../utils/strings", "../services/yamlSchemaService"], factory);
|
|
}
|
|
})(function (require, exports) {
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.JSONDocument = exports.findNodeAtOffset = exports.contains = exports.getNodeValue = exports.newJSONDocument = exports.ValidationResult = exports.EnumMatch = exports.asSchema = exports.ObjectASTNodeImpl = exports.PropertyASTNodeImpl = exports.StringASTNodeImpl = exports.NumberASTNodeImpl = exports.ArrayASTNodeImpl = exports.BooleanASTNodeImpl = exports.NullASTNodeImpl = exports.ASTNodeImpl = exports.ProblemTypeMessages = exports.ProblemType = exports.YAML_SOURCE = exports.formats = void 0;
|
|
const objects_1 = require("../utils/objects");
|
|
const schemaUtils_1 = require("../utils/schemaUtils");
|
|
const vscode_json_languageservice_1 = require("vscode-json-languageservice");
|
|
const nls = require("vscode-nls");
|
|
const vscode_uri_1 = require("vscode-uri");
|
|
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
|
|
const arrUtils_1 = require("../utils/arrUtils");
|
|
const strings_1 = require("../utils/strings");
|
|
const yamlSchemaService_1 = require("../services/yamlSchemaService");
|
|
const localize = nls.loadMessageBundle();
|
|
const MSG_PROPERTY_NOT_ALLOWED = 'Property {0} is not allowed.';
|
|
exports.formats = {
|
|
'color-hex': {
|
|
errorMessage: localize('colorHexFormatWarning', '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: localize('dateTimeFormatWarning', '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: localize('dateFormatWarning', '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: localize('timeFormatWarning', '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: localize('emailFormatWarning', '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: localize('ipv4FormatWarning', 'String does not match IPv4 format.'),
|
|
pattern: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/,
|
|
},
|
|
ipv6: {
|
|
errorMessage: localize('ipv6FormatWarning', 'String does not match IPv6 format.'),
|
|
pattern: /^([0-9a-f]|:){1,4}(:([0-9a-f]{0,4})*){1,7}$/i,
|
|
},
|
|
};
|
|
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 || (exports.ProblemType = {}));
|
|
exports.ProblemTypeMessages = {
|
|
[ProblemType.missingRequiredPropWarning]: 'Missing property "{0}".',
|
|
[ProblemType.typeMismatchWarning]: 'Incorrect type. Expected "{0}".',
|
|
[ProblemType.constWarning]: 'Value must be {0}.',
|
|
};
|
|
class ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
this.offset = offset;
|
|
this.length = length;
|
|
this.parent = parent;
|
|
this.internalNode = internalNode;
|
|
}
|
|
getNodeFromOffsetEndInclusive(offset) {
|
|
const collector = [];
|
|
const findNode = (node) => {
|
|
if (offset >= node.offset && offset <= node.offset + node.length) {
|
|
const children = node.children;
|
|
for (let i = 0; i < children.length && children[i].offset <= offset; i++) {
|
|
const item = findNode(children[i]);
|
|
if (item) {
|
|
collector.push(item);
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
return null;
|
|
};
|
|
const foundNode = findNode(this);
|
|
let currMinDist = Number.MAX_VALUE;
|
|
let currMinNode = null;
|
|
for (const currNode of collector) {
|
|
const minDist = currNode.length + currNode.offset - offset + (offset - currNode.offset);
|
|
if (minDist < currMinDist) {
|
|
currMinNode = currNode;
|
|
currMinDist = minDist;
|
|
}
|
|
}
|
|
return currMinNode || foundNode;
|
|
}
|
|
get children() {
|
|
return [];
|
|
}
|
|
toString() {
|
|
return ('type: ' +
|
|
this.type +
|
|
' (' +
|
|
this.offset +
|
|
'/' +
|
|
this.length +
|
|
')' +
|
|
(this.parent ? ' parent: {' + this.parent.toString() + '}' : ''));
|
|
}
|
|
}
|
|
exports.ASTNodeImpl = ASTNodeImpl;
|
|
class NullASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'null';
|
|
this.value = null;
|
|
}
|
|
}
|
|
exports.NullASTNodeImpl = NullASTNodeImpl;
|
|
class BooleanASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, boolValue, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'boolean';
|
|
this.value = boolValue;
|
|
}
|
|
}
|
|
exports.BooleanASTNodeImpl = BooleanASTNodeImpl;
|
|
class ArrayASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'array';
|
|
this.items = [];
|
|
}
|
|
get children() {
|
|
return this.items;
|
|
}
|
|
}
|
|
exports.ArrayASTNodeImpl = ArrayASTNodeImpl;
|
|
class NumberASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'number';
|
|
this.isInteger = true;
|
|
this.value = Number.NaN;
|
|
}
|
|
}
|
|
exports.NumberASTNodeImpl = NumberASTNodeImpl;
|
|
class StringASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'string';
|
|
this.value = '';
|
|
}
|
|
}
|
|
exports.StringASTNodeImpl = StringASTNodeImpl;
|
|
class PropertyASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'property';
|
|
this.colonOffset = -1;
|
|
}
|
|
get children() {
|
|
return this.valueNode ? [this.keyNode, this.valueNode] : [this.keyNode];
|
|
}
|
|
}
|
|
exports.PropertyASTNodeImpl = PropertyASTNodeImpl;
|
|
class ObjectASTNodeImpl extends ASTNodeImpl {
|
|
constructor(parent, internalNode, offset, length) {
|
|
super(parent, internalNode, offset, length);
|
|
this.type = 'object';
|
|
this.properties = [];
|
|
}
|
|
get children() {
|
|
return this.properties;
|
|
}
|
|
}
|
|
exports.ObjectASTNodeImpl = ObjectASTNodeImpl;
|
|
function asSchema(schema) {
|
|
if (schema === undefined) {
|
|
return undefined;
|
|
}
|
|
if ((0, objects_1.isBoolean)(schema)) {
|
|
return schema ? {} : { not: {} };
|
|
}
|
|
if (typeof schema !== 'object') {
|
|
// we need to report this case as JSONSchemaRef MUST be an Object or Boolean
|
|
console.warn(`Wrong schema: ${JSON.stringify(schema)}, it MUST be an Object or Boolean`);
|
|
schema = {
|
|
type: schema,
|
|
};
|
|
}
|
|
return schema;
|
|
}
|
|
exports.asSchema = asSchema;
|
|
var EnumMatch;
|
|
(function (EnumMatch) {
|
|
EnumMatch[EnumMatch["Key"] = 0] = "Key";
|
|
EnumMatch[EnumMatch["Enum"] = 1] = "Enum";
|
|
})(EnumMatch = exports.EnumMatch || (exports.EnumMatch = {}));
|
|
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 || contains(node, this.focusOffset)) && node !== this.exclude;
|
|
}
|
|
newSub() {
|
|
return new SchemaCollector(-1, this.exclude);
|
|
}
|
|
}
|
|
class NoOpSchemaCollector {
|
|
constructor() {
|
|
// ignore
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
get schemas() {
|
|
return [];
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
add(schema) {
|
|
// ignore
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
merge(other) {
|
|
// ignore
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
include(node) {
|
|
return true;
|
|
}
|
|
newSub() {
|
|
return this;
|
|
}
|
|
}
|
|
NoOpSchemaCollector.instance = new NoOpSchemaCollector();
|
|
class ValidationResult {
|
|
constructor(isKubernetes) {
|
|
this.problems = [];
|
|
this.propertiesMatches = 0;
|
|
this.propertiesValueMatches = 0;
|
|
this.primaryValueMatches = 0;
|
|
this.enumValueMatch = false;
|
|
if (isKubernetes) {
|
|
this.enumValues = [];
|
|
}
|
|
else {
|
|
this.enumValues = null;
|
|
}
|
|
}
|
|
hasProblems() {
|
|
return !!this.problems.length;
|
|
}
|
|
mergeAll(validationResults) {
|
|
for (const validationResult of validationResults) {
|
|
this.merge(validationResult);
|
|
}
|
|
}
|
|
merge(validationResult) {
|
|
this.problems = this.problems.concat(validationResult.problems);
|
|
}
|
|
mergeEnumValues(validationResult) {
|
|
if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) {
|
|
this.enumValues = this.enumValues.concat(validationResult.enumValues);
|
|
for (const error of this.problems) {
|
|
if (error.code === vscode_json_languageservice_1.ErrorCode.EnumValueMismatch) {
|
|
error.message = localize('enumWarning', 'Value is not accepted. Valid values: {0}.', [...new Set(this.enumValues)]
|
|
.map((v) => {
|
|
return JSON.stringify(v);
|
|
})
|
|
.join(', '));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Merge multiple warnings with same problemType together
|
|
* @param subValidationResult another possible result
|
|
*/
|
|
mergeWarningGeneric(subValidationResult, problemTypesToMerge) {
|
|
if (this.problems?.length) {
|
|
for (const problemType of problemTypesToMerge) {
|
|
const bestResults = this.problems.filter((p) => p.problemType === problemType);
|
|
for (const bestResult of bestResults) {
|
|
const mergingResult = subValidationResult.problems?.find((p) => p.problemType === problemType &&
|
|
bestResult.location.offset === p.location.offset &&
|
|
(problemType !== ProblemType.missingRequiredPropWarning || (0, arrUtils_1.isArrayEqual)(p.problemArgs, bestResult.problemArgs)) // missingProp is merged only with same problemArg
|
|
);
|
|
if (mergingResult) {
|
|
if (mergingResult.problemArgs?.length) {
|
|
mergingResult.problemArgs
|
|
.filter((p) => !bestResult.problemArgs.includes(p))
|
|
.forEach((p) => bestResult.problemArgs.push(p));
|
|
bestResult.message = getWarningMessage(bestResult.problemType, bestResult.problemArgs);
|
|
}
|
|
this.mergeSources(mergingResult, bestResult);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mergePropertyMatch(propertyValidationResult) {
|
|
this.merge(propertyValidationResult);
|
|
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 (!bestResult.source.includes(mergingSource)) {
|
|
bestResult.source = bestResult.source + ' | ' + mergingSource;
|
|
}
|
|
if (!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;
|
|
function newJSONDocument(root, diagnostics = []) {
|
|
return new JSONDocument(root, diagnostics, []);
|
|
}
|
|
exports.newJSONDocument = newJSONDocument;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function getNodeValue(node) {
|
|
switch (node.type) {
|
|
case 'array':
|
|
return node.children.map(getNodeValue);
|
|
case 'object': {
|
|
const obj = Object.create(null);
|
|
for (let _i = 0, _a = node.children; _i < _a.length; _i++) {
|
|
const prop = _a[_i];
|
|
const valueNode = prop.children[1];
|
|
if (valueNode) {
|
|
obj[prop.children[0].value] = getNodeValue(valueNode);
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
case 'null':
|
|
case 'string':
|
|
case 'number':
|
|
case 'boolean':
|
|
return node.value;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
exports.getNodeValue = getNodeValue;
|
|
function contains(node, offset, includeRightBound = false) {
|
|
return ((offset >= node.offset && offset <= node.offset + node.length) || (includeRightBound && offset === node.offset + node.length));
|
|
}
|
|
exports.contains = contains;
|
|
function findNodeAtOffset(node, offset, includeRightBound) {
|
|
if (includeRightBound === void 0) {
|
|
includeRightBound = false;
|
|
}
|
|
if (contains(node, offset, includeRightBound)) {
|
|
const children = node.children;
|
|
if (Array.isArray(children)) {
|
|
for (let i = 0; i < children.length && children[i].offset <= offset; i++) {
|
|
const item = findNodeAtOffset(children[i], offset, includeRightBound);
|
|
if (item) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
return undefined;
|
|
}
|
|
exports.findNodeAtOffset = findNodeAtOffset;
|
|
class JSONDocument {
|
|
constructor(root, syntaxErrors = [], comments = []) {
|
|
this.root = root;
|
|
this.syntaxErrors = syntaxErrors;
|
|
this.comments = comments;
|
|
}
|
|
getNodeFromOffset(offset, includeRightBound = false) {
|
|
if (this.root) {
|
|
return findNodeAtOffset(this.root, offset, includeRightBound);
|
|
}
|
|
return undefined;
|
|
}
|
|
getNodeFromOffsetEndInclusive(offset) {
|
|
return this.root && this.root.getNodeFromOffsetEndInclusive(offset);
|
|
}
|
|
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) {
|
|
if (this.root && schema) {
|
|
const validationResult = new ValidationResult(this.isKubernetes);
|
|
validate(this.root, schema, schema, validationResult, NoOpSchemaCollector.instance, {
|
|
isKubernetes: this.isKubernetes,
|
|
disableAdditionalProperties: this.disableAdditionalProperties,
|
|
uri: this.uri,
|
|
});
|
|
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 : vscode_json_languageservice_1.ErrorCode.Undefined, p.source);
|
|
diagnostic.data = { schemaUri: p.schemaUri, ...p.data };
|
|
return diagnostic;
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* This method returns the list of applicable schemas
|
|
*
|
|
* currently used @param didCallFromAutoComplete flag to differentiate the method call, when it is from auto complete
|
|
* then user still types something and skip the validation for timebeing untill completed.
|
|
* On https://github.com/redhat-developer/yaml-language-server/pull/719 the auto completes need to populate the list of enum string which matches to the enum
|
|
* and on https://github.com/redhat-developer/vscode-yaml/issues/803 the validation should throw the error based on the enum string.
|
|
*
|
|
* @param schema schema
|
|
* @param focusOffset offsetValue
|
|
* @param exclude excluded Node
|
|
* @param didCallFromAutoComplete true if method called from AutoComplete
|
|
* @returns array of applicable schemas
|
|
*/
|
|
getMatchingSchemas(schema, focusOffset = -1, exclude = null, didCallFromAutoComplete) {
|
|
const matchingSchemas = new SchemaCollector(focusOffset, exclude);
|
|
if (this.root && schema) {
|
|
validate(this.root, schema, schema, new ValidationResult(this.isKubernetes), matchingSchemas, {
|
|
isKubernetes: this.isKubernetes,
|
|
disableAdditionalProperties: this.disableAdditionalProperties,
|
|
uri: this.uri,
|
|
callFromAutoComplete: didCallFromAutoComplete,
|
|
});
|
|
}
|
|
return matchingSchemas.schemas;
|
|
}
|
|
}
|
|
exports.JSONDocument = JSONDocument;
|
|
function validate(node, schema, originalSchema, validationResult, matchingSchemas, options
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
) {
|
|
const { isKubernetes, callFromAutoComplete } = options;
|
|
if (!node) {
|
|
return;
|
|
}
|
|
// schema should be an Object
|
|
if (typeof schema !== 'object') {
|
|
return;
|
|
}
|
|
if (!schema.url) {
|
|
schema.url = originalSchema.url;
|
|
}
|
|
schema.closestTitle = schema.title || originalSchema.closestTitle;
|
|
switch (node.type) {
|
|
case 'object':
|
|
_validateObjectNode(node, schema, validationResult, matchingSchemas);
|
|
break;
|
|
case 'array':
|
|
_validateArrayNode(node, schema, validationResult, matchingSchemas);
|
|
break;
|
|
case 'string':
|
|
_validateStringNode(node, schema, validationResult);
|
|
break;
|
|
case 'number':
|
|
_validateNumberNode(node, schema, validationResult);
|
|
break;
|
|
case 'property':
|
|
return validate(node.valueNode, schema, schema, validationResult, matchingSchemas, options);
|
|
}
|
|
_validateNode();
|
|
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 },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.errorMessage ||
|
|
localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}.', schema.type.join(', ')),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
else if (schema.type) {
|
|
if (!matchesType(schema.type)) {
|
|
//get more specific name than just object
|
|
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: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
problemType: ProblemType.typeMismatchWarning,
|
|
problemArgs: [schemaType],
|
|
});
|
|
}
|
|
}
|
|
if (Array.isArray(schema.allOf)) {
|
|
for (const subSchemaRef of schema.allOf) {
|
|
validate(node, asSchema(subSchemaRef), schema, validationResult, matchingSchemas, options);
|
|
}
|
|
}
|
|
const notSchema = asSchema(schema.not);
|
|
if (notSchema) {
|
|
const subValidationResult = new ValidationResult(isKubernetes);
|
|
const subMatchingSchemas = matchingSchemas.newSub();
|
|
validate(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: localize('notSchemaWarning', 'Matches a schema that is not allowed.'),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
for (const ms of subMatchingSchemas.schemas) {
|
|
ms.inverted = !ms.inverted;
|
|
matchingSchemas.add(ms);
|
|
}
|
|
}
|
|
const testAlternatives = (alternatives, maxOneMatch) => {
|
|
const matches = [];
|
|
const subMatches = [];
|
|
const noPropertyMatches = [];
|
|
// remember the best match that is used for error messages
|
|
let bestMatch = null;
|
|
for (const subSchemaRef of alternatives) {
|
|
const subSchema = { ...asSchema(subSchemaRef) };
|
|
const subValidationResult = new ValidationResult(isKubernetes);
|
|
const subMatchingSchemas = matchingSchemas.newSub();
|
|
validate(node, subSchema, schema, subValidationResult, subMatchingSchemas, options);
|
|
if (!subValidationResult.hasProblems() || callFromAutoComplete) {
|
|
matches.push(subSchema);
|
|
subMatches.push(subSchema);
|
|
if (subValidationResult.propertiesMatches === 0) {
|
|
noPropertyMatches.push(subSchema);
|
|
}
|
|
if (subSchema.format) {
|
|
subMatches.pop();
|
|
}
|
|
}
|
|
if (!bestMatch) {
|
|
bestMatch = {
|
|
schema: subSchema,
|
|
validationResult: subValidationResult,
|
|
matchingSchemas: subMatchingSchemas,
|
|
};
|
|
}
|
|
else if (isKubernetes) {
|
|
bestMatch = alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas);
|
|
}
|
|
else {
|
|
bestMatch = 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: localize('oneOfWarning', 'Matches multiple schemas when only one must validate.'),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
if (bestMatch !== null) {
|
|
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);
|
|
}
|
|
return matches.length;
|
|
};
|
|
if (Array.isArray(schema.anyOf)) {
|
|
testAlternatives(schema.anyOf, false);
|
|
}
|
|
if (Array.isArray(schema.oneOf)) {
|
|
testAlternatives(schema.oneOf, true);
|
|
}
|
|
const testBranch = (schema, originalSchema) => {
|
|
const subValidationResult = new ValidationResult(isKubernetes);
|
|
const subMatchingSchemas = matchingSchemas.newSub();
|
|
validate(node, asSchema(schema), originalSchema, subValidationResult, subMatchingSchemas, options);
|
|
validationResult.merge(subValidationResult);
|
|
validationResult.propertiesMatches += subValidationResult.propertiesMatches;
|
|
validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
|
|
matchingSchemas.merge(subMatchingSchemas);
|
|
};
|
|
const testCondition = (ifSchema, originalSchema, thenSchema, elseSchema) => {
|
|
const subSchema = asSchema(ifSchema);
|
|
const subValidationResult = new ValidationResult(isKubernetes);
|
|
const subMatchingSchemas = matchingSchemas.newSub();
|
|
validate(node, subSchema, originalSchema, subValidationResult, subMatchingSchemas, options);
|
|
matchingSchemas.merge(subMatchingSchemas);
|
|
const { filePatternAssociation } = subSchema;
|
|
if (filePatternAssociation) {
|
|
const association = new yamlSchemaService_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: localize('ifFilePatternAssociation', `filePatternAssociation '${filePatternAssociation}' does not match with doc uri '${options.uri}'.`),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
// don't want to expose the error up to code-completion results
|
|
// validationResult.merge(subValidationResult);
|
|
}
|
|
}
|
|
if (!subValidationResult.hasProblems()) {
|
|
if (thenSchema) {
|
|
testBranch(thenSchema, originalSchema);
|
|
}
|
|
}
|
|
else if (elseSchema) {
|
|
testBranch(elseSchema, originalSchema);
|
|
}
|
|
};
|
|
const ifSchema = asSchema(schema.if);
|
|
if (ifSchema) {
|
|
testCondition(ifSchema, schema, 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 ((0, objects_1.equals)(val, e) || (callFromAutoComplete && (0, objects_1.isString)(val) && (0, objects_1.isString)(e) && val && e.startsWith(val))) {
|
|
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: vscode_json_languageservice_1.ErrorCode.EnumValueMismatch,
|
|
message: schema.errorMessage ||
|
|
localize('enumWarning', 'Value is not accepted. Valid values: {0}.', schema.enum
|
|
.map((v) => {
|
|
return JSON.stringify(v);
|
|
})
|
|
.join(', ')),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
if ((0, objects_1.isDefined)(schema.const)) {
|
|
const val = getNodeValue(node);
|
|
if (!(0, objects_1.equals)(val, schema.const) &&
|
|
!(callFromAutoComplete && (0, objects_1.isString)(val) && (0, objects_1.isString)(schema.const) && schema.const.startsWith(val))) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
code: vscode_json_languageservice_1.ErrorCode.EnumValueMismatch,
|
|
problemType: ProblemType.constWarning,
|
|
message: schema.errorMessage || getWarningMessage(ProblemType.constWarning, [JSON.stringify(schema.const)]),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
problemArgs: [JSON.stringify(schema.const)],
|
|
});
|
|
validationResult.enumValueMatch = false;
|
|
}
|
|
else {
|
|
validationResult.enumValueMatch = true;
|
|
}
|
|
validationResult.enumValues = [schema.const];
|
|
}
|
|
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: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
function _validateNumberNode(node, schema, validationResult) {
|
|
const val = node.value;
|
|
if ((0, objects_1.isNumber)(schema.multipleOf)) {
|
|
if (val % schema.multipleOf !== 0) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('multipleOfWarning', 'Value is not divisible by {0}.', schema.multipleOf),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
function getExclusiveLimit(limit, exclusive) {
|
|
if ((0, objects_1.isNumber)(exclusive)) {
|
|
return exclusive;
|
|
}
|
|
if ((0, objects_1.isBoolean)(exclusive) && exclusive) {
|
|
return limit;
|
|
}
|
|
return undefined;
|
|
}
|
|
function getLimit(limit, exclusive) {
|
|
if (!(0, objects_1.isBoolean)(exclusive) || !exclusive) {
|
|
return limit;
|
|
}
|
|
return undefined;
|
|
}
|
|
const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum);
|
|
if ((0, objects_1.isNumber)(exclusiveMinimum) && val <= exclusiveMinimum) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('exclusiveMinimumWarning', 'Value is below the exclusive minimum of {0}.', exclusiveMinimum),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
const exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum);
|
|
if ((0, objects_1.isNumber)(exclusiveMaximum) && val >= exclusiveMaximum) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('exclusiveMaximumWarning', 'Value is above the exclusive maximum of {0}.', exclusiveMaximum),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
const minimum = getLimit(schema.minimum, schema.exclusiveMinimum);
|
|
if ((0, objects_1.isNumber)(minimum) && val < minimum) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('minimumWarning', 'Value is below the minimum of {0}.', minimum),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
const maximum = getLimit(schema.maximum, schema.exclusiveMaximum);
|
|
if ((0, objects_1.isNumber)(maximum) && val > maximum) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('maximumWarning', 'Value is above the maximum of {0}.', maximum),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
function _validateStringNode(node, schema, validationResult) {
|
|
if ((0, objects_1.isNumber)(schema.minLength) && node.value.length < schema.minLength) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('minLengthWarning', 'String is shorter than the minimum length of {0}.', schema.minLength),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
if ((0, objects_1.isNumber)(schema.maxLength) && node.value.length > schema.maxLength) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('maxLengthWarning', 'String is longer than the maximum length of {0}.', schema.maxLength),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
if ((0, objects_1.isString)(schema.pattern)) {
|
|
const regex = (0, strings_1.safeCreateUnicodeRegExp)(schema.pattern);
|
|
if (!regex.test(node.value)) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.patternErrorMessage ||
|
|
schema.errorMessage ||
|
|
localize('patternWarning', 'String does not match the pattern of "{0}".', schema.pattern),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
if (schema.format) {
|
|
switch (schema.format) {
|
|
case 'uri':
|
|
case 'uri-reference':
|
|
{
|
|
let errorMessage;
|
|
if (!node.value) {
|
|
errorMessage = localize('uriEmpty', 'URI expected.');
|
|
}
|
|
else {
|
|
try {
|
|
const uri = vscode_uri_1.URI.parse(node.value);
|
|
if (!uri.scheme && schema.format === 'uri') {
|
|
errorMessage = localize('uriSchemeMissing', 'URI with a scheme is expected.');
|
|
}
|
|
}
|
|
catch (e) {
|
|
errorMessage = e.message;
|
|
}
|
|
}
|
|
if (errorMessage) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.patternErrorMessage ||
|
|
schema.errorMessage ||
|
|
localize('uriFormatWarning', 'String is not a URI: {0}', errorMessage),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: 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 (!node.value || !format.pattern.test(node.value)) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.patternErrorMessage || schema.errorMessage || format.errorMessage,
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
function _validateArrayNode(node, schema, validationResult, matchingSchemas) {
|
|
if (Array.isArray(schema.items)) {
|
|
const subSchemas = schema.items;
|
|
for (let index = 0; index < subSchemas.length; index++) {
|
|
const subSchemaRef = subSchemas[index];
|
|
const subSchema = asSchema(subSchemaRef);
|
|
const itemValidationResult = new ValidationResult(isKubernetes);
|
|
const item = node.items[index];
|
|
if (item) {
|
|
validate(item, subSchema, schema, itemValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(itemValidationResult);
|
|
validationResult.mergeEnumValues(itemValidationResult);
|
|
}
|
|
else if (node.items.length >= subSchemas.length) {
|
|
validationResult.propertiesValueMatches++;
|
|
}
|
|
}
|
|
if (node.items.length > subSchemas.length) {
|
|
if (typeof schema.additionalItems === 'object') {
|
|
for (let i = subSchemas.length; i < node.items.length; i++) {
|
|
const itemValidationResult = new ValidationResult(isKubernetes);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
validate(node.items[i], schema.additionalItems, schema, itemValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(itemValidationResult);
|
|
validationResult.mergeEnumValues(itemValidationResult);
|
|
}
|
|
}
|
|
else if (schema.additionalItems === false) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer.', subSchemas.length),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const itemSchema = asSchema(schema.items);
|
|
if (itemSchema) {
|
|
const itemValidationResult = new ValidationResult(isKubernetes);
|
|
node.items.forEach((item) => {
|
|
if (itemSchema.oneOf && itemSchema.oneOf.length === 1) {
|
|
const subSchemaRef = itemSchema.oneOf[0];
|
|
const subSchema = { ...asSchema(subSchemaRef) };
|
|
subSchema.title = schema.title;
|
|
subSchema.closestTitle = schema.closestTitle;
|
|
validate(item, subSchema, schema, itemValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(itemValidationResult);
|
|
validationResult.mergeEnumValues(itemValidationResult);
|
|
}
|
|
else {
|
|
validate(item, itemSchema, schema, itemValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(itemValidationResult);
|
|
validationResult.mergeEnumValues(itemValidationResult);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
const containsSchema = asSchema(schema.contains);
|
|
if (containsSchema) {
|
|
const doesContain = node.items.some((item) => {
|
|
const itemValidationResult = new ValidationResult(isKubernetes);
|
|
validate(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 || localize('requiredItemMissingWarning', 'Array does not contain required item.'),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
if ((0, objects_1.isNumber)(schema.minItems) && node.items.length < schema.minItems) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('minItemsWarning', 'Array has too few items. Expected {0} or more.', schema.minItems),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
if ((0, objects_1.isNumber)(schema.maxItems) && node.items.length > schema.maxItems) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('maxItemsWarning', 'Array has too many items. Expected {0} or fewer.', schema.maxItems),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
if (schema.uniqueItems === true) {
|
|
const values = getNodeValue(node);
|
|
const duplicates = values.some((value, index) => {
|
|
return index !== values.lastIndexOf(value);
|
|
});
|
|
if (duplicates) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('uniqueItemsWarning', 'Array has duplicate items.'),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function _validateObjectNode(node, schema, validationResult, matchingSchemas) {
|
|
const seenKeys = Object.create(null);
|
|
const unprocessedProperties = [];
|
|
const unprocessedNodes = [...node.properties];
|
|
while (unprocessedNodes.length > 0) {
|
|
const propertyNode = unprocessedNodes.pop();
|
|
const key = propertyNode.keyNode.value;
|
|
//Replace the merge key with the actual values of what the node value points to in seen keys
|
|
if (key === '<<' && propertyNode.valueNode) {
|
|
switch (propertyNode.valueNode.type) {
|
|
case 'object': {
|
|
unprocessedNodes.push(...propertyNode.valueNode['properties']);
|
|
break;
|
|
}
|
|
case 'array': {
|
|
propertyNode.valueNode['items'].forEach((sequenceNode) => {
|
|
if (sequenceNode && (0, objects_1.isIterable)(sequenceNode['properties'])) {
|
|
unprocessedNodes.push(...sequenceNode['properties']);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
seenKeys[key] = propertyNode.valueNode;
|
|
unprocessedProperties.push(key);
|
|
}
|
|
}
|
|
if (Array.isArray(schema.required)) {
|
|
for (const propertyName of schema.required) {
|
|
if (seenKeys[propertyName] === undefined) {
|
|
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,
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: getWarningMessage(ProblemType.missingRequiredPropWarning, [propertyName]),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
problemArgs: [propertyName],
|
|
problemType: ProblemType.missingRequiredPropWarning,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const propertyProcessed = (prop) => {
|
|
let index = unprocessedProperties.indexOf(prop);
|
|
while (index >= 0) {
|
|
unprocessedProperties.splice(index, 1);
|
|
index = unprocessedProperties.indexOf(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 ((0, objects_1.isBoolean)(propertySchema)) {
|
|
if (!propertySchema) {
|
|
const propertyNode = child.parent;
|
|
validationResult.problems.push({
|
|
location: {
|
|
offset: propertyNode.keyNode.offset,
|
|
length: propertyNode.keyNode.length,
|
|
},
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.errorMessage || localize('DisallowedExtraPropWarning', MSG_PROPERTY_NOT_ALLOWED, propertyName),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
else {
|
|
validationResult.propertiesMatches++;
|
|
validationResult.propertiesValueMatches++;
|
|
}
|
|
}
|
|
else {
|
|
propertySchema.url = schema.url ?? originalSchema.url;
|
|
const propertyValidationResult = new ValidationResult(isKubernetes);
|
|
validate(child, propertySchema, schema, propertyValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(propertyValidationResult);
|
|
validationResult.mergeEnumValues(propertyValidationResult);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (schema.patternProperties) {
|
|
for (const propertyPattern of Object.keys(schema.patternProperties)) {
|
|
const regex = (0, strings_1.safeCreateUnicodeRegExp)(propertyPattern);
|
|
for (const propertyName of unprocessedProperties.slice(0)) {
|
|
if (regex.test(propertyName)) {
|
|
propertyProcessed(propertyName);
|
|
const child = seenKeys[propertyName];
|
|
if (child) {
|
|
const propertySchema = schema.patternProperties[propertyPattern];
|
|
if ((0, objects_1.isBoolean)(propertySchema)) {
|
|
if (!propertySchema) {
|
|
const propertyNode = child.parent;
|
|
validationResult.problems.push({
|
|
location: {
|
|
offset: propertyNode.keyNode.offset,
|
|
length: propertyNode.keyNode.length,
|
|
},
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.errorMessage || localize('DisallowedExtraPropWarning', MSG_PROPERTY_NOT_ALLOWED, propertyName),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
else {
|
|
validationResult.propertiesMatches++;
|
|
validationResult.propertiesValueMatches++;
|
|
}
|
|
}
|
|
else {
|
|
const propertyValidationResult = new ValidationResult(isKubernetes);
|
|
validate(child, propertySchema, schema, propertyValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(propertyValidationResult);
|
|
validationResult.mergeEnumValues(propertyValidationResult);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (typeof schema.additionalProperties === 'object') {
|
|
for (const propertyName of unprocessedProperties) {
|
|
const child = seenKeys[propertyName];
|
|
if (child) {
|
|
const propertyValidationResult = new ValidationResult(isKubernetes);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
validate(child, schema.additionalProperties, schema, propertyValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(propertyValidationResult);
|
|
validationResult.mergeEnumValues(propertyValidationResult);
|
|
}
|
|
}
|
|
}
|
|
else if (schema.additionalProperties === false ||
|
|
(schema.type === 'object' && schema.additionalProperties === undefined && options.disableAdditionalProperties === true)) {
|
|
if (unprocessedProperties.length > 0) {
|
|
const possibleProperties = schema.properties && Object.keys(schema.properties).filter((prop) => !seenKeys[prop]);
|
|
for (const propertyName of unprocessedProperties) {
|
|
const child = seenKeys[propertyName];
|
|
if (child) {
|
|
let propertyNode = null;
|
|
if (child.type !== 'property') {
|
|
propertyNode = child.parent;
|
|
if (propertyNode.type === 'object') {
|
|
propertyNode = propertyNode.properties[0];
|
|
}
|
|
}
|
|
else {
|
|
propertyNode = child;
|
|
}
|
|
const problem = {
|
|
location: {
|
|
offset: propertyNode.keyNode.offset,
|
|
length: propertyNode.keyNode.length,
|
|
},
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: schema.errorMessage || localize('DisallowedExtraPropWarning', MSG_PROPERTY_NOT_ALLOWED, propertyName),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
};
|
|
if (possibleProperties?.length) {
|
|
problem.data = { properties: possibleProperties };
|
|
}
|
|
validationResult.problems.push(problem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ((0, objects_1.isNumber)(schema.maxProperties)) {
|
|
if (node.properties.length > schema.maxProperties) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('MaxPropWarning', 'Object has more properties than limit of {0}.', schema.maxProperties),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
if ((0, objects_1.isNumber)(schema.minProperties)) {
|
|
if (node.properties.length < schema.minProperties) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
}
|
|
if (schema.dependencies) {
|
|
for (const key of Object.keys(schema.dependencies)) {
|
|
const prop = seenKeys[key];
|
|
if (prop) {
|
|
const propertyDep = schema.dependencies[key];
|
|
if (Array.isArray(propertyDep)) {
|
|
for (const requiredProp of propertyDep) {
|
|
if (!seenKeys[requiredProp]) {
|
|
validationResult.problems.push({
|
|
location: { offset: node.offset, length: node.length },
|
|
severity: vscode_languageserver_types_1.DiagnosticSeverity.Warning,
|
|
message: localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}.', requiredProp, key),
|
|
source: getSchemaSource(schema, originalSchema),
|
|
schemaUri: getSchemaUri(schema, originalSchema),
|
|
});
|
|
}
|
|
else {
|
|
validationResult.propertiesValueMatches++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const propertySchema = asSchema(propertyDep);
|
|
if (propertySchema) {
|
|
const propertyValidationResult = new ValidationResult(isKubernetes);
|
|
validate(node, propertySchema, schema, propertyValidationResult, matchingSchemas, options);
|
|
validationResult.mergePropertyMatch(propertyValidationResult);
|
|
validationResult.mergeEnumValues(propertyValidationResult);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const propertyNames = asSchema(schema.propertyNames);
|
|
if (propertyNames) {
|
|
for (const f of node.properties) {
|
|
const key = f.keyNode;
|
|
if (key) {
|
|
validate(key, propertyNames, schema, validationResult, NoOpSchemaCollector.instance, options);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//Alternative comparison is specifically used by the kubernetes/openshift schema but may lead to better results then genericComparison depending on the schema
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas) {
|
|
const compareResult = subValidationResult.compareKubernetes(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);
|
|
}
|
|
return bestMatch;
|
|
}
|
|
//genericComparison tries to find the best matching schema using a generic comparison
|
|
function genericComparison(node, maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas) {
|
|
if (!maxOneMatch &&
|
|
!subValidationResult.hasProblems() &&
|
|
(!bestMatch.validationResult.hasProblems() || callFromAutoComplete)) {
|
|
// no errors, both are equally good matches
|
|
bestMatch.matchingSchemas.merge(subMatchingSchemas);
|
|
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
|
|
bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
|
|
}
|
|
else {
|
|
const compareResult = subValidationResult.compareGeneric(bestMatch.validationResult);
|
|
if (compareResult > 0 ||
|
|
(compareResult === 0 &&
|
|
maxOneMatch &&
|
|
bestMatch.schema.type === 'object' &&
|
|
node.type !== 'null' &&
|
|
node.type !== bestMatch.schema.type)) {
|
|
// 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);
|
|
bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [
|
|
ProblemType.missingRequiredPropWarning,
|
|
ProblemType.typeMismatchWarning,
|
|
ProblemType.constWarning,
|
|
]);
|
|
}
|
|
}
|
|
return bestMatch;
|
|
}
|
|
}
|
|
function getSchemaSource(schema, originalSchema) {
|
|
if (schema) {
|
|
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) {
|
|
const url = vscode_uri_1.URI.parse(uriString);
|
|
if (url.scheme === 'file') {
|
|
label = url.fsPath;
|
|
}
|
|
label = url.toString();
|
|
}
|
|
}
|
|
if (label) {
|
|
return `${YAML_SCHEMA_PREFIX}${label}`;
|
|
}
|
|
}
|
|
return exports.YAML_SOURCE;
|
|
}
|
|
function getSchemaUri(schema, originalSchema) {
|
|
const uriString = schema.url ?? originalSchema.url;
|
|
return uriString ? [uriString] : [];
|
|
}
|
|
function getWarningMessage(problemType, args) {
|
|
return localize(problemType, exports.ProblemTypeMessages[problemType], args.join(' | '));
|
|
}
|
|
});
|
|
//# sourceMappingURL=jsonParser07.js.map
|