Skip to content

Commit 98a94cf

Browse files
feat: add proper handling for additionalProperties
1 parent 8ceddc7 commit 98a94cf

1 file changed

Lines changed: 86 additions & 8 deletions

File tree

packages/core/src/rules/oas3/no-illogical-composition-keywords.ts

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,19 @@ function arePropertiesMutuallyExclusive(
249249
return { isExclusive: false };
250250
}
251251

252-
function areSignaturesMutuallyExclusive(sig1: SchemaSignature, sig2: SchemaSignature): ReturnType {
252+
function areSignaturesMutuallyExclusive(
253+
sig1: SchemaSignature,
254+
sig2: SchemaSignature,
255+
depth: number = 0
256+
): ReturnType {
257+
const MAX_DEPTH = 10;
258+
if (depth > MAX_DEPTH) {
259+
// If we've gone too deep, assume they're not mutually exclusive to be safe
260+
return {
261+
isExclusive: false,
262+
reason: 'Maximum recursion depth reached while checking schema exclusivity.',
263+
};
264+
}
253265
// Check top-level constraint exclusivity (types, formats, const values, enum values, and all constraints)
254266
const topLevelResult = arePropertiesMutuallyExclusive(sig1, sig2);
255267
if (!topLevelResult.isExclusive && topLevelResult.reason) {
@@ -333,14 +345,80 @@ function areSignaturesMutuallyExclusive(sig1: SchemaSignature, sig2: SchemaSigna
333345
const addlProps1 = sig1.additionalProperties;
334346
const addlProps2 = sig2.additionalProperties;
335347

336-
const allowsAdditional1 = addlProps1 !== false;
337-
const allowsAdditional2 = addlProps2 !== false;
348+
// Determine the type of additionalProperties for each schema
349+
const isBoolean1 = typeof addlProps1 === 'boolean';
350+
const isBoolean2 = typeof addlProps2 === 'boolean';
351+
const isSchema1 = isPlainObject(addlProps1);
352+
const isSchema2 = isPlainObject(addlProps2);
353+
354+
// Case 1: Both are booleans
355+
if (isBoolean1 && isBoolean2) {
356+
const allowsAdditional1 = addlProps1 !== false;
357+
const allowsAdditional2 = addlProps2 !== false;
338358

339-
if (allowsAdditional1 !== allowsAdditional2) {
340-
return {
341-
isExclusive: false,
342-
reason: 'One schema allows additional properties while the other does not.',
343-
};
359+
// If one allows and the other doesn't, they're not mutually exclusive (ambiguous)
360+
if (allowsAdditional1 !== allowsAdditional2) {
361+
return {
362+
isExclusive: false,
363+
reason: 'One schema allows additional properties while the other does not.',
364+
};
365+
}
366+
}
367+
// Case 2: One is boolean, the other is a schema/ref
368+
else if ((isBoolean1 && isSchema2) || (isSchema1 && isBoolean2)) {
369+
// If boolean is false, it means no additional properties allowed
370+
// If the other has a schema, they conflict (one allows with constraints, one disallows)
371+
const boolValue = isBoolean1 ? addlProps1 : addlProps2;
372+
373+
if (boolValue === false) {
374+
return {
375+
isExclusive: false,
376+
reason:
377+
'One schema disallows additional properties (false) while the other defines a schema for them.',
378+
};
379+
}
380+
// If boolean is true, one allows any additional properties, the other allows with constraints
381+
// This could be mutually exclusive if other constraints distinguish them, but the
382+
// additionalProperties themselves don't provide discrimination
383+
}
384+
// Case 3: Both are schemas/refs
385+
else if (isSchema1 && isSchema2) {
386+
// Check if the additionalProperties schemas are mutually exclusive
387+
const addlPropsResult = areSignaturesMutuallyExclusive(
388+
addlProps1 as SchemaSignature,
389+
addlProps2 as SchemaSignature,
390+
depth + 1
391+
);
392+
393+
// If the additionalProperties schemas are not mutually exclusive, it creates ambiguity
394+
if (!addlPropsResult.isExclusive) {
395+
return {
396+
isExclusive: false,
397+
reason: `Schemas have overlapping additionalProperties definitions. ${
398+
addlPropsResult.reason || ''
399+
}`,
400+
};
401+
}
402+
// If they ARE mutually exclusive, this helps distinguish the schemas
403+
// The additionalProperties provide discrimination, so schemas are mutually exclusive
404+
return { isExclusive: true };
405+
}
406+
// Case 4: One or both are undefined (default is true in JSON Schema)
407+
else {
408+
// Default behavior: if undefined, it typically means additionalProperties: true
409+
// If one is explicitly false and the other is undefined, they differ
410+
if (addlProps1 === false || addlProps2 === false) {
411+
const otherIsUndefined =
412+
addlProps1 === false ? !isDefined(addlProps2) : !isDefined(addlProps1);
413+
414+
if (otherIsUndefined) {
415+
return {
416+
isExclusive: false,
417+
reason:
418+
'One schema explicitly disallows additional properties while the other allows them by default.',
419+
};
420+
}
421+
}
344422
}
345423
}
346424

0 commit comments

Comments
 (0)