Skip to content

Commit 897600d

Browse files
committed
fixed exponential recursion in calculateIndexScore by removing recursive getClosestMatchingOption call for nested oneOf
1 parent 31b1736 commit 897600d

2 files changed

Lines changed: 25 additions & 28 deletions

File tree

packages/utils/src/schema/getClosestMatchingOption.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,9 @@ export const JUNK_OPTION: StrictRJSFSchema = {
3030
/** Recursive function that calculates the score of a `formData` against the given `schema`. The computation is fairly
3131
* simple. Initially the total score is 0. When `schema.properties` object exists, then all the `key/value` pairs within
3232
* the object are processed as follows after obtaining the formValue from `formData` using the `key`:
33-
* - If the `value` contains a `$ref`, `calculateIndexScore()` is called recursively with the formValue and the new
34-
* schema that is the result of the ref in the schema being resolved and that sub-schema's resulting score is added to
35-
* the total.
36-
* - If the `value` contains a `oneOf` and there is a formValue, then score based on the index returned from calling
37-
* `getClosestMatchingOption()` of that oneOf.
33+
* - If the `value` contains a `$ref`, it is resolved and scoring continues on the resolved schema.
34+
* - If the `value` contains a `oneOf`/`anyOf` and there is a formValue, the first matching option is found via
35+
* `getFirstMatchingOption()` and `calculateIndexScore()` is called recursively on that option.
3836
* - If the type of the `value` is 'object', `calculateIndexScore()` is called recursively with the formValue and the
3937
* `value` itself as the sub-schema, and the score is added to the total.
4038
* - If the type of the `value` matches the guessed-type of the `formValue`, the score is incremented by 1, UNLESS the
@@ -66,36 +64,33 @@ export function calculateIndexScore<T = any, S extends StrictRJSFSchema = RJSFSc
6664
return score;
6765
}
6866
if (has(value, REF_KEY)) {
69-
const newSchema = retrieveSchema<T, S, F>(
67+
value = retrieveSchema<T, S, F>(
7068
validator,
7169
value as S,
7270
rootSchema,
7371
formValue,
7472
experimental_customMergeAllOf,
7573
);
76-
return (
77-
score +
78-
calculateIndexScore<T, S, F>(
79-
validator,
80-
rootSchema,
81-
newSchema,
82-
formValue || {},
83-
experimental_customMergeAllOf,
84-
)
85-
);
8674
}
8775
if ((has(value, ONE_OF_KEY) || has(value, ANY_OF_KEY)) && formValue) {
8876
const key = has(value, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY;
77+
const options = get(value, key) as S[];
8978
const discriminator = getDiscriminatorFieldFromSchema<S>(value as S);
79+
const matched = getFirstMatchingOption<T, S, F>(validator, formValue, options, rootSchema, discriminator);
80+
const resolvedOption = retrieveSchema<T, S, F>(
81+
validator,
82+
options[matched] as S,
83+
rootSchema,
84+
formValue,
85+
experimental_customMergeAllOf,
86+
);
9087
return (
9188
score +
92-
getClosestMatchingOption<T, S, F>(
89+
calculateIndexScore<T, S, F>(
9390
validator,
9491
rootSchema,
92+
resolvedOption,
9593
formValue,
96-
get(value, key) as S[],
97-
-1,
98-
discriminator,
9994
experimental_customMergeAllOf,
10095
)
10196
);

packages/utils/test/schema/getClosestMatchingOptionTest.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
4545
it('returns 0 when formData is empty object', () => {
4646
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, {})).toEqual(0);
4747
});
48-
it('returns 1 for first option in oneOf schema', () => {
49-
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, ONE_OF_SCHEMA_DATA)).toEqual(1);
48+
it('returns 2 for first option in oneOf schema', () => {
49+
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, ONE_OF_SCHEMA_DATA)).toEqual(2);
5050
});
51-
it('returns 8 for second option in oneOf schema', () => {
52-
expect(calculateIndexScore(testValidator, oneOfSchema, secondOption, ONE_OF_SCHEMA_DATA)).toEqual(9);
51+
it('returns 10 for second option in oneOf schema', () => {
52+
expect(calculateIndexScore(testValidator, oneOfSchema, secondOption, ONE_OF_SCHEMA_DATA)).toEqual(10);
5353
});
5454
it('returns 1 for a schema that has a type matching the formData type', () => {
5555
expect(calculateIndexScore(testValidator, oneOfSchema, { type: 'boolean' }, true)).toEqual(1);
@@ -161,9 +161,10 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
161161
},
162162
};
163163
const formData = { ipsum: { night: 'nicht' } };
164-
// Mock to return true for the last of the second one-ofs
164+
// Mock: first 3 calls (JUNK for lorem, lorem itself, JUNK for ipsum) fail;
165+
// 4th call (ipsum with oneOf stripped to {}) succeeds, giving a unique valid match
165166
testValidator.setReturnValues({
166-
isValid: [false, false, false, false, false, false, false, true],
167+
isValid: [false, false, false, true],
167168
});
168169
expect(getClosestMatchingOption(testValidator, schema, formData, get(schema, 'items.oneOf'))).toEqual(1);
169170
});
@@ -207,9 +208,10 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
207208
},
208209
};
209210
const formData = { ipsum: { night: 'nicht' } };
210-
// Mock to return true for the last of the second anyOfs
211+
// Mock: first 3 calls (JUNK for lorem, lorem itself, JUNK for ipsum) fail;
212+
// 4th call (ipsum with anyOf stripped to {}) succeeds, giving a unique valid match
211213
testValidator.setReturnValues({
212-
isValid: [false, false, false, false, false, false, false, true],
214+
isValid: [false, false, false, true],
213215
});
214216
expect(getClosestMatchingOption(testValidator, schema, formData, get(schema, 'items.anyOf'))).toEqual(1);
215217
});

0 commit comments

Comments
 (0)