public validate()

in language-service/src/parser/jsonParser.ts [715:1061]


	public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void {
		if (!matchingSchemas.include(this)) {
			return;
		}

		super.validate(schema, validationResult, matchingSchemas);
		let seenKeys: ASTNodeMap = Object.create(null);
		let unprocessedProperties: string[] = [];
		this.properties.forEach(node => {
			const key: string = node.key.value;

			// Replace the merge key with the actual values of what the node value points to in seen keys
			if (key === "<<" && node.value) {
				switch(node.value.type) {
					case "object": {
						node.value["properties"].forEach(propASTNode => {
							const propKey = propASTNode.key.value;
							seenKeys[propKey] = propASTNode.value;
							unprocessedProperties.push(propKey);
						});
						break;
					}
					case "array": {
						node.value["items"].forEach(sequenceNode => {
							sequenceNode["properties"].forEach(propASTNode => {
								const seqKey = propASTNode.key.value;
								seenKeys[seqKey] = propASTNode.value;
								unprocessedProperties.push(seqKey);
							});
						});
						break;
					}
					default: {
						break;
					}
				}
			} else {
				seenKeys[key] = node.value;
				unprocessedProperties.push(key);
			}
		});

		const findMatchingProperties = (propertyKey: string): ASTNodeMap => {
			let result: ASTNodeMap = Object.create(null);
			const compareKey: string = propertyKey.toUpperCase();

			Object.keys(seenKeys).forEach((propertyName: string) => {
				if (propertyName.toUpperCase() === compareKey) {
					result[propertyName] = seenKeys[propertyName];
				}
			});

			return result;
		}

		const hasProperty = (propertyKey: string): boolean => {
			if (seenKeys[propertyKey]) {
				return true;
			}

			if (schema.properties) {
				const propSchema: JSONSchema = schema.properties[propertyKey];

				if (propSchema) {
					const ignoreKeyCase: boolean = ASTNode.getIgnoreKeyCase(propSchema);

					if (ignoreKeyCase) {
						const matchedKeys: ASTNodeMap = findMatchingProperties(propertyKey);
						if (Object.keys(matchedKeys).length > 0) {
							return true;
						}
					}

					if (Array.isArray(propSchema.aliases)) {
						return propSchema.aliases.some((aliasName: string): boolean => {
							if (seenKeys[aliasName]) {
								return true;
							}

							if (ignoreKeyCase) {
								const matchedKeys: ASTNodeMap = findMatchingProperties(aliasName);
								return Object.keys(matchedKeys).length > 0;
							}

							return false;
						});
					}
				}
			}

			return false;
		}

		if (Array.isArray(schema.required)) {
			schema.required.forEach((propertyName: string) => {
				if (!hasProperty(propertyName)) {
					const key = this.parent && (<PropertyASTNode>this.parent).key;
					const location = key ? { start: key.start, end: key.end } : { start: this.start, end: this.start + 1 };
					validationResult.addProblem({
						location: location,
						severity: ProblemSeverity.Warning,
						getMessage: () => localize('MissingRequiredPropWarning', 'Missing property "{0}".', propertyName)
					});
				}
			});
		}

		const propertyProcessed = (prop: string): void => {
			let index = unprocessedProperties.indexOf(prop);
			while (index >= 0) {
				unprocessedProperties.splice(index, 1);
				index = unprocessedProperties.indexOf(prop);
			}
		};

		if (schema.properties) {
			Object.keys(schema.properties).forEach((schemaPropertyName: string) => {
				const propSchema: JSONSchema = schema.properties[schemaPropertyName];

				let children: ASTNodeMap = {};
				const ignoreKeyCase: boolean = ASTNode.getIgnoreKeyCase(propSchema);

				if (ignoreKeyCase) {
					children = findMatchingProperties(schemaPropertyName);
				}
				else if (seenKeys[schemaPropertyName]) {
					children[schemaPropertyName] = seenKeys[schemaPropertyName];
				}

				if (Array.isArray(propSchema.aliases)) {
					propSchema.aliases.forEach((aliasName: string): void => {
						if (ignoreKeyCase) {
							Object.assign(children, findMatchingProperties(aliasName));
						}
						else if (seenKeys[aliasName]) {
							children[aliasName] = seenKeys[aliasName];
						}
					});
				}

				let child: ASTNode = null;
				const numChildren: number = Object.keys(children).length;
				const generateErrors: boolean = numChildren > 1;

				Object.keys(children).forEach((childKey:string): void => {
					propertyProcessed(childKey);

					if (generateErrors) {
						const childProperty: PropertyASTNode = <PropertyASTNode>(children[childKey].parent);
						validationResult.addProblem({
							location: {start: childProperty.key.start, end: childProperty.key.end},
							severity: ProblemSeverity.Error,
							getMessage: () => localize('DuplicatePropError', 'Multiple properties found matching {0}', schemaPropertyName)
						})
					}
					else {
						child = children[childKey];
					}
				});

				if (child) {
					let propertyValidationResult = new ValidationResult();
					child.validate(propSchema, propertyValidationResult, matchingSchemas);
					validationResult.mergePropertyMatch(propertyValidationResult);
				}
			});
		}

		if (schema.patternProperties) {
			Object.keys(schema.patternProperties).forEach((propertyPattern: string) => {
				const ignoreKeyCase: boolean = ASTNode.getIgnoreKeyCase(schema.patternProperties[propertyPattern]);
				const regex = new RegExp(propertyPattern, ignoreKeyCase ? "i" : "");
				unprocessedProperties.slice(0).forEach((propertyName: string) => {
					if (regex.test(propertyName)) {
						propertyProcessed(propertyName);
						const child = seenKeys[propertyName];
						if (child) {
							let propertyValidationResult = new ValidationResult();
							const childSchema: JSONSchema = schema.patternProperties[propertyPattern];
							child.validate(childSchema, propertyValidationResult, matchingSchemas);
							validationResult.mergePropertyMatch(propertyValidationResult);
						}
					}
				});
			});
		}

		if (typeof schema.additionalProperties === 'object') {
			unprocessedProperties.forEach((propertyName: string) => {
				const child = seenKeys[propertyName];
				if (child) {
					let propertyValidationResult = new ValidationResult();
					child.validate(<any>schema.additionalProperties, propertyValidationResult, matchingSchemas);
					validationResult.mergePropertyMatch(propertyValidationResult);
				}
			});
		} else if (schema.additionalProperties === false) {
			if (unprocessedProperties.length > 0) {
				unprocessedProperties.forEach((propertyName: string) => {
					//Auto-complete can insert a "holder" node when parsing, do not count it as an error
					//against additionalProperties
					if (propertyName !== nodeHolder) {
						const child: ASTNode = seenKeys[propertyName];
						if (child) {
							let errorLocation: IRange = null;
							let errorNode: ASTNode = child;

							if (errorNode.type !== "property" && errorNode.parent) {
								if (errorNode.parent.type === "property") {
									//This works for StringASTNode
									errorNode = errorNode.parent;
								} else if (errorNode.parent.type === "object") {
									//The tree structure and parent links can be weird
									//NullASTNode's parent will be the object and not the property
									const parentObject: ObjectASTNode = <ObjectASTNode>errorNode.parent;
									parentObject.properties.some((propNode: PropertyASTNode) => {
										if (propNode.value == child) {
											errorNode = propNode;
											return true;
										}
										return false;
									});
								}
							}

							if (errorNode.type === "property") {
								const propertyNode: PropertyASTNode = <PropertyASTNode>errorNode;
								errorLocation = {
									start: propertyNode.key.start,
									end: propertyNode.key.end
								};
							} else {
								errorLocation = {
									start: errorNode.start,
									end: errorNode.end
								};
							}

							validationResult.addProblem({
								location: errorLocation,
								severity: ProblemSeverity.Warning,
								getMessage: () => schema.errorMessage || localize('DisallowedExtraPropWarning', 'Unexpected property {0}', propertyName)
							});
						}
					}
				});
			}
		}

		if (schema.maxProperties) {
			if (this.properties.length > schema.maxProperties) {
				validationResult.addProblem({
					location: { start: this.start, end: this.end },
					severity: ProblemSeverity.Warning,
					getMessage: () => localize('MaxPropWarning', 'Object has more properties than limit of {0}.', schema.maxProperties)
				});
			}
		}

		if (schema.minProperties) {
			if (this.properties.length < schema.minProperties) {
				validationResult.addProblem({
					location: { start: this.start, end: this.end },
					severity: ProblemSeverity.Warning,
					getMessage: () => localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties)
				});
			}
		}

		if (schema.dependencies) {
			Object.keys(schema.dependencies).forEach((key: string) => {
				if (hasProperty(key)) {
					const propertyDep = schema.dependencies[key]
					if (Array.isArray(propertyDep)) {
						propertyDep.forEach((requiredProp: string) => {
							if (!hasProperty(requiredProp)) {
								validationResult.addProblem({
									location: { start: this.start, end: this.end },
									severity: ProblemSeverity.Warning,
									getMessage: () => localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}.', requiredProp, key)
								});
							} else {
								validationResult.propertiesValueMatches++;
							}
						});
					} else if (propertyDep) {
						let propertyvalidationResult = new ValidationResult();
						this.validate(propertyDep, propertyvalidationResult, matchingSchemas);
						validationResult.mergePropertyMatch(propertyvalidationResult);
					}
				}
			});
		}

		if (schema.firstProperty?.length) {
			const firstProperty = this.properties[0];
			if (firstProperty?.key?.value) {
				let firstPropKey: string = firstProperty.key.value;

				if (!schema.firstProperty.some((listProperty: string) => {
					if (listProperty === firstPropKey) {
						return true;
					}

					if (schema.properties) {
						const propertySchema: JSONSchema = schema.properties[listProperty];
						if (propertySchema) {
							const ignoreCase: boolean = ASTNode.getIgnoreKeyCase(propertySchema);
							if (ignoreCase && listProperty.toUpperCase() === firstPropKey.toUpperCase()) {
								return true;
							}

							if (Array.isArray(propertySchema.aliases)) {
								return propertySchema.aliases.some((aliasName: string): boolean => {
									if (aliasName === firstPropKey) {
										return true;
									}

									return ignoreCase && aliasName.toUpperCase() === firstPropKey.toUpperCase();
								});
							}
						}
					}

					return false;
				})) {
					if (schema.firstProperty.length == 1) {
						validationResult.addProblem({
							location: { start: firstProperty.start, end: firstProperty.end },
							severity: ProblemSeverity.Error,
							getMessage: () => localize('firstPropertyError', "The first property must be {0}", schema.firstProperty[0])
						});
					}
					else {
						validationResult.addProblem({
							location: { start: firstProperty.start, end: firstProperty.end },
							severity: ProblemSeverity.Error,
							getMessage: () => {
								const separator: string = localize('listSeparator', ", ");
								return localize('firstPropertyErrorList', "The first property must be one of: {0}", schema.firstProperty.join(separator));
							}
						});
					}
				}
			}
		}
	}