vscode-json-languageservice
Version:
Language service for JSON
1,466 lines • 72.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Json from 'jsonc-parser';
import { isNumber, equals, isBoolean, isString, isDefined, isObject } from '../utils/objects.js';
import { extendedRegExp, stringLength } from '../utils/strings.js';
import { ErrorCode, Diagnostic, DiagnosticSeverity, Range, SchemaDraft } from '../jsonLanguageTypes.js';
import { URI } from 'vscode-uri';
import { isKeywordEnabled, isFormatAssertionEnabled } from '../services/vocabularies.js';
import * as l10n from '@vscode/l10n';
const formats = {
'color-hex': { errorMessage: l10n.t('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: l10n.t('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: l10n.t('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: l10n.t('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 },
'duration': { errorMessage: l10n.t('String is not an ISO 8601 duration.'), pattern: /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/ },
'uuid': { errorMessage: l10n.t('String is not a valid UUID.'), pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i },
'email': { errorMessage: l10n.t('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-Z0-9-]+\.)+[a-zA-Z]{2,}))$/ },
'hostname': { errorMessage: l10n.t('String is not a hostname.'), pattern: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i },
'ipv4': { errorMessage: l10n.t('String is not an IPv4 address.'), pattern: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/ },
'ipv6': { errorMessage: l10n.t('String is not an IPv6 address.'), pattern: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i },
};
export class ASTNodeImpl {
constructor(parent, offset, length = 0) {
this.offset = offset;
this.length = length;
this.parent = parent;
}
get children() {
return [];
}
toString() {
return 'type: ' + this.type + ' (' + this.offset + '/' + this.length + ')' + (this.parent ? ' parent: {' + this.parent.toString() + '}' : '');
}
}
export class NullASTNodeImpl extends ASTNodeImpl {
constructor(parent, offset) {
super(parent, offset);
this.type = 'null';
this.value = null;
}
}
export class BooleanASTNodeImpl extends ASTNodeImpl {
constructor(parent, boolValue, offset) {
super(parent, offset);
this.type = 'boolean';
this.value = boolValue;
}
}
export class ArrayASTNodeImpl extends ASTNodeImpl {
constructor(parent, offset) {
super(parent, offset);
this.type = 'array';
this.items = [];
}
get children() {
return this.items;
}
}
export class NumberASTNodeImpl extends ASTNodeImpl {
constructor(parent, offset) {
super(parent, offset);
this.type = 'number';
this.isInteger = true;
this.value = Number.NaN;
}
}
export class StringASTNodeImpl extends ASTNodeImpl {
constructor(parent, offset, length) {
super(parent, offset, length);
this.type = 'string';
this.value = '';
}
}
export class PropertyASTNodeImpl extends ASTNodeImpl {
constructor(parent, offset, keyNode) {
super(parent, offset);
this.type = 'property';
this.colonOffset = -1;
this.keyNode = keyNode;
}
get children() {
return this.valueNode ? [this.keyNode, this.valueNode] : [this.keyNode];
}
}
export class ObjectASTNodeImpl extends ASTNodeImpl {
constructor(parent, offset) {
super(parent, offset);
this.type = 'object';
this.properties = [];
}
get children() {
return this.properties;
}
}
export function asSchema(schema) {
if (isBoolean(schema)) {
return schema ? {} : { "not": {} };
}
return schema;
}
export var EnumMatch;
(function (EnumMatch) {
EnumMatch[EnumMatch["Key"] = 0] = "Key";
EnumMatch[EnumMatch["Enum"] = 1] = "Enum";
})(EnumMatch || (EnumMatch = {}));
const httpPrefix = `http://json-schema.org/`;
const httpsPrefix = `https://json-schema.org/`;
export function normalizeId(id) {
// use the https prefix for the old json-schema.org meta schemas
// See https://github.com/microsoft/vscode/issues/195189
if (id.startsWith(httpPrefix)) {
id = httpsPrefix + id.substring(httpPrefix.length);
}
// remove trailing '#', normalize drive capitalization
try {
return URI.parse(id).toString(true);
}
catch (e) {
return id;
}
}
export function getSchemaDraftFromId(schemaId) {
return schemaDraftFromId[normalizeId(schemaId)] ?? undefined;
}
const schemaDraftFromId = {
'https://json-schema.org/draft-03/schema': SchemaDraft.v3,
'https://json-schema.org/draft-04/schema': SchemaDraft.v4,
'https://json-schema.org/draft-06/schema': SchemaDraft.v6,
'https://json-schema.org/draft-07/schema': SchemaDraft.v7,
'https://json-schema.org/draft/2019-09/schema': SchemaDraft.v2019_09,
'https://json-schema.org/draft/2020-12/schema': SchemaDraft.v2020_12
};
class EvaluationContext {
constructor(schemaDraft, activeVocabularies, explicitSchemaDraft) {
this.schemaDraft = schemaDraft;
this.activeVocabularies = activeVocabularies;
this.explicitSchemaDraft = explicitSchemaDraft;
}
}
class SchemaCollector {
constructor(focusOffset = -1, exclude) {
this.focusOffset = focusOffset;
this.exclude = exclude;
this.schemas = [];
}
add(schema) {
this.schemas.push(schema);
}
merge(other) {
Array.prototype.push.apply(this.schemas, 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() { }
get schemas() { return []; }
add(_schema) { }
merge(_other) { }
include(_node) { return true; }
newSub() { return this; }
}
NoOpSchemaCollector.instance = new NoOpSchemaCollector();
export class ValidationResult {
constructor() {
this.problems = [];
this.propertiesMatches = 0;
this.processedProperties = new Set();
this.propertiesValueMatches = 0;
this.primaryValueMatches = 0;
this.enumValueMatch = false;
this.enumValues = undefined;
}
hasProblems() {
return !!this.problems.length;
}
merge(validationResult) {
this.problems = this.problems.concat(validationResult.problems);
this.propertiesMatches += validationResult.propertiesMatches;
this.propertiesValueMatches += validationResult.propertiesValueMatches;
this.mergeProcessedProperties(validationResult);
}
mergeEnumValues(validationResult) {
if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) {
this.enumValues = this.enumValues.concat(validationResult.enumValues);
}
}
updateEnumMismatchProblemMessages() {
if (!this.enumValueMatch && this.enumValues) {
for (const error of this.problems) {
if (error.code === ErrorCode.EnumValueMismatch) {
error.message = l10n.t('Value is not accepted. Valid values: {0}.', this.enumValues.map(v => JSON.stringify(v)).join(', '));
}
}
}
}
mergePropertyMatch(propertyValidationResult) {
this.problems = this.problems.concat(propertyValidationResult.problems);
this.propertiesMatches++;
if (propertyValidationResult.enumValueMatch || !propertyValidationResult.hasProblems() && propertyValidationResult.propertiesMatches) {
this.propertiesValueMatches++;
}
if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues && propertyValidationResult.enumValues.length === 1) {
this.primaryValueMatches++;
}
}
mergeProcessedProperties(validationResult) {
validationResult.processedProperties.forEach(p => this.processedProperties.add(p));
}
compare(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.primaryValueMatches !== other.primaryValueMatches) {
return this.primaryValueMatches - other.primaryValueMatches;
}
if (this.propertiesValueMatches !== other.propertiesValueMatches) {
return this.propertiesValueMatches - other.propertiesValueMatches;
}
return this.propertiesMatches - other.propertiesMatches;
}
}
export function newJSONDocument(root, diagnostics = [], comments = []) {
return new JSONDocument(root, diagnostics, comments);
}
export function getNodeValue(node) {
return Json.getNodeValue(node);
}
export function getNodePath(node) {
return Json.getNodePath(node);
}
export function contains(node, offset, includeRightBound = false) {
return offset >= node.offset && offset < (node.offset + node.length) || includeRightBound && offset === (node.offset + node.length);
}
export class JSONDocument {
constructor(root, syntaxErrors = [], comments = []) {
this.root = root;
this.syntaxErrors = syntaxErrors;
this.comments = comments;
}
getNodeFromOffset(offset, includeRightBound = false) {
if (this.root) {
return Json.findNodeAtOffset(this.root, offset, includeRightBound);
}
return undefined;
}
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, severity = DiagnosticSeverity.Warning, schemaDraft, activeVocabularies) {
if (this.root && schema) {
const validationResult = new ValidationResult();
const { explicitDraft, effectiveDraft } = resolveDrafts(schema, schemaDraft);
const context = new EvaluationContext(effectiveDraft, activeVocabularies, explicitDraft);
const schemaStack = [];
const schemaRoots = [schema];
validate(this.root, schema, validationResult, NoOpSchemaCollector.instance, context, schemaStack, schemaRoots);
return validationResult.problems.map(p => {
const range = Range.create(textDocument.positionAt(p.location.offset), textDocument.positionAt(p.location.offset + p.location.length));
return Diagnostic.create(range, p.message, p.severity ?? severity, p.code);
});
}
return undefined;
}
getMatchingSchemas(schema, focusOffset = -1, exclude, activeVocabularies) {
if (this.root && schema) {
const matchingSchemas = new SchemaCollector(focusOffset, exclude);
const { explicitDraft, effectiveDraft } = resolveDrafts(schema);
const context = new EvaluationContext(effectiveDraft, activeVocabularies, explicitDraft);
const schemaStack = [];
const schemaRoots = [schema];
validate(this.root, schema, new ValidationResult(), matchingSchemas, context, schemaStack, schemaRoots);
return matchingSchemas.schemas;
}
return [];
}
}
/*
* Resolves the schema draft versions for validation.
* - explicitDraft: the draft explicitly declared via $schema or passed as an override.
* Undefined when no $schema is present and no override is provided.
* - effectiveDraft: the draft used for keyword gating, defaulting to 2020-12.
* These are tracked separately so that behaviors like format assertion can
* distinguish "explicitly 2020-12" (annotation-only per spec) from
* "no $schema, defaulting to 2020-12" (asserts for backward compatibility).
*/
function resolveDrafts(schema, schemaDraftOverride) {
const explicitDraft = schemaDraftOverride ?? (schema.$schema ? getSchemaDraftFromId(schema.$schema) : undefined);
const effectiveDraft = explicitDraft ?? SchemaDraft.v2020_12;
return { explicitDraft, effectiveDraft };
}
// Keywords introduced in 2019-09 (not available in draft-07 and earlier)
const keywords201909 = new Set([
'dependentRequired', 'dependentSchemas',
'unevaluatedProperties', 'unevaluatedItems',
'minContains', 'maxContains'
]);
function validate(n, schema, validationResult, matchingSchemas, context, schemaStack, schemaRoots) {
if (!n || !matchingSchemas.include(n)) {
return;
}
if (n.type === 'property') {
return validate(n.valueNode, schema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
}
const node = n;
if (schema.$recursiveRef) {
const hasRecursiveAnchor = (s) => s.$recursiveAnchor === true;
const isSchemaRoot = (s) => s.$id || s.id || s.$originalId;
// Find nearest schema resource root
const currentResourceRoot = schemaStack.slice().reverse().find(isSchemaRoot);
// If current resource has $recursiveAnchor, find first $recursiveAnchor in stack
// Otherwise use current resource root or fall back to document root
const targetSchema = (currentResourceRoot && hasRecursiveAnchor(currentResourceRoot))
? schemaStack.find(hasRecursiveAnchor)
: currentResourceRoot ?? schemaRoots[0];
// Validate against the target schema (avoiding infinite loop).
// Fall through afterwards so sibling keywords (e.g. unevaluatedProperties)
// are still evaluated against this node, accumulating processedProperties.
if (targetSchema && targetSchema !== schema) {
validate(node, targetSchema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
}
}
// Track schema document roots (schemas with $id or $originalId) if not already the root
const isNewRoot = (schema.$id || schema.id || schema.$originalId) && !schemaRoots.includes(schema);
if (isNewRoot) {
schemaRoots.push(schema);
}
// Push current schema to stack for $recursiveRef resolution
schemaStack.push(schema);
// 2020-12 $dynamicRef. The initial (static) target and the referenced plain-name
// fragment were recorded during schema resolution in $dynamicRefInfo (target and
// name). "Bookending": dynamic resolution applies only when that
// initial target is itself a $dynamicAnchor of the referenced name; then the
// reference resolves to the outermost matching $dynamicAnchor currently in the
// dynamic scope (schemaRoots, ordered outermost-first). Otherwise it behaves
// like a plain $ref to the initial target. This runs after the schemaRoots push
// so the resource that contains the $dynamicRef participates in its own
// dynamic-scope resolution.
const dynamicRefInfo = schema.$dynamicRefInfo;
const dynamicRefTarget = dynamicRefInfo?.target;
if (dynamicRefTarget !== undefined) {
let targetSchema = dynamicRefTarget;
const dynamicName = dynamicRefInfo?.name;
if (dynamicName !== undefined && dynamicRefTarget.$dynamicAnchor === dynamicName) {
for (const root of schemaRoots) {
const candidate = root.$anchorMaps?.dynamic.get(dynamicName);
if (candidate) {
targetSchema = candidate;
break;
}
}
}
// Validate the instance against the referenced schema, then fall through so
// that any sibling keywords on this same node are validated too: per 2020-12
// $dynamicRef is an applicator that applies alongside its siblings (like $ref),
// not a redirect that replaces them. The shared cleanup below pops the stack.
if (targetSchema && targetSchema !== schema) {
validate(node, targetSchema, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
}
}
const enabled = (keyword) => {
// Draft-based keyword gating: keywords only exist in certain drafts.
// 'dependencies' was replaced by dependentRequired/dependentSchemas in 2019-09
if (keyword === 'dependencies') {
return context.schemaDraft <= SchemaDraft.v7;
}
// Keywords introduced in 2019-09 (not available in draft-07 and earlier)
if (keywords201909.has(keyword)) {
return context.schemaDraft >= SchemaDraft.v2019_09;
}
// 'prefixItems' was introduced in 2020-12
if (keyword === 'prefixItems') {
return context.schemaDraft >= SchemaDraft.v2020_12;
}
// Vocabulary-based filtering only applies for 2019-09+ (vocabulary is not a concept in older drafts)
if (context.schemaDraft >= SchemaDraft.v2019_09 && context.activeVocabularies) {
return isKeywordEnabled(keyword, context.activeVocabularies);
}
// No vocabulary info or pre-2019-09: enable all draft-appropriate keywords
return true;
};
_validateNode();
switch (node.type) {
case 'object':
_validateObjectNode(node);
break;
case 'array':
_validateArrayNode(node);
break;
case 'string':
_validateStringNode(node);
break;
case 'number':
_validateNumberNode(node);
break;
}
matchingSchemas.add({ node: node, schema: schema });
// Pop schema from stack when done
schemaStack.pop();
// Pop schema root if we pushed one
if (isNewRoot) {
schemaRoots.pop();
}
function _validateNode() {
function matchesType(type) {
return node.type === type || (type === 'integer' && node.type === 'number' && node.isInteger);
}
if (Array.isArray(schema.type) && enabled('type')) {
if (!schema.type.some(matchesType)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.errorMessage || l10n.t('Incorrect type. Expected one of {0}.', schema.type.join(', '))
});
}
}
else if (schema.type && !Array.isArray(schema.type) && enabled('type')) {
if (!matchesType(schema.type)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.errorMessage || l10n.t('Incorrect type. Expected "{0}".', schema.type)
});
}
}
if (Array.isArray(schema.allOf) && enabled('allOf')) {
for (const subSchemaRef of schema.allOf) {
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, asSchema(subSchemaRef), subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
validationResult.merge(subValidationResult);
matchingSchemas.merge(subMatchingSchemas);
}
}
const notSchema = asSchema(schema.not);
if (notSchema && enabled('not')) {
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, notSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
if (!subValidationResult.hasProblems()) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.errorMessage || l10n.t("Matches a schema that is not allowed.")
});
}
for (const ms of subMatchingSchemas.schemas) {
ms.inverted = !ms.inverted;
matchingSchemas.add(ms);
}
}
const testAlternatives = (alternatives, maxOneMatch) => {
const matches = [];
const alternativesToTest = _tryDiscriminatorOptimization(alternatives) ?? alternatives;
// remember the best match that is used for error messages
let bestMatch = undefined;
for (const subSchemaRef of alternativesToTest) {
const subSchema = asSchema(subSchemaRef);
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, subSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
if (!subValidationResult.hasProblems()) {
matches.push(subSchema);
}
if (!bestMatch) {
bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas };
}
else {
if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) {
// no errors, both are equally good matches
bestMatch.matchingSchemas.merge(subMatchingSchemas);
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches;
bestMatch.validationResult.mergeProcessedProperties(subValidationResult);
}
else {
const 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.merge(subMatchingSchemas);
bestMatch.validationResult.mergeEnumValues(subValidationResult);
}
}
}
}
if (matches.length > 1 && maxOneMatch) {
validationResult.problems.push({
location: { offset: node.offset, length: 1 },
message: l10n.t("Matches multiple schemas when only one must validate.")
});
}
if (bestMatch) {
bestMatch.validationResult.updateEnumMismatchProblemMessages();
validationResult.merge(bestMatch.validationResult);
matchingSchemas.merge(bestMatch.matchingSchemas);
}
return matches.length;
};
if (Array.isArray(schema.anyOf) && enabled('anyOf')) {
testAlternatives(schema.anyOf, false);
}
if (Array.isArray(schema.oneOf) && enabled('oneOf')) {
testAlternatives(schema.oneOf, true);
}
const testBranch = (schema) => {
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, asSchema(schema), subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
validationResult.merge(subValidationResult);
matchingSchemas.merge(subMatchingSchemas);
};
const testCondition = (ifSchema, thenSchema, elseSchema) => {
const subSchema = asSchema(ifSchema);
const subValidationResult = new ValidationResult();
const subMatchingSchemas = matchingSchemas.newSub();
validate(node, subSchema, subValidationResult, subMatchingSchemas, context, schemaStack, schemaRoots);
matchingSchemas.merge(subMatchingSchemas);
// if passed: merge properties from if;
// otherwise don't merge properties from if, only from else
if (!subValidationResult.hasProblems()) {
validationResult.mergeProcessedProperties(subValidationResult);
if (thenSchema) {
testBranch(thenSchema);
}
}
else if (elseSchema) {
testBranch(elseSchema);
}
};
const ifSchema = asSchema(schema.if);
if (ifSchema && enabled('if')) {
testCondition(ifSchema, asSchema(schema.then), asSchema(schema.else));
}
if (Array.isArray(schema.enum) && enabled('enum')) {
const val = getNodeValue(node);
let enumValueMatch = false;
for (const e of schema.enum) {
if (equals(val, e)) {
enumValueMatch = true;
break;
}
}
validationResult.enumValues = schema.enum;
validationResult.enumValueMatch = enumValueMatch;
if (!enumValueMatch) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
code: ErrorCode.EnumValueMismatch,
message: schema.errorMessage || l10n.t('Value is not accepted. Valid values: {0}.', schema.enum.map(v => JSON.stringify(v)).join(', '))
});
}
}
if (isDefined(schema.const) && enabled('const')) {
const val = getNodeValue(node);
if (!equals(val, schema.const)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
code: ErrorCode.EnumValueMismatch,
message: schema.errorMessage || l10n.t('Value must be {0}.', JSON.stringify(schema.const))
});
validationResult.enumValueMatch = false;
}
else {
validationResult.enumValueMatch = true;
}
validationResult.enumValues = [schema.const];
}
let deprecationMessage = schema.deprecationMessage;
if (deprecationMessage || schema.deprecated) {
deprecationMessage = deprecationMessage || l10n.t('Value is deprecated');
let targetNode = node.parent?.type === 'property' ? node.parent : node;
validationResult.problems.push({
location: { offset: targetNode.offset, length: targetNode.length },
severity: DiagnosticSeverity.Warning,
message: deprecationMessage,
code: ErrorCode.Deprecated
});
}
}
function _tryDiscriminatorOptimization(alternatives) {
if (alternatives.length < 2) {
return undefined;
}
const buildConstMap = (getSchemas) => {
const constMap = new Map();
for (let i = 0; i < alternatives.length; i++) {
const schemas = getSchemas(asSchema(alternatives[i]), i);
if (!schemas) {
return undefined; // Early exit if any alternative can't be processed
}
schemas.forEach(([key, schema]) => {
if (schema.const !== undefined) {
if (!constMap.has(key)) {
constMap.set(key, new Map());
}
const valueMap = constMap.get(key);
if (!valueMap.has(schema.const)) {
valueMap.set(schema.const, []);
}
valueMap.get(schema.const).push(i);
}
else if (schema.enum && Array.isArray(schema.enum)) {
for (const enumValue of schema.enum) {
if (!constMap.has(key)) {
constMap.set(key, new Map());
}
const valueMap = constMap.get(key);
if (!valueMap.has(enumValue)) {
valueMap.set(enumValue, []);
}
valueMap.get(enumValue).push(i);
}
}
});
}
return constMap;
};
const findDiscriminator = (constMap, getValue) => {
for (const [key, valueMap] of constMap) {
const coveredAlts = new Set();
valueMap.forEach(indices => indices.forEach(idx => coveredAlts.add(idx)));
if (coveredAlts.size === alternatives.length) {
const discriminatorValue = getValue(key);
const matchingIndices = valueMap.get(discriminatorValue);
if (matchingIndices?.length) {
return matchingIndices.map(idx => alternatives[idx]);
}
break; // Found valid discriminator but no match
}
}
return undefined;
};
if (node.type === 'object' && node.properties?.length) {
const constMap = buildConstMap((schema) => schema.properties ? Object.entries(schema.properties).map(([k, v]) => [k, asSchema(v)]) : undefined);
if (constMap) {
return findDiscriminator(constMap, (propName) => {
const prop = node.properties.find(p => p.keyNode.value === propName);
return prop?.valueNode?.type === 'string' ? prop.valueNode.value : undefined;
});
}
}
else if (node.type === 'array' && node.items?.length) {
const constMap = buildConstMap((schema) => {
const itemSchemas = schema.prefixItems || (Array.isArray(schema.items) ? schema.items : undefined);
return itemSchemas ? itemSchemas.map((item, idx) => [idx, asSchema(item)]) : undefined;
});
if (constMap) {
return findDiscriminator(constMap, (itemIndex) => {
const item = node.items[itemIndex];
return item?.type === 'string' ? item.value : undefined;
});
}
}
return undefined;
}
function _validateNumberNode(node) {
const val = node.value;
function normalizeFloats(float) {
const parts = /^(-?\d+)(?:\.(\d+))?(?:e([-+]\d+))?$/.exec(float.toString());
return parts && {
value: Number(parts[1] + (parts[2] || '')),
multiplier: (parts[2]?.length || 0) - (parseInt(parts[3]) || 0)
};
}
;
if (isNumber(schema.multipleOf) && enabled('multipleOf')) {
let remainder = -1;
if (Number.isInteger(schema.multipleOf)) {
remainder = val % schema.multipleOf;
}
else {
let normMultipleOf = normalizeFloats(schema.multipleOf);
let normValue = normalizeFloats(val);
if (normMultipleOf && normValue) {
const multiplier = 10 ** Math.abs(normValue.multiplier - normMultipleOf.multiplier);
if (normValue.multiplier < normMultipleOf.multiplier) {
normValue.value *= multiplier;
}
else {
normMultipleOf.value *= multiplier;
}
remainder = normValue.value % normMultipleOf.value;
}
}
if (remainder !== 0) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Value is not divisible by {0}.', schema.multipleOf)
});
}
}
function getExclusiveLimit(limit, exclusive) {
if (isNumber(exclusive)) {
return exclusive;
}
if (isBoolean(exclusive) && exclusive) {
return limit;
}
return undefined;
}
function getLimit(limit, exclusive) {
if (!isBoolean(exclusive) || !exclusive) {
return limit;
}
return undefined;
}
const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum);
if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum && enabled('exclusiveMinimum')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Value is below the exclusive minimum of {0}.', exclusiveMinimum)
});
}
const exclusiveMaximum = getExclusiveLimit(schema.maximum, schema.exclusiveMaximum);
if (isNumber(exclusiveMaximum) && val >= exclusiveMaximum && enabled('exclusiveMaximum')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Value is above the exclusive maximum of {0}.', exclusiveMaximum)
});
}
const minimum = getLimit(schema.minimum, schema.exclusiveMinimum);
if (isNumber(minimum) && val < minimum && enabled('minimum')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Value is below the minimum of {0}.', minimum)
});
}
const maximum = getLimit(schema.maximum, schema.exclusiveMaximum);
if (isNumber(maximum) && val > maximum && enabled('maximum')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Value is above the maximum of {0}.', maximum)
});
}
}
function _validateStringNode(node) {
if (isNumber(schema.minLength) && stringLength(node.value) < schema.minLength && enabled('minLength')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('String is shorter than the minimum length of {0}.', schema.minLength)
});
}
if (isNumber(schema.maxLength) && stringLength(node.value) > schema.maxLength && enabled('maxLength')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('String is longer than the maximum length of {0}.', schema.maxLength)
});
}
if (isString(schema.pattern) && enabled('pattern')) {
const regex = extendedRegExp(schema.pattern);
if (regex && !(regex.test(node.value))) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.patternErrorMessage || schema.errorMessage || l10n.t('String does not match the pattern of "{0}".', schema.pattern)
});
}
}
// Only validate format if format-assertion vocabulary is active (not annotation-only)
if (schema.format && enabled('format') && isFormatAssertionEnabled(context.activeVocabularies, context.explicitSchemaDraft)) {
switch (schema.format) {
case 'uri':
case 'uri-reference':
{
let errorMessage;
if (!node.value) {
errorMessage = l10n.t('URI expected.');
}
else {
const match = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/.exec(node.value);
if (!match) {
errorMessage = l10n.t('URI is expected.');
}
else if (!match[2] && schema.format === 'uri') {
errorMessage = l10n.t('URI with a scheme is expected.');
}
}
if (errorMessage) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.patternErrorMessage || schema.errorMessage || l10n.t('String is not a URI: {0}', errorMessage)
});
}
}
break;
case 'color-hex':
case 'date-time':
case 'date':
case 'time':
case 'duration':
case 'uuid':
case 'email':
case 'hostname':
case 'ipv4':
case 'ipv6':
const format = formats[schema.format];
if (!node.value || !format.pattern.exec(node.value)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.patternErrorMessage || schema.errorMessage || format.errorMessage
});
}
default:
}
}
}
function _validateArrayNode(node) {
let prefixItemsSchemas;
let additionalItemSchema;
if (context.schemaDraft >= SchemaDraft.v2020_12) {
prefixItemsSchemas = schema.prefixItems;
additionalItemSchema = !Array.isArray(schema.items) ? schema.items : undefined;
}
else {
prefixItemsSchemas = Array.isArray(schema.items) ? schema.items : undefined;
additionalItemSchema = !Array.isArray(schema.items) ? schema.items : schema.additionalItems;
}
let index = 0;
if (prefixItemsSchemas !== undefined) {
const max = Math.min(prefixItemsSchemas.length, node.items.length);
for (; index < max; index++) {
const subSchemaRef = prefixItemsSchemas[index];
const subSchema = asSchema(subSchemaRef);
const itemValidationResult = new ValidationResult();
const item = node.items[index];
if (item) {
validate(item, subSchema, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(itemValidationResult);
}
validationResult.processedProperties.add(String(index));
}
}
if (additionalItemSchema !== undefined && index < node.items.length) {
if (typeof additionalItemSchema === 'boolean') {
if (additionalItemSchema === false) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Array has too many items according to schema. Expected {0} or fewer.', index)
});
}
for (; index < node.items.length; index++) {
validationResult.processedProperties.add(String(index));
validationResult.propertiesValueMatches++;
}
}
else {
for (; index < node.items.length; index++) {
const itemValidationResult = new ValidationResult();
validate(node.items[index], additionalItemSchema, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(itemValidationResult);
validationResult.processedProperties.add(String(index));
}
}
}
const containsSchema = asSchema(schema.contains);
if (containsSchema && enabled('contains')) {
let containsCount = 0;
for (let index = 0; index < node.items.length; index++) {
const item = node.items[index];
const itemValidationResult = new ValidationResult();
validate(item, containsSchema, itemValidationResult, NoOpSchemaCollector.instance, context, schemaStack, schemaRoots);
if (!itemValidationResult.hasProblems()) {
containsCount++;
if (context.schemaDraft >= SchemaDraft.v2020_12) {
validationResult.processedProperties.add(String(index));
}
}
}
if (containsCount === 0 && !isNumber(schema.minContains)) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.errorMessage || l10n.t('Array does not contain required item.')
});
}
if (isNumber(schema.minContains) && containsCount < schema.minContains && enabled('minContains')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.errorMessage || l10n.t('Array has too few items that match the contains contraint. Expected {0} or more.', schema.minContains)
});
}
if (isNumber(schema.maxContains) && containsCount > schema.maxContains && enabled('maxContains')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: schema.errorMessage || l10n.t('Array has too many items that match the contains contraint. Expected {0} or less.', schema.maxContains)
});
}
}
const unevaluatedItems = schema.unevaluatedItems;
if (unevaluatedItems !== undefined && enabled('unevaluatedItems')) {
for (let i = 0; i < node.items.length; i++) {
if (!validationResult.processedProperties.has(String(i))) {
if (unevaluatedItems === false) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Item does not match any validation rule from the array.')
});
}
else {
const itemValidationResult = new ValidationResult();
validate(node.items[i], schema.unevaluatedItems, itemValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(itemValidationResult);
}
}
validationResult.processedProperties.add(String(i));
validationResult.propertiesValueMatches++;
}
}
if (isNumber(schema.minItems) && node.items.length < schema.minItems && enabled('minItems')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Array has too few items. Expected {0} or more.', schema.minItems)
});
}
if (isNumber(schema.maxItems) && node.items.length > schema.maxItems && enabled('maxItems')) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Array has too many items. Expected {0} or fewer.', schema.maxItems)
});
}
if (schema.uniqueItems === true && enabled('uniqueItems')) {
const values = getNodeValue(node);
function hasDuplicates() {
for (let i = 0; i < values.length - 1; i++) {
const value = values[i];
for (let j = i + 1; j < values.length; j++) {
if (equals(value, values[j])) {
return true;
}
}
}
return false;
}
if (hasDuplicates()) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Array has duplicate items.')
});
}
}
}
function _validateObjectNode(node) {
const seenKeys = Object.create(null);
const unprocessedProperties = new Set();
for (const propertyNode of node.properties) {
const key = propertyNode.keyNode.value;
seenKeys[key] = propertyNode.valueNode;
unprocessedProperties.add(key);
}
if (Array.isArray(schema.required) && enabled('required')) {
for (const propertyName of schema.required) {
if (!seenKeys[propertyName]) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Missing property "{0}".', propertyName)
});
}
}
}
const propertyProcessed = (prop) => {
unprocessedProperties.delete(prop);
validationResult.processedProperties.add(prop);
};
if (schema.properties && enabled('properties')) {
for (const propertyName of Object.keys(schema.properties)) {
propertyProcessed(propertyName);
const propertySchema = schema.properties[propertyName];
const child = seenKeys[propertyName];
if (child) {
if (isBoolean(propertySchema)) {
if (!propertySchema) {
const propertyNode = child.parent;
validationResult.problems.push({
location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
});
}
else {
validationResult.propertiesMatches++;
validationResult.propertiesValueMatches++;
}
}
else {
const propertyValidationResult = new ValidationResult();
validate(child, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
if (schema.patternProperties && enabled('patternProperties')) {
// Collect all properties matched by any pattern first
// A property must be validated against all patterns it matches
// Properties in `properties` can also match `patternProperties`, check all keys
const allPropertyNames = Object.keys(seenKeys);
const patternPropertyMatches = new Set();
for (const propertyPattern of Object.keys(schema.patternProperties)) {
const regex = extendedRegExp(propertyPattern);
if (regex) {
for (const propertyName of allPropertyNames) {
if (regex.test(propertyName)) {
// Only mark as pattern match if not already in `properties`
// for additionalProperties tracking
if (unprocessedProperties.has(propertyName)) {
patternPropertyMatches.add(propertyName);
}
const child = seenKeys[propertyName];
if (child) {
const propertySchema = schema.patternProperties[propertyPattern];
if (isBoolean(propertySchema)) {
if (!propertySchema) {
const propertyNode = child.parent;
validationResult.problems.push({
location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
});
}
else {
validationResult.propertiesMatches++;
validationResult.propertiesValueMatches++;
}
}
else {
const propertyValidationResult = new ValidationResult();
validate(child, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
}
}
// Mark all matched properties as processed after all patterns have been checked
patternPropertyMatches.forEach(propertyProcessed);
}
const additionalProperties = schema.additionalProperties;
if (additionalProperties !== undefined && enabled('additionalProperties')) {
for (const propertyName of unprocessedProperties) {
propertyProcessed(propertyName);
const child = seenKeys[propertyName];
if (child) {
if (additionalProperties === false) {
const propertyNode = child.parent;
validationResult.problems.push({
location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
});
}
else if (additionalProperties !== true) {
const propertyValidationResult = new ValidationResult();
validate(child, additionalProperties, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
if (schema.dependentRequired && enabled('dependentRequired')) {
for (const key in schema.dependentRequired) {
const prop = seenKeys[key];
const propertyDeps = schema.dependentRequired[key];
if (prop && Array.isArray(propertyDeps)) {
_validatePropertyDependencies(key, propertyDeps);
}
}
}
if (schema.dependentSchemas && enabled('dependentSchemas')) {
for (const key in schema.dependentSchemas) {
const prop = seenKeys[key];
const propertyDeps = schema.dependentSchemas[key];
if (prop) {
_validatePropertyDependencies(key, propertyDeps);
}
}
}
const unevaluatedProperties = schema.unevaluatedProperties;
if (unevaluatedProperties !== undefined && enabled('unevaluatedProperties')) {
const processed = [];
for (const propertyName of unprocessedProperties) {
if (!validationResult.processedProperties.has(propertyName)) {
processed.push(propertyName);
const child = seenKeys[propertyName];
if (child) {
if (unevaluatedProperties === false) {
const propertyNode = child.parent;
validationResult.problems.push({
location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length },
message: schema.errorMessage || l10n.t('Property {0} is not allowed.', propertyName)
});
}
else if (unevaluatedProperties !== true) {
const propertyValidationResult = new ValidationResult();
validate(child, unevaluatedProperties, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(propertyValidationResult);
}
}
}
}
processed.forEach(propertyProcessed);
}
if (isNumber(schema.maxProperties) && enabled('maxProperties')) {
if (node.properties.length > schema.maxProperties) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Object has more properties than limit of {0}.', schema.maxProperties)
});
}
}
if (isNumber(schema.minProperties) && enabled('minProperties')) {
if (node.properties.length < schema.minProperties) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Object has fewer properties than the required number of {0}', schema.minProperties)
});
}
}
if (schema.dependencies && enabled('dependencies')) {
for (const key in schema.dependencies) {
const prop = seenKeys[key];
if (prop) {
_validatePropertyDependencies(key, schema.dependencies[key]);
}
}
}
const propertyNames = asSchema(schema.propertyNames);
if (propertyNames && enabled('propertyNames')) {
for (const f of node.properties) {
const key = f.keyNode;
if (key) {
validate(key, propertyNames, validationResult, matchingSchemas, context, schemaStack, schemaRoots);
}
}
}
function _validatePropertyDependencies(key, propertyDep) {
if (Array.isArray(propertyDep)) {
for (const requiredProp of propertyDep) {
if (!seenKeys[requiredProp]) {
validationResult.problems.push({
location: { offset: node.offset, length: node.length },
message: l10n.t('Object is missing property {0} required by property {1}.', requiredProp, key)
});
}
else {
validationResult.propertiesValueMatches++;
}
}
}
else {
const propertySchema = asSchema(propertyDep);
if (propertySchema) {
const propertyValidationResult = new ValidationResult();
validate(node, propertySchema, propertyValidationResult, matchingSchemas, context, schemaStack, schemaRoots);
validationResult.mergePropertyMatch(propertyValidationResult);
validationResult.mergeProcessedProperties(propertyValidationResult);
}
}
}
}
}
export function parse(textDocument, config) {
const problems = [];
let lastProblemOffset = -1;
const text = textDocument.getText();
const scanner = Json.createScanner(text, false);
const commentRanges = config && config.collectComments ? [] : undefined;
function _scanNext() {
while (true) {
const token = scanner.scan();
_checkScanError();
switch (token) {
case 12 /* Json.SyntaxKind.LineCommentTrivia */:
case 13 /* Json.SyntaxKind.BlockCommentTrivia */:
if (Array.isArray(commentRanges)) {
commentRanges.push(Range.create(textDocument.positionAt(scanner.getTokenOffset()), textDocument.positionAt(scanner.getTokenOffset() + scanner.getTokenLength())));
}
break;
case 15 /* Json.SyntaxKind.Trivia */:
case 14 /* Json.SyntaxKind.LineBreakTrivia */:
break;
default:
return token;
}
}
}
function _accept(token) {
if (scanner.getToken() === token) {
_scanNext();
return true;
}
return false;
}
function _errorAtRange(message, code, startOffset, endOffset, severity = DiagnosticSeverity.Error) {
if (problems.length === 0 || startOffset !== lastProblemOffset) {
const range = Range.create(textDocument.positionAt(startOffset), textDocument.positionAt(endOffset));
problems.push(Diagnostic.create(range, message, severity, code, textDocument.languageId));
lastProblemOffset = startOffset;
}
}
function _error(message, code, node = undefined, skipUntilAfter = [], skipUntil = []) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenOffset() + scanner.getTokenLength();
if (start === end && start > 0) {
start--;
while (start > 0 && /\s/.test(text.charAt(start))) {
start--;
}
end = start + 1;
}
_errorAtRange(message, code, start, end);
if (node) {
_finalize(node, false);
}
if (skipUntilAfter.length + skipUntil.length > 0) {
let token = scanner.getToken();
while (token !== 17 /* Json.SyntaxKind.EOF */) {
if (skipUntilAfter.indexOf(token) !== -1) {
_scanNext();
break;
}
else if (skipUntil.indexOf(token) !== -1) {
break;
}
token = _scanNext();
}
}
return node;
}
function _checkScanError() {
switch (scanner.getTokenError()) {
case 4 /* Json.ScanError.InvalidUnicode */:
_error(l10n.t('Invalid unicode sequence in string.'), ErrorCode.InvalidUnicode);
return true;
case 5 /* Json.ScanError.InvalidEscapeCharacter */:
_error(l10n.t('Invalid escape character in string.'), ErrorCode.InvalidEscapeCharacter);
return true;
case 3 /* Json.ScanError.UnexpectedEndOfNumber */:
_error(l10n.t('Unexpected end of number.'), ErrorCode.UnexpectedEndOfNumber);
return true;
case 1 /* Json.ScanError.UnexpectedEndOfComment */:
_error(l10n.t('Unexpected end of comment.'), ErrorCode.UnexpectedEndOfComment);
return true;
case 2 /* Json.ScanError.UnexpectedEndOfString */:
_error(l10n.t('Unexpected end of string.'), ErrorCode.UnexpectedEndOfString);
return true;
case 6 /* Json.ScanError.InvalidCharacter */:
_error(l10n.t('Invalid characters in string. Control characters must be escaped.'), ErrorCode.InvalidCharacter);
return true;
}
return false;
}
function _finalize(node, scanNext) {
node.length = scanner.getTokenOffset() + scanner.getTokenLength() - node.offset;
if (scanNext) {
_scanNext();
}
return node;
}
function _parseArray(parent) {
if (scanner.getToken() !== 3 /* Json.SyntaxKind.OpenBracketToken */) {
return undefined;
}
const node = new ArrayASTNodeImpl(parent, scanner.getTokenOffset());
_scanNext(); // consume OpenBracketToken
const count = 0;
let needsComma = false;
while (scanner.getToken() !== 4 /* Json.SyntaxKind.CloseBracketToken */ && scanner.getToken() !== 17 /* Json.SyntaxKind.EOF */) {
if (scanner.getToken() === 5 /* Json.SyntaxKind.CommaToken */) {
if (!needsComma) {
_error(l10n.t('Value expected'), ErrorCode.ValueExpected);
}
const commaOffset = scanner.getTokenOffset();
_scanNext(); // consume comma
if (scanner.getToken() === 4 /* Json.SyntaxKind.CloseBracketToken */) {
if (needsComma) {
_errorAtRange(l10n.t('Trailing comma'), ErrorCode.TrailingComma, commaOffset, commaOffset + 1);
}
continue;
}
}
else if (needsComma) {
_error(l10n.t('Expected comma'), ErrorCode.CommaExpected);
}
const item = _parseValue(node);
if (!item) {
_error(l10n.t('Value expected'), ErrorCode.ValueExpected, undefined, [], [4 /* Json.SyntaxKind.CloseBracketToken */, 5 /* Json.SyntaxKind.CommaToken */]);
}
else {
node.items.push(item);
}
needsComma = true;
}
if (scanner.getToken() !== 4 /* Json.SyntaxKind.CloseBracketToken */) {
return _error(l10n.t('Expected comma or closing bracket'), ErrorCode.CommaOrCloseBacketExpected, node);
}
return _finalize(node, true);
}
const keyPlaceholder = new StringASTNodeImpl(undefined, 0, 0);
function _parseProperty(parent, keysSeen) {
const node = new PropertyASTNodeImpl(parent, scanner.getTokenOffset(), keyPlaceholder);
let key = _parseString(node);
if (!key) {
if (scanner.getToken() === 16 /* Json.SyntaxKind.Unknown */) {
// give a more helpful error message
_error(l10n.t('Property keys must be doublequoted'), ErrorCode.PropertyKeysMustBeDoublequoted);
const keyNode = new StringASTNodeImpl(node, scanner.getTokenOffset(), scanner.getTokenLength());
keyNode.value = scanner.getTokenValue();
key = keyNode;
_scanNext(); // consume Unknown
}
else {
return undefined;
}
}
node.keyNode = key;
// For JSON files that forbid code comments, there is a convention to use the key name "//" to add comments.
// Multiple instances of "//" are okay.
if (key.value !== "//") {
const seen = keysSeen[key.value];
if (seen) {
_errorAtRange(l10n.t("Duplicate object key"), ErrorCode.DuplicateKey, node.keyNode.offset, node.keyNode.offset + node.keyNode.length, DiagnosticSeverity.Warning);
if (isObject(seen)) {
_errorAtRange(l10n.t("Duplicate object key"), ErrorCode.DuplicateKey, seen.keyNode.offset, seen.keyNode.offset + seen.keyNode.length, DiagnosticSeverity.Warning);
}
keysSeen[key.value] = true; // if the same key is duplicate again, avoid duplicate error reporting
}
else {
keysSeen[key.value] = node;
}
}
if (scanner.getToken() === 6 /* Json.SyntaxKind.ColonToken */) {
node.colonOffset = scanner.getTokenOffset();
_scanNext(); // consume ColonToken
}
else {
_error(l10n.t('Colon expected'), ErrorCode.ColonExpected);
if (scanner.getToken() === 10 /* Json.SyntaxKind.StringLiteral */ && textDocument.positionAt(key.offset + key.length).line < textDocument.positionAt(scanner.getTokenOffset()).line) {
node.length = key.length;
return node;
}
}
const value = _parseValue(node);
if (!value) {
return _error(l10n.t('Value expected'), ErrorCode.ValueExpected, node, [], [2 /* Json.SyntaxKind.CloseBraceToken */, 5 /* Json.SyntaxKind.CommaToken */]);
}
node.valueNode = value;
node.length = value.offset + value.length - node.offset;
return node;
}
function _parseObject(parent) {
if (scanner.getToken() !== 1 /* Json.SyntaxKind.OpenBraceToken */) {
return undefined;
}
const node = new ObjectASTNodeImpl(parent, scanner.getTokenOffset());
const keysSeen = Object.create(null);
_scanNext(); // consume OpenBraceToken
let needsComma = false;
while (scanner.getToken() !== 2 /* Json.SyntaxKind.CloseBraceToken */ && scanner.getToken() !== 17 /* Json.SyntaxKind.EOF */) {
if (scanner.getToken() === 5 /* Json.SyntaxKind.CommaToken */) {
if (!needsComma) {
_error(l10n.t('Property expected'), ErrorCode.PropertyExpected);
}
const commaOffset = scanner.getTokenOffset();
_scanNext(); // consume comma
if (scanner.getToken() === 2 /* Json.SyntaxKind.CloseBraceToken */) {
if (needsComma) {
_errorAtRange(l10n.t('Trailing comma'), ErrorCode.TrailingComma, commaOffset, commaOffset + 1);
}
continue;
}
}
else if (needsComma) {
_error(l10n.t('Expected comma'), ErrorCode.CommaExpected);
}
const property = _parseProperty(node, keysSeen);
if (!property) {
_error(l10n.t('Property expected'), ErrorCode.PropertyExpected, undefined, [], [2 /* Json.SyntaxKind.CloseBraceToken */, 5 /* Json.SyntaxKind.CommaToken */]);
}
else {
node.properties.push(property);
}
needsComma = true;
}
if (scanner.getToken() !== 2 /* Json.SyntaxKind.CloseBraceToken */) {
return _error(l10n.t('Expected comma or closing brace'), ErrorCode.CommaOrCloseBraceExpected, node);
}
return _finalize(node, true);
}
function _parseString(parent) {
if (scanner.getToken() !== 10 /* Json.SyntaxKind.StringLiteral */) {
return undefined;
}
const node = new StringASTNodeImpl(parent, scanner.getTokenOffset());
node.value = scanner.getTokenValue();
return _finalize(node, true);
}
function _parseNumber(parent) {
if (scanner.getToken() !== 11 /* Json.SyntaxKind.NumericLiteral */) {
return undefined;
}
const node = new NumberASTNodeImpl(parent, scanner.getTokenOffset());
if (scanner.getTokenError() === 0 /* Json.ScanError.None */) {
const tokenValue = scanner.getTokenValue();
try {
const numberValue = JSON.parse(tokenValue);
if (!isNumber(numberValue)) {
return _error(l10n.t('Invalid number format.'), ErrorCode.Undefined, node);
}
node.value = numberValue;
}
catch (e) {
return _error(l10n.t('Invalid number format.'), ErrorCode.Undefined, node);
}
node.isInteger = tokenValue.indexOf('.') === -1;
}
return _finalize(node, true);
}
function _parseLiteral(parent) {
let node;
switch (scanner.getToken()) {
case 7 /* Json.SyntaxKind.NullKeyword */:
return _finalize(new NullASTNodeImpl(parent, scanner.getTokenOffset()), true);
case 8 /* Json.SyntaxKind.TrueKeyword */:
return _finalize(new BooleanASTNodeImpl(parent, true, scanner.getTokenOffset()), true);
case 9 /* Json.SyntaxKind.FalseKeyword */:
return _finalize(new BooleanASTNodeImpl(parent, false, scanner.getTokenOffset()), true);
default:
return undefined;
}
}
function _parseValue(parent) {
return _parseArray(parent) || _parseObject(parent) || _parseString(parent) || _parseNumber(parent) || _parseLiteral(parent);
}
let _root = undefined;
const token = _scanNext();
if (token !== 17 /* Json.SyntaxKind.EOF */) {
_root = _parseValue(_root);
if (!_root) {
_error(l10n.t('Expected a JSON object, array or literal.'), ErrorCode.Undefined);
}
else if (scanner.getToken() !== 17 /* Json.SyntaxKind.EOF */) {
_error(l10n.t('End of file expected.'), ErrorCode.Undefined);
}
}
return new JSONDocument(_root, problems, commentRanges);
}