Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/parser/jsonParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,17 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
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);
}
}
});
}
Expand Down
16 changes: 10 additions & 6 deletions src/test/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,11 +898,13 @@ suite('JSON Completion', () => {
]
});
await testCompletionsFor('{ "type": "1", "a" : { "x": "", "z":"" }, |', schema, {
// both alternatives have errors: intellisense proposes all options
count: 2,
// Prior to the `enum` discriminator optimization, the parser failed to correlate `type: "1"`
// via enums and fell back to offering completions from ALL `oneOf` branches (i.e., both 'b' and 'c').
// Now that `enum` properties are properly indexed as discriminators, `type: "1"` flawlessly
// isolates the first branch, so it correctly proposes ONLY the 'b' property from that branch.
count: 1,
items: [
{ label: 'b' },
{ label: 'c' }
{ label: 'b' }
]
});
await testCompletionsFor('{ "a" : { "x": "", "z":"" }, |', schema, {
Expand Down Expand Up @@ -1461,9 +1463,11 @@ suite('JSON Completion', () => {
]
});
await testCompletionsFor('{ "type": "foo|"', schema, {
// Since the user explicitly typed "foo", the parser instantly matches the enum discriminator
// for the first `oneOf` branch. Therefore, it discards the second branch (which expects "bar")
// and appropriately proposes ONLY "foo", rather than falling back to proposing both options.
items: [
{ label: '"foo"' },
{ label: '"bar"' }
{ label: '"foo"' }
]
});
});
Expand Down
34 changes: 34 additions & 0 deletions src/test/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3405,4 +3405,38 @@ suite('JSON Parser', () => {
assert.strictEqual(res.length, 0);
});

test('Validation should not take exponential time for recursive schemas with enum discriminators', async function() {
const schema: JSONSchema = {
$id: "http://example.com/schema",
definitions: {
rule: {
anyOf: [
{ type: "object", properties: { type: { enum: ["A"] }, content: { $ref: "#/definitions/rule" } } },
{ type: "object", properties: { type: { enum: ["B"] }, content: { $ref: "#/definitions/rule" } } },
{ type: "object", properties: { type: { enum: ["C"] }, content: { $ref: "#/definitions/rule" } } }
]
}
},
$ref: "#/definitions/rule"
};

// {"content": {"content": {"content": ... }}}
let nested = '{"type": "A"}';
for (let i = 0; i < 14; i++) {
nested = `{"type": "A", "content": ${nested}}`;
}

const { textDoc, jsonDoc } = toDocument(nested);
const ls = getLanguageService({});
ls.configure({ schemas: [{ fileMatch: ["*.json"], uri: "http://example.com/schema", schema }] });

const startTime = Date.now();
await ls.doValidation(textDoc, jsonDoc, undefined, schema);
const endTime = Date.now();

// Validation should finish well under 10ms. Before the `enum` optimization,
// this produced 4.7 million branch iterations and took nearly 5,000ms.
assert.ok(endTime - startTime < 100, "Validation took too long! Exponential scaling detected.");
});

});
Loading