Skip to content

Commit 82a0201

Browse files
feat: add checks for object min/max props, required props and additionlProperties
1 parent 6d87e72 commit 82a0201

File tree

1 file changed

+75
-0
lines changed

1 file changed

+75
-0
lines changed

packages/core/src/rules/oas3/no-illogical-one-of-usage.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,18 @@ function arePropertySchemasMutuallyExclusive(
261261
}
262262
}
263263

264+
// Object property count constraints - check if ranges don't overlap
265+
if (prop1.type === 'object' && prop2.type === 'object') {
266+
const minProps1 = prop1.minProperties ?? 0;
267+
const maxProps1 = prop1.maxProperties ?? Infinity;
268+
const minProps2 = prop2.minProperties ?? 0;
269+
const maxProps2 = prop2.maxProperties ?? Infinity;
270+
271+
if (rangesDoNotOverlap(minProps1, maxProps1, minProps2, maxProps2)) {
272+
return { isExclusive: true };
273+
}
274+
}
275+
264276
return { isExclusive: false };
265277
}
266278

@@ -358,6 +370,26 @@ function areSignaturesMutuallyExclusive(
358370
reason: `Schemas have overlapping properties: ${ambiguousProperties.join(', ')}.`,
359371
};
360372
}
373+
374+
// Check if one schema requires properties that the other doesn't have at all
375+
// This makes them mutually exclusive
376+
if (sig1.required && sig1.required.size > 0) {
377+
const requiredNotInSig2Properties = [...sig1.required].filter(
378+
(prop) => !sig2.properties || !sig2.properties.has(prop)
379+
);
380+
if (requiredNotInSig2Properties.length > 0) {
381+
return { isExclusive: true };
382+
}
383+
}
384+
385+
if (sig2.required && sig2.required.size > 0) {
386+
const requiredNotInSig1Properties = [...sig2.required].filter(
387+
(prop) => !sig1.properties || !sig1.properties.has(prop)
388+
);
389+
if (requiredNotInSig1Properties.length > 0) {
390+
return { isExclusive: true };
391+
}
392+
}
361393
}
362394

363395
// Array items check
@@ -371,6 +403,32 @@ function areSignaturesMutuallyExclusive(
371403
}
372404
}
373405

406+
// additionalProperties check - schemas with conflicting additionalProperties settings
407+
if (sig1.additionalProperties !== undefined || sig2.additionalProperties !== undefined) {
408+
const addlProps1 = sig1.additionalProperties;
409+
const addlProps2 = sig2.additionalProperties;
410+
411+
// If one explicitly disallows additional properties (false) and the other allows them (true or schema)
412+
// AND they have overlapping required properties, this creates ambiguity
413+
const allowsAdditional1 = addlProps1 !== false;
414+
const allowsAdditional2 = addlProps2 !== false;
415+
416+
if (allowsAdditional1 !== allowsAdditional2) {
417+
// Check if they have overlapping required properties
418+
if (sig1.required && sig2.required) {
419+
const requiredOverlap = [...sig1.required].filter((prop) => sig2.required!.has(prop));
420+
if (requiredOverlap.length > 0) {
421+
return {
422+
isExclusive: false,
423+
reason: `Schemas have conflicting additionalProperties settings with overlapping required properties: ${requiredOverlap.join(
424+
', '
425+
)}.`,
426+
};
427+
}
428+
}
429+
}
430+
}
431+
374432
return { isExclusive: true };
375433
}
376434

@@ -405,6 +463,23 @@ function checkOneOfMutualExclusivity(
405463
resolve: UserContext['resolve'],
406464
parentSchema: Oas3Schema | Oas3_1Schema
407465
): void {
466+
// Check for empty schemas first - an empty schema {} accepts any value
467+
for (let i = 0; i < schemas.length; i++) {
468+
const schema = schemas[i];
469+
470+
// Skip $ref schemas - they're not empty
471+
if (isRef(schema)) continue;
472+
473+
// Check if schema is empty (no properties defined)
474+
const keys = Object.keys(schema);
475+
if (keys.length === 0) {
476+
report({
477+
message: `Empty schema at position ${i} in \`oneOf\` matches all values and cannot be mutually exclusive.`,
478+
location: location.child(['oneOf', i]),
479+
});
480+
}
481+
}
482+
408483
// Check for impossible nullable + oneOf with null type combination
409484
// This is a specific anti-pattern where the parent schema is nullable
410485
// AND one of the oneOf options is type: 'null', making it impossible to distinguish

0 commit comments

Comments
 (0)