fix: address Copilot review round 14 — allOf under additionalProperties, own-property const match

- rootUnion: the added-names guard is now a shared `addsForbiddenNames` and
  applies to the `allOf` fold as well as to union branches. A member adding a
  name a restrictive root `additionalProperties` rejects would otherwise be
  merged beside the keyword, where the name reads as allowed.
- selectBranchIndex tests `Object.hasOwn` before reading a pinned value: a
  field legally named `constructor` resolved to the inherited one and ruled
  out the very branch that pins it.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 05:36:30 -04:00
commit 5b6d68a9b6

View file

@ -147,6 +147,24 @@ describe("resolveRootUnion", () => {
expect(base.properties).toBeUndefined();
});
it("leaves an allOf that adds names a restrictive additionalProperties forbids", () => {
// The root rejects `x` as an additional property; folding the member in
// would move it beside the keyword, where it reads as allowed.
const { base } = resolveRootUnion({
type: "object",
additionalProperties: false,
allOf: [
{
type: "object",
properties: { x: { type: "string" } },
required: ["x"],
},
],
});
expect(base.allOf).toHaveLength(1);
expect(base.properties).toBeUndefined();
});
it("merges allOf branches unconditionally", () => {
const { base, branches } = resolveRootUnion({
type: "object",
@ -754,6 +772,31 @@ describe("resolveRootUnion", () => {
expect(selectBranchIndex(objectPinned, { tag: { a: 2 } })).toBe(1);
});
it("does not read an inherited property as a supplied constant", () => {
// `constructor` is a legal field name; reading the inherited one would
// rule out the very branch that pins it.
const pinnedOnInherited = resolveRootUnion({
type: "object",
anyOf: [
{
type: "object",
properties: Object.fromEntries([
["constructor", { const: "a" }],
["x", { type: "string" }],
]),
},
{
type: "object",
properties: Object.fromEntries([
["constructor", { const: "b" }],
["y", { type: "string" }],
]),
},
] as unknown[],
}).branches;
expect(selectBranchIndex(pinnedOnInherited, { x: "supplied" })).toBe(0);
});
it("reports none when the values identify nothing", () => {
expect(selectBranchIndex(branches, {})).toBeNull();
expect(selectBranchIndex(branches, { kind: "other" })).toBeNull();