fix: address Copilot review round 5 — union member faithfulness, partial discriminators, const-null siblings
- rootUnion: union members get the same flattenability test as allOf members,
so a branch whose nested `allOf: [false]` makes it unsatisfiable is no
longer offered as a callable alternative with that constraint erased.
- selectBranchIndex: weigh only the pinned constants the caller SUPPLIED. One
it did not supply is a value this identification exists to seed, so
requiring it meant a deep link naming `kind` alone matched no branch
whenever the branches also pinned, say, a `version`. A supplied constant
that disagrees is still evidence against.
- admitsNull: `const: null` is conjunctive with its siblings, not an
override — `{ type: "string", const: null }` and
`{ const: null, anyOf: [...] }` reject every value, so the claim is made
only where the type and a sibling union leave null on the table.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
7e4af5ed34
commit
f556f8831b
2 changed files with 103 additions and 14 deletions
|
|
@ -368,17 +368,15 @@ function collapsed<T extends NullableUnionSchema>(
|
|||
* an optimistic guess, and a schema this module cannot read renders through the
|
||||
* JSON editor with its constraints intact.
|
||||
*/
|
||||
/** Whether an absent or null-naming `type` leaves null on the table. */
|
||||
function typeAdmitsNull(type: string | string[] | undefined): boolean {
|
||||
return type === undefined || typeNamesNull(type);
|
||||
}
|
||||
|
||||
export function admitsNull(schema: NullableUnionSchema): boolean {
|
||||
if (nullExcludedBySiblings(schema)) {
|
||||
return false;
|
||||
}
|
||||
// `const: null` admits null and nothing else, whatever the schema says (or
|
||||
// omits) about `type`. Without this a required field pinned to null is seeded
|
||||
// with the only value it accepts and then reported as missing, leaving submit
|
||||
// permanently disabled on a form the user cannot change (#2123).
|
||||
if (schema.const === null) {
|
||||
return true;
|
||||
}
|
||||
// Applicators this module does not evaluate. `not: { type: "null" }` rules
|
||||
// null out; `allOf` can add a member that does; and `oneOf` requires
|
||||
// **exactly one** branch to match, so a null branch does not by itself mean
|
||||
|
|
@ -388,6 +386,20 @@ export function admitsNull(schema: NullableUnionSchema): boolean {
|
|||
if (hasOpaqueApplicator(schema)) {
|
||||
return false;
|
||||
}
|
||||
// `const: null` admits null and nothing else — the case a required field
|
||||
// pinned to null needs, or it is seeded with the only value it accepts and
|
||||
// then reported missing, leaving submit permanently disabled on a field the
|
||||
// user cannot change (#2123).
|
||||
//
|
||||
// Claimed only where the siblings agree, since `const` is conjunctive with
|
||||
// them rather than an override: `{ type: "string", const: null }` and
|
||||
// `{ const: null, anyOf: [...] }` reject every value, and saying otherwise
|
||||
// would let the gate accept a `null` the schema forbids. The opaque
|
||||
// applicators (`not`, `allOf`, `oneOf`) are already refused above.
|
||||
if (schema.const === null) {
|
||||
return schema.anyOf === undefined && typeAdmitsNull(schema.type);
|
||||
}
|
||||
|
||||
if (schema.nullable === true) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,6 +430,11 @@ export function resolveRootUnion<T extends RootUnionSchema>(
|
|||
(branch) =>
|
||||
branch === null ||
|
||||
!isOfferable(branch) ||
|
||||
// The same faithfulness test the `allOf` fold applies: a member
|
||||
// carrying a constraint the merge does not copy — a nested `allOf`, a
|
||||
// `not`, a `$ref` — would have it erased along with the union keyword,
|
||||
// which can present an unsatisfiable branch as a callable one.
|
||||
!isFlattenable(branch) ||
|
||||
conflictsWithBase(base, branch),
|
||||
)
|
||||
) {
|
||||
|
|
@ -476,9 +481,14 @@ export function selectBranchIndex<T extends RootUnionSchema>(
|
|||
] as const,
|
||||
)
|
||||
.filter(([, constValue]) => constValue !== undefined);
|
||||
// Only the pinned names the caller actually supplied are evidence. A
|
||||
// constant it did not supply is one this identification exists to *seed* —
|
||||
// requiring it would mean a deep link naming `kind` alone matched no branch
|
||||
// whenever the branches also pin, say, a `version`.
|
||||
const supplied = pinned.filter(([name]) => values[name] !== undefined);
|
||||
if (
|
||||
pinned.length > 0 &&
|
||||
pinned.every(([name, constValue]) => values[name] === constValue)
|
||||
supplied.length > 0 &&
|
||||
supplied.every(([name, constValue]) => values[name] === constValue)
|
||||
) {
|
||||
matches.push(index);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue