Skip to content

Commit f5de98b

Browse files
Fix: Support enum discriminators to prevent exponential validation freeze (#284) (#320)
1 parent 26e4a5f commit f5de98b

File tree

3 files changed

+55
-6
lines changed

3 files changed

+55
-6
lines changed

src/parser/jsonParser.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,17 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
646646
valueMap.set(schema.const, []);
647647
}
648648
valueMap.get(schema.const)!.push(i);
649+
} else if (schema.enum && Array.isArray(schema.enum)) {
650+
for (const enumValue of schema.enum) {
651+
if (!constMap.has(key)) {
652+
constMap.set(key, new Map());
653+
}
654+
const valueMap = constMap.get(key)!;
655+
if (!valueMap.has(enumValue)) {
656+
valueMap.set(enumValue, []);
657+
}
658+
valueMap.get(enumValue)!.push(i);
659+
}
649660
}
650661
});
651662
}

src/test/completion.test.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -898,11 +898,13 @@ suite('JSON Completion', () => {
898898
]
899899
});
900900
await testCompletionsFor('{ "type": "1", "a" : { "x": "", "z":"" }, |', schema, {
901-
// both alternatives have errors: intellisense proposes all options
902-
count: 2,
901+
// Prior to the `enum` discriminator optimization, the parser failed to correlate `type: "1"`
902+
// via enums and fell back to offering completions from ALL `oneOf` branches (i.e., both 'b' and 'c').
903+
// Now that `enum` properties are properly indexed as discriminators, `type: "1"` flawlessly
904+
// isolates the first branch, so it correctly proposes ONLY the 'b' property from that branch.
905+
count: 1,
903906
items: [
904-
{ label: 'b' },
905-
{ label: 'c' }
907+
{ label: 'b' }
906908
]
907909
});
908910
await testCompletionsFor('{ "a" : { "x": "", "z":"" }, |', schema, {
@@ -1461,9 +1463,11 @@ suite('JSON Completion', () => {
14611463
]
14621464
});
14631465
await testCompletionsFor('{ "type": "foo|"', schema, {
1466+
// Since the user explicitly typed "foo", the parser instantly matches the enum discriminator
1467+
// for the first `oneOf` branch. Therefore, it discards the second branch (which expects "bar")
1468+
// and appropriately proposes ONLY "foo", rather than falling back to proposing both options.
14641469
items: [
1465-
{ label: '"foo"' },
1466-
{ label: '"bar"' }
1470+
{ label: '"foo"' }
14671471
]
14681472
});
14691473
});

src/test/parser.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3405,4 +3405,38 @@ suite('JSON Parser', () => {
34053405
assert.strictEqual(res.length, 0);
34063406
});
34073407

3408+
test('Validation should not take exponential time for recursive schemas with enum discriminators', async function() {
3409+
const schema: JSONSchema = {
3410+
$id: "http://example.com/schema",
3411+
definitions: {
3412+
rule: {
3413+
anyOf: [
3414+
{ type: "object", properties: { type: { enum: ["A"] }, content: { $ref: "#/definitions/rule" } } },
3415+
{ type: "object", properties: { type: { enum: ["B"] }, content: { $ref: "#/definitions/rule" } } },
3416+
{ type: "object", properties: { type: { enum: ["C"] }, content: { $ref: "#/definitions/rule" } } }
3417+
]
3418+
}
3419+
},
3420+
$ref: "#/definitions/rule"
3421+
};
3422+
3423+
// {"content": {"content": {"content": ... }}}
3424+
let nested = '{"type": "A"}';
3425+
for (let i = 0; i < 14; i++) {
3426+
nested = `{"type": "A", "content": ${nested}}`;
3427+
}
3428+
3429+
const { textDoc, jsonDoc } = toDocument(nested);
3430+
const ls = getLanguageService({});
3431+
ls.configure({ schemas: [{ fileMatch: ["*.json"], uri: "http://example.com/schema", schema }] });
3432+
3433+
const startTime = Date.now();
3434+
await ls.doValidation(textDoc, jsonDoc, undefined, schema);
3435+
const endTime = Date.now();
3436+
3437+
// Validation should finish well under 10ms. Before the `enum` optimization,
3438+
// this produced 4.7 million branch iterations and took nearly 5,000ms.
3439+
assert.ok(endTime - startTime < 100, "Validation took too long! Exponential scaling detected.");
3440+
});
3441+
34083442
});

0 commit comments

Comments
 (0)