Files
vscode/extensions/json/server/src/jsonParser.ts
2016-02-23 08:53:32 +01:00

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