From 35c48c93a68404803ad61fea708115e44a9835f3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 19:23:31 -0500 Subject: [PATCH] Name functions stored in JavaScript object literals as Node does A function stored in a JavaScript object literal's property printed as [Function (anonymous)] and had an empty `.name`. The property's boxed function now takes the name Node gives it: a function read by name keeps that name (`{ f }`, `{ k: f }`), and an anonymous function, arrow or method takes the property's key (`{ g: function () {} }`, `{ m() {} }`, `{ 'a-b': () => 1 }`, a computed key that folds to one string). --- .../lowering/expressions/object-literals.ts | 32 ++++++++++++++++++- ...760-object-literal-function-names.cjs.json | 6 ++++ .../6760-object-literal-function-names.cjs | 29 +++++++++++++++++ ...760-object-literal-function-names.cjs.json | 1 + 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6760-object-literal-function-names.cjs.json create mode 100644 tests/corpus/6760-object-literal-function-names.cjs create mode 100644 tests/harness/corpus-costs/6760-object-literal-function-names.cjs.json diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index e276326c9..565db0cd8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -20,7 +20,7 @@ import type { IrExpr, IrLocal, IrRecordShape, IrStmt, IrType, SrcLoc } from "../ import { isCjsExportTableLiteral, isJsSourceFile, locOf } from "../../program.js"; import { NARROW_FIRST } from "../surfaces.js"; import { recordShapeMismatchDiag } from "../../../diagnostics/diagnostic.js"; -import { PoisonError, neverTaintedJsType } from "../lowerer.js"; +import { PoisonError, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js"; import type { Lowerer } from "../lowerer.js"; import { lowerIndexMergeHelper } from "../containers/indexed-objects.js"; import type { IndexMergeContributor } from "../containers/indexed-objects.js"; @@ -188,6 +188,16 @@ function propNameText(lowerer: Lowerer, name: ts.PropertyName): string { return (name as ts.Identifier | ts.StringLiteral).text; } +/** The name an anonymous function or a method takes from its property key + * (NamedEvaluation): an identifier, string or numeric key, or a computed + * key that folds to one string. Null for other keys. */ +function propertyFunctionName(lowerer: Lowerer, name: ts.PropertyName): string | null { + if (ts.isComputedPropertyName(name)) return literalComputedKey(lowerer, name); + if (ts.isNumericLiteral(name)) return String(Number(name.text)); + if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text; + return null; +} + /** The runtime-keyed JS object literal (a computed key that doesn't fold): * builds a dyn object member-by-member. Keys evaluate before their values, * properties in source order — JS's object-literal evaluation exactly. @@ -406,6 +416,26 @@ export function lowerDynObjectLiteral( let v = boxValue ? boxValue(valueExpr as ts.Expression, raw) : lowerer.coerceToExpected(raw, DYN); + // A function that boxes into a property takes its name as Node gives + // it: a function read by name keeps that name (`{ f }`, `{ k: f }`), + // and an anonymous function or a method takes the property's name + // (`{ g: function () {} }`, `{ m() {} }`). + if ( + !boxValue && + v.kind === "dynFrom" && + v.value.type.kind === "func" && + v.fnName === undefined + ) { + let anonymous: ts.Node = valueExpr; + while (ts.isParenthesizedExpression(anonymous)) anonymous = anonymous.expression; + const name = + ts.isMethodDeclaration(prop) || + ((ts.isFunctionExpression(anonymous) || ts.isArrowFunction(anonymous)) && + !(ts.isFunctionExpression(anonymous) && anonymous.name)) + ? propertyFunctionName(lowerer, prop.name!) + : jsFuncNameOf(valueExpr, lowerer); + if (name !== null) v = { ...v, fnName: name }; + } // A JS object's array-valued properties retain the same source array. // Repeated references must compare equal and mutations remain shared. if ( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6760-object-literal-function-names.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6760-object-literal-function-names.cjs.json new file mode 100644 index 000000000..7ed9ff707 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6760-object-literal-function-names.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6760-object-literal-function-names.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6760-object-literal-function-names.cjs b/tests/corpus/6760-object-literal-function-names.cjs new file mode 100644 index 000000000..618716a3f --- /dev/null +++ b/tests/corpus/6760-object-literal-function-names.cjs @@ -0,0 +1,29 @@ +'use strict'; +// A function stored in an object literal's property keeps the name Node +// gives it: a function read by name keeps that name, and an anonymous +// function, arrow or method takes the property's key. +function f() {} +const g = function () {}; +const h = () => 1; +const named = function inner() {}; +const o = { + f, + g, + h, + k: f, + anon: function () {}, + arrow: () => 2, + 'a-b': () => 3, + ['computed']: () => 4, + m() {}, + named, + inline: function own() {}, +}; +for (const key of Object.keys(o)) console.log(key, JSON.stringify(o[key].name)); +console.log(o); +const mixed = { n: 1, s: 'x', f, g: function () {} }; +console.log(mixed, mixed.g.name); +function show(value) { + console.log(value); +} +show({ f, m() {} }); diff --git a/tests/harness/corpus-costs/6760-object-literal-function-names.cjs.json b/tests/harness/corpus-costs/6760-object-literal-function-names.cjs.json new file mode 100644 index 000000000..229d6a30a --- /dev/null +++ b/tests/harness/corpus-costs/6760-object-literal-function-names.cjs.json @@ -0,0 +1 @@ +{ "plain": 2630, "san": 3350 }