diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index ef55fa7e5..2988f9feb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -62,6 +62,40 @@ export function lowerReflectCall( } if (call.questionDotToken) return null; const reflectMember = lowerer.stdlibGlobalMember(access, "Reflect"); + // Reflect.has(target, key) — the `in` operator's presence query over + // the target's checked-dynamic view (own and inherited names, proxy + // `has` traps included). A primitive target is Node's TypeError + // ("Reflect.has called on non-object"), which the presence query + // answers false for instead: fenced, never silently divergent. + if ( + reflectMember === "has" && + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some(ts.isSpreadElement) + ) { + const loc = locOf(call); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const primitive = + isUnitType(target.type) || + ["bool", "f64", "string", "bigint", "symbol"].includes(target.type.kind); + if (primitive) + lowerer.noLowering( + "Reflect.has on a primitive target", + call.arguments.nodes[0]!, + "Node throws a TypeError for a primitive target; narrow to an object first", + ); + if (!lowerer.dynConvertible(target.type)) + lowerer.noLowering("Reflect.has on this target", call.arguments.nodes[0]!); + return { + kind: "libCall", + fn: "dyn.hasKeyComputed", + args: [ + lowerer.coerceToExpected(target, DYN), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), + ], + type: BOOL, + loc, + }; + } // Reflect.getOwnPropertyDescriptor and Reflect.defineProperty over // checked objects (execa's Promise mixin): the Object operations, with // defineProperty answering true. A non-object target throws the Object diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 955b57610..fa7afada4 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -7885,6 +7885,10 @@ function lowerObjectStaticCall( isUnitType(object.type) ) object = { kind: "dynFrom", value: object, type: DYN, loc: locOf(call) }; + // A record, array or class instance reflects through its own + // checked-dynamic view (own fields and elements, in property order). + if (object.type.kind !== "dyn" && lowerer.dynConvertible(object.type)) + object = lowerer.coerceToExpected(object, DYN); if (object.type.kind === "dyn") return { kind: "libCall", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json new file mode 100644 index 000000000..4081fc5ef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8426-reflect-object-reflections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8426-reflect-object-reflections.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8426-reflect-object-reflections.ts b/tests/corpus/8426-reflect-object-reflections.ts new file mode 100644 index 000000000..bae308d81 --- /dev/null +++ b/tests/corpus/8426-reflect-object-reflections.ts @@ -0,0 +1,28 @@ +// Reflect.has over the checked-dynamic view, and the Object reflection +// operations over records, arrays and class instances (own fields and +// elements in property order). Reflect.has is [[HasProperty]], so inherited +// names (Array.prototype's, Object.prototype's, a class's methods) answer +// true; a primitive target is Node's TypeError and stays a lowerer fence +// (SC2020) rather than a silent `false`. +class Point { + x = 1; + y = 2; + sum(): number { + return this.x + this.y; + } +} +const record = { a: 1, b: "two" }; +const list = [10, 20]; +const point = new Point(); +console.log(Reflect.has(record, "a"), Reflect.has(record, "z")); +console.log(Reflect.has(list, "0"), Reflect.has(list, "length"), Reflect.has(list, "9")); +console.log(Reflect.has(point, "x"), Reflect.has(point, "sum")); +const key = "b"; +console.log(Reflect.has(record, key), Reflect.has(record, key.toUpperCase())); +console.log(Reflect.has(record, "toString"), Reflect.has(record, "constructor")); +console.log(Object.getOwnPropertyNames(record).join(",")); +console.log(Object.getOwnPropertyNames(point).join(",")); +console.log(Object.getOwnPropertyNames(list).length); +const descriptors = Object.getOwnPropertyDescriptors(record); +console.log(Object.keys(descriptors).join(",")); +console.log(Object.getOwnPropertySymbols(record).length); diff --git a/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json b/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json new file mode 100644 index 000000000..a1a93d6be --- /dev/null +++ b/tests/harness/corpus-costs/8426-reflect-object-reflections.ts.json @@ -0,0 +1 @@ +{ "plain": 3121, "san": 5843 }