mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-22 09:38:38 +01:00
1014 lines
31 KiB
TypeScript
1014 lines
31 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import nls = require('./utils/nls');
|
|
import Json = require('./json-toolbox/json');
|
|
import JsonSchema = require('./json-toolbox/jsonSchema');
|
|
import {JSONLocation} from './jsonLocation';
|
|
|
|
export interface IRange {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
export interface IError {
|
|
location: IRange;
|
|
message: string;
|
|
}
|
|
|
|
export class ASTNode {
|
|
public start: number;
|
|
public end: number;
|
|
public type: string;
|
|
public parent: ASTNode;
|
|
public name: string;
|
|
|
|
constructor(parent: ASTNode, type: string, name: string, start: number, end?: number) {
|
|
this.type = type;
|
|
this.name = name;
|
|
this.start = start;
|
|
this.end = end;
|
|
this.parent = parent;
|
|
}
|
|
|
|
public getNodeLocation(): JSONLocation {
|
|
let path = this.parent ? this.parent.getNodeLocation() : new JSONLocation([]);
|
|
if (this.name) {
|
|
path = path.append(this.name);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
|
|
public getChildNodes(): ASTNode[] {
|
|
return [];
|
|
}
|
|
|
|
public getValue(): any {
|
|
// override in children
|
|
return;
|
|
}
|
|
|
|
public contains(offset: number, includeRightBound: boolean = false): boolean {
|
|
return offset >= this.start && offset < this.end || includeRightBound && offset === this.end;
|
|
}
|
|
|
|
public visit(visitor: (node: ASTNode) => boolean): boolean {
|
|
return visitor(this);
|
|
}
|
|
|
|
public getNodeFromOffset(offset: number): ASTNode {
|
|
let findNode = (node: ASTNode): ASTNode => {
|
|
if (offset >= node.start && offset < node.end) {
|
|
let children = node.getChildNodes();
|
|
for (let i = 0; i < children.length && children[i].start <= offset; i++) {
|
|
let item = findNode(children[i]);
|
|
if (item) {
|
|
return item;
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
return null;
|
|
};
|
|
return findNode(this);
|
|
}
|
|
|
|
public getNodeFromOffsetEndInclusive(offset: number): ASTNode {
|
|
let findNode = (node: ASTNode): ASTNode => {
|
|
if (offset >= node.start && offset <= node.end) {
|
|
let children = node.getChildNodes();
|
|
for (let i = 0; i < children.length && children[i].start <= offset; i++) {
|
|
let item = findNode(children[i]);
|
|
if (item) {
|
|
return item;
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
return null;
|
|
};
|
|
return findNode(this);
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, validationResult: ValidationResult, matchingSchemas: IApplicableSchema[], offset: number = -1): void {
|
|
if (offset !== -1 && !this.contains(offset)) {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(schema.type)) {
|
|
if ((<string[]>schema.type).indexOf(this.type) === -1) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}', schema.type.join(', '))
|
|
});
|
|
}
|
|
}
|
|
else if (schema.type) {
|
|
if (this.type !== schema.type) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('typeMismatchWarning', 'Incorrect type. Expected "{0}"', schema.type)
|
|
});
|
|
}
|
|
}
|
|
if (Array.isArray(schema.allOf)) {
|
|
schema.allOf.forEach((subSchema) => {
|
|
this.validate(subSchema, validationResult, matchingSchemas, offset);
|
|
});
|
|
}
|
|
if (schema.not) {
|
|
let subValidationResult = new ValidationResult();
|
|
let subMatchingSchemas: IApplicableSchema[] = [];
|
|
this.validate(schema.not, subValidationResult, subMatchingSchemas, offset);
|
|
if (!subValidationResult.hasErrors()) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('notSchemaWarning', "Matches a schema that is not allowed.")
|
|
});
|
|
}
|
|
if (matchingSchemas) {
|
|
subMatchingSchemas.forEach((ms) => {
|
|
ms.inverted = !ms.inverted;
|
|
matchingSchemas.push(ms);
|
|
});
|
|
}
|
|
}
|
|
|
|
let testAlternatives = (alternatives: JsonSchema.IJSONSchema[], maxOneMatch: boolean) => {
|
|
let matches = [];
|
|
|
|
// remember the best match that is used for error messages
|
|
let bestMatch: { schema: JsonSchema.IJSONSchema; validationResult: ValidationResult; matchingSchemas: IApplicableSchema[]; } = null;
|
|
alternatives.forEach((subSchema) => {
|
|
let subValidationResult = new ValidationResult();
|
|
let subMatchingSchemas: IApplicableSchema[] = [];
|
|
this.validate(subSchema, subValidationResult, subMatchingSchemas);
|
|
if (!subValidationResult.hasErrors()) {
|
|
matches.push(subSchema);
|
|
}
|
|
if (!bestMatch) {
|
|
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
|
|
} else {
|
|
if (!maxOneMatch && !subValidationResult.hasErrors() && !bestMatch.validationResult.hasErrors()) {
|
|
// no errors, both are equally good matches
|
|
bestMatch.matchingSchemas.push.apply(bestMatch.matchingSchemas, subMatchingSchemas);
|
|
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
|
|
bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
|
|
} else {
|
|
let compareResult = subValidationResult.compare(bestMatch.validationResult);
|
|
if (compareResult > 0) {
|
|
// our node is the best matching so far
|
|
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
|
|
} else if (compareResult === 0) {
|
|
// there's already a best matching but we are as good
|
|
bestMatch.matchingSchemas.push.apply(bestMatch.matchingSchemas, subMatchingSchemas);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (matches.length > 1 && maxOneMatch) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.start + 1 },
|
|
message: nls.localize('oneOfWarning', "Matches multiple schemas when only one must validate.")
|
|
});
|
|
}
|
|
if (bestMatch !== null) {
|
|
validationResult.merge(bestMatch.validationResult);
|
|
validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches;
|
|
validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches;
|
|
if (matchingSchemas) {
|
|
matchingSchemas.push.apply(matchingSchemas, bestMatch.matchingSchemas);
|
|
}
|
|
}
|
|
return matches.length;
|
|
};
|
|
if (Array.isArray(schema.anyOf)) {
|
|
testAlternatives(schema.anyOf, false);
|
|
}
|
|
if (Array.isArray(schema.oneOf)) {
|
|
testAlternatives(schema.oneOf, true);
|
|
}
|
|
|
|
if (Array.isArray(schema.enum)) {
|
|
if (schema.enum.indexOf(this.getValue()) === -1) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('enumWarning', 'Value is not an accepted value. Valid values: {0}', JSON.stringify(schema.enum))
|
|
});
|
|
} else {
|
|
validationResult.enumValueMatch = true;
|
|
}
|
|
}
|
|
|
|
if (matchingSchemas !== null) {
|
|
matchingSchemas.push({ node: this, schema: schema });
|
|
}
|
|
}
|
|
}
|
|
|
|
export class NullASTNode extends ASTNode {
|
|
|
|
constructor(parent: ASTNode, name: string, start: number, end?: number) {
|
|
super(parent, 'null', name, start, end);
|
|
}
|
|
|
|
public getValue(): any {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export class BooleanASTNode extends ASTNode {
|
|
|
|
private value: boolean;
|
|
|
|
constructor(parent: ASTNode, name: string, value: boolean, start: number, end?: number) {
|
|
super(parent, 'boolean', name, start, end);
|
|
this.value = value;
|
|
}
|
|
|
|
public getValue(): any {
|
|
return this.value;
|
|
}
|
|
|
|
}
|
|
|
|
export class ArrayASTNode extends ASTNode {
|
|
|
|
public items: ASTNode[];
|
|
|
|
constructor(parent: ASTNode, name: string, start: number, end?: number) {
|
|
super(parent, 'array', name, start, end);
|
|
this.items = [];
|
|
}
|
|
|
|
public getChildNodes(): ASTNode[] {
|
|
return this.items;
|
|
}
|
|
|
|
public getValue(): any {
|
|
return this.items.map((v) => v.getValue());
|
|
}
|
|
|
|
public addItem(item: ASTNode): boolean {
|
|
if (item) {
|
|
this.items.push(item);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public visit(visitor: (node: ASTNode) => boolean): boolean {
|
|
let ctn = visitor(this);
|
|
for (let i = 0; i < this.items.length && ctn; i++) {
|
|
ctn = this.items[i].visit(visitor);
|
|
}
|
|
return ctn;
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, validationResult: ValidationResult, matchingSchemas: IApplicableSchema[], offset: number = -1): void {
|
|
if (offset !== -1 && !this.contains(offset)) {
|
|
return;
|
|
}
|
|
super.validate(schema, validationResult, matchingSchemas, offset);
|
|
|
|
if (Array.isArray(schema.items)) {
|
|
let subSchemas: JsonSchema.IJSONSchema[] = schema.items;
|
|
subSchemas.forEach((subSchema, index) => {
|
|
let itemValidationResult = new ValidationResult();
|
|
let item = this.items[index];
|
|
if (item) {
|
|
item.validate(subSchema, itemValidationResult, matchingSchemas, offset);
|
|
validationResult.mergePropertyMatch(itemValidationResult);
|
|
} else if (this.items.length >= schema.items.length) {
|
|
validationResult.propertiesValueMatches++;
|
|
}
|
|
});
|
|
|
|
if (schema.additionalItems === false && this.items.length > subSchemas.length) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer', subSchemas.length)
|
|
});
|
|
} else if (this.items.length >= schema.items.length) {
|
|
validationResult.propertiesValueMatches += (this.items.length - schema.items.length);
|
|
}
|
|
}
|
|
else if (schema.items) {
|
|
this.items.forEach((item) => {
|
|
let itemValidationResult = new ValidationResult();
|
|
item.validate(schema.items, itemValidationResult, matchingSchemas, offset);
|
|
validationResult.mergePropertyMatch(itemValidationResult);
|
|
});
|
|
}
|
|
|
|
if (schema.minItems && this.items.length < schema.minItems) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('minItemsWarning', 'Array has too few items. Expected {0} or more', schema.minItems)
|
|
});
|
|
}
|
|
|
|
if (schema.maxItems && this.items.length > schema.maxItems) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('maxItemsWarning', 'Array has too many items. Expected {0} or fewer', schema.minItems)
|
|
});
|
|
}
|
|
|
|
if (schema.uniqueItems === true) {
|
|
let values = this.items.map((node) => {
|
|
return node.getValue();
|
|
});
|
|
let duplicates = values.some((value, index) => {
|
|
return index !== values.lastIndexOf(value);
|
|
});
|
|
if (duplicates) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('uniqueItemsWarning', 'Array has duplicate items')
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export class NumberASTNode extends ASTNode {
|
|
|
|
public isInteger: boolean;
|
|
public value: number;
|
|
|
|
constructor(parent: ASTNode, name: string, start: number, end?: number) {
|
|
super(parent, 'number', name, start, end);
|
|
this.isInteger = true;
|
|
this.value = Number.NaN;
|
|
}
|
|
|
|
public getValue(): any {
|
|
return this.value;
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, validationResult: ValidationResult, matchingSchemas: IApplicableSchema[], offset: number = -1): void {
|
|
if (offset !== -1 && !this.contains(offset)) {
|
|
return;
|
|
}
|
|
|
|
// work around type validation in the base class
|
|
let typeIsInteger = false;
|
|
if (schema.type === 'integer' || (Array.isArray(schema.type) && (<string[]>schema.type).indexOf('integer') !== -1)) {
|
|
typeIsInteger = true;
|
|
}
|
|
if (typeIsInteger && this.isInteger === true) {
|
|
this.type = 'integer';
|
|
}
|
|
super.validate(schema, validationResult, matchingSchemas, offset);
|
|
this.type = 'number';
|
|
|
|
let val = this.getValue();
|
|
|
|
if (typeof schema.multipleOf === 'number') {
|
|
if (val % schema.multipleOf !== 0) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('multipleOfWarning', 'Value is not divisible by {0}', schema.multipleOf)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (typeof schema.minimum === 'number') {
|
|
if (schema.exclusiveMinimum && val <= schema.minimum) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('exclusiveMinimumWarning', 'Value is below the exclusive minimum of {0}', schema.minimum)
|
|
});
|
|
}
|
|
if (!schema.exclusiveMinimum && val < schema.minimum) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('minimumWarning', 'Value is below the minimum of {0}', schema.minimum)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (typeof schema.maximum === 'number') {
|
|
if (schema.exclusiveMaximum && val >= schema.maximum) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('exclusiveMaximumWarning', 'Value is above the exclusive maximum of {0}', schema.maximum)
|
|
});
|
|
}
|
|
if (!schema.exclusiveMaximum && val > schema.maximum) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('maximumWarning', 'Value is above the maximum of {0}', schema.maximum)
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
export class StringASTNode extends ASTNode {
|
|
public isKey: boolean;
|
|
public value: string;
|
|
|
|
constructor(parent: ASTNode, name: string, isKey: boolean, start: number, end?: number) {
|
|
super(parent, 'string', name, start, end);
|
|
this.isKey = isKey;
|
|
this.value = '';
|
|
}
|
|
|
|
public getValue(): any {
|
|
return this.value;
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, validationResult: ValidationResult, matchingSchemas: IApplicableSchema[], offset: number = -1): void {
|
|
if (offset !== -1 && !this.contains(offset)) {
|
|
return;
|
|
}
|
|
super.validate(schema, validationResult, matchingSchemas, offset);
|
|
|
|
if (schema.minLength && this.value.length < schema.minLength) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('minLengthWarning', 'String is shorter than the minimum length of ', schema.minLength)
|
|
});
|
|
}
|
|
|
|
if (schema.maxLength && this.value.length > schema.maxLength) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('maxLengthWarning', 'String is shorter than the maximum length of ', schema.maxLength)
|
|
});
|
|
}
|
|
|
|
if (schema.pattern) {
|
|
let regex = new RegExp(schema.pattern);
|
|
if (!regex.test(this.value)) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: schema.errorMessage || nls.localize('patternWarning', 'String does not match the pattern of "{0}"', schema.pattern)
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
export class PropertyASTNode extends ASTNode {
|
|
public key: StringASTNode;
|
|
public value: ASTNode;
|
|
public colonOffset: number;
|
|
|
|
constructor(parent: ASTNode, key: StringASTNode) {
|
|
super(parent, 'property', null, key.start);
|
|
this.key = key;
|
|
key.parent = this;
|
|
key.name = key.value;
|
|
this.colonOffset = -1;
|
|
}
|
|
|
|
public getChildNodes(): ASTNode[] {
|
|
return this.value ? [this.key, this.value] : [this.key];
|
|
}
|
|
|
|
public setValue(value: ASTNode): boolean {
|
|
this.value = value;
|
|
return value !== null;
|
|
}
|
|
|
|
public visit(visitor: (node: ASTNode) => boolean): boolean {
|
|
return visitor(this) && this.key.visit(visitor) && this.value && this.value.visit(visitor);
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, validationResult: ValidationResult, matchingSchemas: IApplicableSchema[], offset: number = -1): void {
|
|
if (offset !== -1 && !this.contains(offset)) {
|
|
return;
|
|
}
|
|
if (this.value) {
|
|
this.value.validate(schema, validationResult, matchingSchemas, offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class ObjectASTNode extends ASTNode {
|
|
public properties: PropertyASTNode[];
|
|
|
|
constructor(parent: ASTNode, name: string, start: number, end?: number) {
|
|
super(parent, 'object', name, start, end);
|
|
|
|
this.properties = [];
|
|
}
|
|
|
|
public getChildNodes(): ASTNode[] {
|
|
return this.properties;
|
|
}
|
|
|
|
public addProperty(node: PropertyASTNode): boolean {
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
this.properties.push(node);
|
|
return true;
|
|
}
|
|
|
|
public getFirstProperty(key: string): PropertyASTNode {
|
|
for (let i = 0; i < this.properties.length; i++) {
|
|
if (this.properties[i].key.value === key) {
|
|
return this.properties[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public getKeyList(): string[] {
|
|
return this.properties.map((p) => p.key.getValue());
|
|
}
|
|
|
|
public getValue(): any {
|
|
let value: any = {};
|
|
this.properties.forEach((p) => {
|
|
let v = p.value && p.value.getValue();
|
|
if (v) {
|
|
value[p.key.getValue()] = v;
|
|
}
|
|
});
|
|
return value;
|
|
}
|
|
|
|
public visit(visitor: (node: ASTNode) => boolean): boolean {
|
|
let ctn = visitor(this);
|
|
for (let i = 0; i < this.properties.length && ctn; i++) {
|
|
ctn = this.properties[i].visit(visitor);
|
|
}
|
|
return ctn;
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, validationResult: ValidationResult, matchingSchemas: IApplicableSchema[], offset: number = -1): void {
|
|
if (offset !== -1 && !this.contains(offset)) {
|
|
return;
|
|
}
|
|
|
|
super.validate(schema, validationResult, matchingSchemas, offset);
|
|
let seenKeys: { [key: string]: ASTNode } = {};
|
|
let unprocessedProperties: string[] = [];
|
|
this.properties.forEach((node) => {
|
|
let key = node.key.value;
|
|
seenKeys[key] = node.value;
|
|
unprocessedProperties.push(key);
|
|
});
|
|
|
|
if (Array.isArray(schema.required)) {
|
|
schema.required.forEach((propertyName: string) => {
|
|
if (!seenKeys[propertyName]) {
|
|
let key = this.parent && this.parent && (<PropertyASTNode>this.parent).key;
|
|
let location = key ? { start: key.start, end: key.end } : { start: this.start, end: this.start + 1 };
|
|
validationResult.warnings.push({
|
|
location: location,
|
|
message: nls.localize('MissingRequiredPropWarning', 'Missing property "{0}"', propertyName)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
let propertyProcessed = (prop: string) => {
|
|
let index = unprocessedProperties.indexOf(prop);
|
|
while (index >= 0) {
|
|
unprocessedProperties.splice(index, 1);
|
|
index = unprocessedProperties.indexOf(prop);
|
|
}
|
|
};
|
|
|
|
if (schema.properties) {
|
|
Object.keys(schema.properties).forEach((propertyName: string) => {
|
|
propertyProcessed(propertyName);
|
|
let prop = schema.properties[propertyName];
|
|
let child = seenKeys[propertyName];
|
|
if (child) {
|
|
let propertyvalidationResult = new ValidationResult();
|
|
child.validate(prop, propertyvalidationResult, matchingSchemas, offset);
|
|
validationResult.mergePropertyMatch(propertyvalidationResult);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
if (schema.patternProperties) {
|
|
Object.keys(schema.patternProperties).forEach((propertyPattern: string) => {
|
|
let regex = new RegExp(propertyPattern);
|
|
unprocessedProperties.slice(0).forEach((propertyName: string) => {
|
|
if (regex.test(propertyName)) {
|
|
propertyProcessed(propertyName);
|
|
let child = seenKeys[propertyName];
|
|
if (child) {
|
|
let propertyvalidationResult = new ValidationResult();
|
|
child.validate(schema.patternProperties[propertyPattern], propertyvalidationResult, matchingSchemas, offset);
|
|
validationResult.mergePropertyMatch(propertyvalidationResult);
|
|
}
|
|
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (schema.additionalProperties) {
|
|
unprocessedProperties.forEach((propertyName: string) => {
|
|
let child = seenKeys[propertyName];
|
|
if (child) {
|
|
let propertyvalidationResult = new ValidationResult();
|
|
child.validate(schema.additionalProperties, propertyvalidationResult, matchingSchemas, offset);
|
|
validationResult.mergePropertyMatch(propertyvalidationResult);
|
|
}
|
|
});
|
|
} else if (schema.additionalProperties === false) {
|
|
if (unprocessedProperties.length > 0) {
|
|
unprocessedProperties.forEach((propertyName: string) => {
|
|
let child = seenKeys[propertyName];
|
|
if (child) {
|
|
let propertyNode = <PropertyASTNode>child.parent;
|
|
|
|
validationResult.warnings.push({
|
|
location: { start: propertyNode.key.start, end: propertyNode.key.end },
|
|
message: nls.localize('DisallowedExtraPropWarning', 'Property {0} is not allowed', propertyName)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (schema.maxProperties) {
|
|
if (this.properties.length > schema.maxProperties) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('MaxPropWarning', 'Object has more properties than limit of {0}', schema.maxProperties)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (schema.minProperties) {
|
|
if (this.properties.length < schema.minProperties) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (schema.dependencies) {
|
|
Object.keys(schema.dependencies).forEach((key: string) => {
|
|
let prop = seenKeys[key];
|
|
if (prop) {
|
|
if (Array.isArray(schema.dependencies[key])) {
|
|
let valueAsArray: string[] = schema.dependencies[key];
|
|
valueAsArray.forEach((requiredProp: string) => {
|
|
if (!seenKeys[requiredProp]) {
|
|
validationResult.warnings.push({
|
|
location: { start: this.start, end: this.end },
|
|
message: nls.localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}', requiredProp, key)
|
|
});
|
|
} else {
|
|
validationResult.propertiesValueMatches++;
|
|
}
|
|
});
|
|
} else if (schema.dependencies[key]) {
|
|
let valueAsSchema: JsonSchema.IJSONSchema = schema.dependencies[key];
|
|
let propertyvalidationResult = new ValidationResult();
|
|
this.validate(valueAsSchema, propertyvalidationResult, matchingSchemas, offset);
|
|
validationResult.mergePropertyMatch(propertyvalidationResult);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export class JSONDocumentConfig {
|
|
public ignoreDanglingComma: boolean;
|
|
|
|
constructor() {
|
|
this.ignoreDanglingComma = false;
|
|
}
|
|
}
|
|
|
|
export interface IApplicableSchema {
|
|
node: ASTNode;
|
|
inverted?: boolean;
|
|
schema: JsonSchema.IJSONSchema;
|
|
}
|
|
|
|
export class ValidationResult {
|
|
public errors: IError[];
|
|
public warnings: IError[];
|
|
|
|
public propertiesMatches: number;
|
|
public propertiesValueMatches: number;
|
|
public enumValueMatch: boolean;
|
|
|
|
constructor() {
|
|
this.errors = [];
|
|
this.warnings = [];
|
|
this.propertiesMatches = 0;
|
|
this.propertiesValueMatches = 0;
|
|
this.enumValueMatch = false;
|
|
}
|
|
|
|
public hasErrors(): boolean {
|
|
return !!this.errors.length || !!this.warnings.length;
|
|
}
|
|
|
|
public mergeAll(validationResults: ValidationResult[]): void {
|
|
validationResults.forEach((validationResult) => {
|
|
this.merge(validationResult);
|
|
});
|
|
}
|
|
|
|
public merge(validationResult: ValidationResult): void {
|
|
this.errors = this.errors.concat(validationResult.errors);
|
|
this.warnings = this.warnings.concat(validationResult.warnings);
|
|
}
|
|
|
|
public mergePropertyMatch(propertyValidationResult: ValidationResult): void {
|
|
this.merge(propertyValidationResult);
|
|
this.propertiesMatches++;
|
|
if (propertyValidationResult.enumValueMatch || !propertyValidationResult.hasErrors() && propertyValidationResult.propertiesMatches) {
|
|
this.propertiesValueMatches++;
|
|
}
|
|
}
|
|
|
|
public compare(other: ValidationResult): number {
|
|
let hasErrors = this.hasErrors();
|
|
if (hasErrors !== other.hasErrors()) {
|
|
return hasErrors ? -1 : 1;
|
|
}
|
|
if (this.enumValueMatch !== other.enumValueMatch) {
|
|
return other.enumValueMatch ? -1 : 1;
|
|
}
|
|
if (this.propertiesValueMatches !== other.propertiesValueMatches) {
|
|
return this.propertiesValueMatches - other.propertiesValueMatches;
|
|
}
|
|
return this.propertiesMatches - other.propertiesMatches;
|
|
}
|
|
|
|
}
|
|
|
|
export class JSONDocument {
|
|
public config: JSONDocumentConfig;
|
|
public root: ASTNode;
|
|
|
|
private validationResult: ValidationResult;
|
|
|
|
constructor(config: JSONDocumentConfig) {
|
|
this.config = config;
|
|
this.validationResult = new ValidationResult();
|
|
}
|
|
|
|
public get errors(): IError[] {
|
|
return this.validationResult.errors;
|
|
}
|
|
|
|
public get warnings(): IError[] {
|
|
return this.validationResult.warnings;
|
|
}
|
|
|
|
public getNodeFromOffset(offset: number): ASTNode {
|
|
return this.root && this.root.getNodeFromOffset(offset);
|
|
}
|
|
|
|
public getNodeFromOffsetEndInclusive(offset: number): ASTNode {
|
|
return this.root && this.root.getNodeFromOffsetEndInclusive(offset);
|
|
}
|
|
|
|
public visit(visitor: (node: ASTNode) => boolean): void {
|
|
if (this.root) {
|
|
this.root.visit(visitor);
|
|
}
|
|
}
|
|
|
|
public validate(schema: JsonSchema.IJSONSchema, matchingSchemas: IApplicableSchema[] = null, offset: number = -1): void {
|
|
if (this.root) {
|
|
this.root.validate(schema, this.validationResult, matchingSchemas, offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parse(text: string, config = new JSONDocumentConfig()): JSONDocument {
|
|
|
|
let _doc = new JSONDocument(config);
|
|
let _scanner = Json.createScanner(text, true);
|
|
|
|
function _accept(token: Json.SyntaxKind): boolean {
|
|
if (_scanner.getToken() === token) {
|
|
_scanner.scan();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function _error<T extends ASTNode>(message: string, node: T = null, skipUntilAfter: Json.SyntaxKind[] = [], skipUntil: Json.SyntaxKind[] = []): T {
|
|
if (_doc.errors.length === 0 || _doc.errors[0].location.start !== _scanner.getTokenOffset()) {
|
|
// ignore multiple errors on the same offset
|
|
let error = { message: message, location: { start: _scanner.getTokenOffset(), end: _scanner.getTokenOffset() + _scanner.getTokenLength() } };
|
|
_doc.errors.push(error);
|
|
}
|
|
|
|
if (node) {
|
|
_finalize(node, false);
|
|
}
|
|
if (skipUntilAfter.length + skipUntil.length > 0) {
|
|
let token = _scanner.getToken();
|
|
while (token !== Json.SyntaxKind.EOF) {
|
|
if (skipUntilAfter.indexOf(token) !== -1) {
|
|
_scanner.scan();
|
|
break;
|
|
} else if (skipUntil.indexOf(token) !== -1) {
|
|
break;
|
|
}
|
|
token = _scanner.scan();
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function _checkScanError(): boolean {
|
|
switch (_scanner.getTokenError()) {
|
|
case Json.ScanError.InvalidUnicode:
|
|
_error(nls.localize('InvalidUnicode', 'Invalid unicode sequence in string'));
|
|
return true;
|
|
case Json.ScanError.InvalidEscapeCharacter:
|
|
_error(nls.localize('InvalidEscapeCharacter', 'Invalid escape character in string'));
|
|
return true;
|
|
case Json.ScanError.UnexpectedEndOfNumber:
|
|
_error(nls.localize('UnexpectedEndOfNumber', 'Unexpected end of number'));
|
|
return true;
|
|
case Json.ScanError.UnexpectedEndOfComment:
|
|
_error(nls.localize('UnexpectedEndOfComment', 'Unexpected end of comment'));
|
|
return true;
|
|
case Json.ScanError.UnexpectedEndOfString:
|
|
_error(nls.localize('UnexpectedEndOfString', 'Unexpected end of string'));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function _finalize<T extends ASTNode>(node: T, scanNext: boolean): T {
|
|
node.end = _scanner.getTokenOffset() + _scanner.getTokenLength();
|
|
|
|
if (scanNext) {
|
|
_scanner.scan();
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function _parseArray(parent: ASTNode, name: string): ArrayASTNode {
|
|
if (_scanner.getToken() !== Json.SyntaxKind.OpenBracketToken) {
|
|
return null;
|
|
}
|
|
let node = new ArrayASTNode(parent, name, _scanner.getTokenOffset());
|
|
_scanner.scan(); // consume OpenBracketToken
|
|
|
|
let count = 0;
|
|
if (node.addItem(_parseValue(node, '' + count++))) {
|
|
while (_accept(Json.SyntaxKind.CommaToken)) {
|
|
if (!node.addItem(_parseValue(node, '' + count++)) && !_doc.config.ignoreDanglingComma) {
|
|
_error(nls.localize('ValueExpected', 'Value expected'));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_scanner.getToken() !== Json.SyntaxKind.CloseBracketToken) {
|
|
return _error(nls.localize('ExpectedCloseBracket', 'Expected comma or closing bracket'), node);
|
|
}
|
|
|
|
return _finalize(node, true);
|
|
}
|
|
|
|
function _parseProperty(parent: ObjectASTNode, keysSeen: any): PropertyASTNode {
|
|
|
|
let key = _parseString(null, null, true);
|
|
if (!key) {
|
|
if (_scanner.getToken() === Json.SyntaxKind.Unknown) {
|
|
// give a more helpful error message
|
|
let value = _scanner.getTokenValue();
|
|
if (value.length > 0 && (value.charAt(0) === '\'' || Json.isLetter(value.charAt(0).charCodeAt(0)))) {
|
|
_error(nls.localize('DoubleQuotesExpected', 'Property keys must be doublequoted'));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
let node = new PropertyASTNode(parent, key);
|
|
|
|
if (keysSeen[key.value]) {
|
|
_doc.warnings.push({ location: { start: node.key.start, end: node.key.end }, message: nls.localize('DuplicateKeyWarning', "Duplicate object key") });
|
|
}
|
|
keysSeen[key.value] = true;
|
|
|
|
if (_scanner.getToken() === Json.SyntaxKind.ColonToken) {
|
|
node.colonOffset = _scanner.getTokenOffset();
|
|
} else {
|
|
return _error(nls.localize('ColonExpected', 'Colon expected'), node, [], [Json.SyntaxKind.CloseBraceToken, Json.SyntaxKind.CommaToken]);
|
|
}
|
|
|
|
_scanner.scan(); // consume ColonToken
|
|
|
|
if (!node.setValue(_parseValue(node, key.value))) {
|
|
return _error(nls.localize('ValueExpected', 'Value expected'), node, [], [Json.SyntaxKind.CloseBraceToken, Json.SyntaxKind.CommaToken]);
|
|
}
|
|
node.end = node.value.end;
|
|
return node;
|
|
}
|
|
|
|
function _parseObject(parent: ASTNode, name: string): ObjectASTNode {
|
|
if (_scanner.getToken() !== Json.SyntaxKind.OpenBraceToken) {
|
|
return null;
|
|
}
|
|
let node = new ObjectASTNode(parent, name, _scanner.getTokenOffset());
|
|
_scanner.scan(); // consume OpenBraceToken
|
|
|
|
let keysSeen: any = {};
|
|
if (node.addProperty(_parseProperty(node, keysSeen))) {
|
|
while (_accept(Json.SyntaxKind.CommaToken)) {
|
|
if (!node.addProperty(_parseProperty(node, keysSeen)) && !_doc.config.ignoreDanglingComma) {
|
|
_error(nls.localize('PropertyExpected', 'Property expected'));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_scanner.getToken() !== Json.SyntaxKind.CloseBraceToken) {
|
|
return _error(nls.localize('ExpectedCloseBrace', 'Expected comma or closing brace'), node);
|
|
}
|
|
return _finalize(node, true);
|
|
}
|
|
|
|
function _parseString(parent: ASTNode, name: string, isKey: boolean): StringASTNode {
|
|
if (_scanner.getToken() !== Json.SyntaxKind.StringLiteral) {
|
|
return null;
|
|
}
|
|
|
|
let node = new StringASTNode(parent, name, isKey, _scanner.getTokenOffset());
|
|
node.value = _scanner.getTokenValue();
|
|
|
|
_checkScanError();
|
|
|
|
return _finalize(node, true);
|
|
}
|
|
|
|
function _parseNumber(parent: ASTNode, name: string): NumberASTNode {
|
|
if (_scanner.getToken() !== Json.SyntaxKind.NumericLiteral) {
|
|
return null;
|
|
}
|
|
|
|
let node = new NumberASTNode(parent, name, _scanner.getTokenOffset());
|
|
if (!_checkScanError()) {
|
|
let tokenValue = _scanner.getTokenValue();
|
|
try {
|
|
let numberValue = JSON.parse(tokenValue);
|
|
if (typeof numberValue !== 'number') {
|
|
return _error(nls.localize('InvalidNumberFormat', 'Invalid number format'), node);
|
|
}
|
|
node.value = numberValue;
|
|
} catch (e) {
|
|
return _error(nls.localize('InvalidNumberFormat', 'Invalid number format'), node);
|
|
}
|
|
node.isInteger = tokenValue.indexOf('.') === -1;
|
|
}
|
|
return _finalize(node, true);
|
|
}
|
|
|
|
function _parseLiteral(parent: ASTNode, name: string): ASTNode {
|
|
let node: ASTNode;
|
|
switch (_scanner.getToken()) {
|
|
case Json.SyntaxKind.NullKeyword:
|
|
node = new NullASTNode(parent, name, _scanner.getTokenOffset());
|
|
break;
|
|
case Json.SyntaxKind.TrueKeyword:
|
|
node = new BooleanASTNode(parent, name, true, _scanner.getTokenOffset());
|
|
break;
|
|
case Json.SyntaxKind.FalseKeyword:
|
|
node = new BooleanASTNode(parent, name, false, _scanner.getTokenOffset());
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
return _finalize(node, true);
|
|
}
|
|
|
|
function _parseValue(parent: ASTNode, name: string): ASTNode {
|
|
return _parseArray(parent, name) || _parseObject(parent, name) || _parseString(parent, name, false) || _parseNumber(parent, name) || _parseLiteral(parent, name);
|
|
}
|
|
|
|
_scanner.scan();
|
|
|
|
_doc.root = _parseValue(null, null);
|
|
if (!_doc.root) {
|
|
_error(nls.localize('Invalid symbol', 'Expected a JSON object, array or literal'));
|
|
} else if (_scanner.getToken() !== Json.SyntaxKind.EOF) {
|
|
_error(nls.localize('End of file expected', 'End of file expected'));
|
|
}
|
|
return _doc;
|
|
} |