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));
}
});
}
}
}
}
}