Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/fix-extension-proto-member-2026-8-22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi"
---

Fix `@extension` dropping a member named `__proto__` from an object value. The member is now kept as a regular own property instead of being assigned through the `Object.prototype.__proto__` setter, which dropped it and, when its value was an object, made that value the prototype of the stored extension.
7 changes: 7 additions & 0 deletions .chronus/changes/fix-marshal-proto-member-2026-8-21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Fix object values passed to decorators dropping a member named `__proto__`. The member is now marshalled as a regular own property instead of being assigned through the `Object.prototype.__proto__` setter, which dropped it and, when its value was an object, made that value the prototype of the marshalled object.
10 changes: 5 additions & 5 deletions packages/compiler/src/core/js-marshaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ function numericValueToJs(type: NumericValue, valueConstraint: Type | undefined)
}

function objectValueToJs(type: ObjectValue): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const [key, value] of type.properties) {
result[key] = marshalTypeForJs(value.value, undefined);
}
return result;
// Object.fromEntries defines each member as an own property, so a member named `__proto__`
// is kept instead of going through the Object.prototype setter and being dropped.
return Object.fromEntries(
[...type.properties].map(([key, value]) => [key, marshalTypeForJs(value.value, undefined)]),
);
}
function arrayValueToJs(type: ArrayValue) {
return type.values.map((x) => marshalTypeForJs(x, undefined));
Expand Down
23 changes: 23 additions & 0 deletions packages/compiler/test/checker/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,29 @@ describe("compiler: checker: decorators", () => {
);
deepStrictEqual(arg, { name: { other: "foo" } });
});

// Regression tests for https://github.com/microsoft/typespec/issues/11743
it("keeps a member named __proto__ holding a string as an own property", async () => {
const arg = await testCallDecorator("valueof unknown", `#{__proto__: "written", ok: 1}`);
ok(Object.prototype.hasOwnProperty.call(arg, "__proto__"));
strictEqual(arg.__proto__, "written");
strictEqual(arg.ok, 1);
deepStrictEqual(Object.keys(arg), ["__proto__", "ok"]);
strictEqual(Object.getPrototypeOf(arg), Object.prototype);
});

it("keeps a member named __proto__ holding an object as an own property", async () => {
const arg = await testCallDecorator(
"valueof unknown",
`#{__proto__: #{polluted: true}, ok: 1}`,
);
ok(Object.prototype.hasOwnProperty.call(arg, "__proto__"));
deepStrictEqual(arg.__proto__, { polluted: true });
strictEqual(arg.ok, 1);
deepStrictEqual(Object.keys(arg), ["__proto__", "ok"]);
strictEqual(Object.getPrototypeOf(arg), Object.prototype);
strictEqual(arg.polluted, undefined);
});
});

describe("passing an array value", () => {
Expand Down
15 changes: 7 additions & 8 deletions packages/openapi/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,13 @@ function convertRemainingValuesToExtensions(program: Program, value: unknown): u
if (isTypeSpecValue(value)) {
return serializeValueAsJson(program, value, value.type);
} else {
const result: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value)) {
if (val === undefined) {
continue;
}
result[key] = convertRemainingValuesToExtensions(program, val);
}
return result;
// Object.fromEntries defines each member as an own property, so a member named
// `__proto__` is kept instead of going through the Object.prototype setter.
return Object.fromEntries(
Object.entries(value)
.filter(([, val]) => val !== undefined)
.map(([key, val]) => [key, convertRemainingValuesToExtensions(program, val)]),
);
}
default:
return value;
Expand Down
30 changes: 29 additions & 1 deletion packages/openapi/test/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expectDiagnostics, t } from "@typespec/compiler/testing";
import { deepStrictEqual } from "assert";
import { deepStrictEqual, ok, strictEqual } from "assert";
import { describe, it } from "vitest";
import {
getExtensions,
Expand Down Expand Up @@ -38,6 +38,34 @@ describe("@extension", () => {
});
});

// Regression tests for https://github.com/microsoft/typespec/issues/11743
it("keeps a member named __proto__ holding a string as an own property", async () => {
const { program, Foo } = await Tester.compile(t.code`
@extension("x-custom", #{__proto__: "written", ok: 1})
model ${t.model("Foo")} {}
`);

const value = getExtensions(program, Foo).get("x-custom");
ok(Object.prototype.hasOwnProperty.call(value, "__proto__"));
deepStrictEqual(Object.keys(value), ["__proto__", "ok"]);
strictEqual(value.__proto__, "written");
strictEqual(Object.getPrototypeOf(value), Object.prototype);
});

it("keeps a member named __proto__ holding an object as an own property", async () => {
const { program, Foo } = await Tester.compile(t.code`
@extension("x-custom", #{__proto__: #{polluted: true}, ok: 1})
model ${t.model("Foo")} {}
`);

const value = getExtensions(program, Foo).get("x-custom");
ok(Object.prototype.hasOwnProperty.call(value, "__proto__"));
deepStrictEqual(Object.keys(value), ["__proto__", "ok"]);
deepStrictEqual(value.__proto__, { polluted: true });
strictEqual(Object.getPrototypeOf(value), Object.prototype);
strictEqual(value.polluted, undefined);
});

it.each([
{ value: `#{ name: "foo" }`, expected: { name: "foo" } },
{ value: `#{ items: #[ #{foo: "bar" }]}`, expected: { items: [{ foo: "bar" }] } },
Expand Down