diff --git a/.chronus/changes/fix-extension-proto-member-2026-8-22.md b/.chronus/changes/fix-extension-proto-member-2026-8-22.md new file mode 100644 index 00000000000..e05925e6da7 --- /dev/null +++ b/.chronus/changes/fix-extension-proto-member-2026-8-22.md @@ -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. diff --git a/.chronus/changes/fix-marshal-proto-member-2026-8-21.md b/.chronus/changes/fix-marshal-proto-member-2026-8-21.md new file mode 100644 index 00000000000..9fb79af0034 --- /dev/null +++ b/.chronus/changes/fix-marshal-proto-member-2026-8-21.md @@ -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. diff --git a/packages/compiler/src/core/js-marshaller.ts b/packages/compiler/src/core/js-marshaller.ts index b15b35729fc..e33cf7e5d35 100644 --- a/packages/compiler/src/core/js-marshaller.ts +++ b/packages/compiler/src/core/js-marshaller.ts @@ -86,11 +86,11 @@ function numericValueToJs(type: NumericValue, valueConstraint: Type | undefined) } function objectValueToJs(type: ObjectValue): Record { - const result: Record = {}; - 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)); diff --git a/packages/compiler/test/checker/decorators.test.ts b/packages/compiler/test/checker/decorators.test.ts index 0dad4ab7b20..c1d0bbf295a 100644 --- a/packages/compiler/test/checker/decorators.test.ts +++ b/packages/compiler/test/checker/decorators.test.ts @@ -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", () => { diff --git a/packages/openapi/src/decorators.ts b/packages/openapi/src/decorators.ts index f43d02c6f57..dc4efba3d1a 100644 --- a/packages/openapi/src/decorators.ts +++ b/packages/openapi/src/decorators.ts @@ -97,14 +97,13 @@ function convertRemainingValuesToExtensions(program: Program, value: unknown): u if (isTypeSpecValue(value)) { return serializeValueAsJson(program, value, value.type); } else { - const result: Record = {}; - 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; diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index 5739379883b..7fa8d543344 100644 --- a/packages/openapi/test/decorators.test.ts +++ b/packages/openapi/test/decorators.test.ts @@ -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, @@ -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" }] } },