mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-11 02:03:59 +08:00
Name a function by its definition, not by the binding
`var a = String; a.name` printed "a". Name inference followed any const binding chain and answered the last identifier, but V8's SetFunctionName runs only for an anonymous function or class definition: every other value keeps the name it already has. The resolution now stops at a named definition, another binding or a member read, and follows only an anonymous definition's binding, which is the binding that named the function when it was created. An import alias keeps that name: `const root = (text) => text` exported from a module and imported as `builder` is still "root". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9558fcddf3
commit
bba843a43a
@@ -2166,6 +2166,19 @@ function builtinMemberValueName(
|
||||
return builtinModuleFnOf(lowerer, module, member)?.valueName ?? null;
|
||||
}
|
||||
|
||||
/** Whether `node` is a function, arrow or class DEFINITION that carries no
|
||||
* name of its own, so the binding or property it initializes names it
|
||||
* (V8's SetFunctionName). Every other expression evaluates to a value that
|
||||
* already has whatever name it has. */
|
||||
function isAnonymousCallableDefinition(node: ts.Node): boolean {
|
||||
if (ts.isArrowFunction(node)) return true;
|
||||
return (ts.isFunctionExpression(node) || ts.isClassExpression(node)) && node.name === undefined;
|
||||
}
|
||||
|
||||
/** The name V8 gives a function value read from this expression: its own
|
||||
* definition, through parenthesized, rebindable and const bindings and
|
||||
* property reads (`process.cwd`), or the binding an anonymous definition
|
||||
* initializes. Null when the source does not determine it. */
|
||||
export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null {
|
||||
let n: ts.Node = node;
|
||||
while (ts.isParenthesizedExpression(n)) n = n.expression;
|
||||
@@ -2179,20 +2192,31 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null {
|
||||
n = declaration;
|
||||
break;
|
||||
}
|
||||
if (
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
!declaration.initializer ||
|
||||
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) break;
|
||||
let value: ts.Node = declaration.initializer;
|
||||
while (ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
// Name inference (V8's SetFunctionName) covers an ANONYMOUS function or
|
||||
// class definition and nothing else: the binding names it, so a
|
||||
// rebindable binding keeps the binding's spelling below. Any other
|
||||
// initializer is a value that carries its own name — a named
|
||||
// definition, a builtin function (`var a = String; a.name` is
|
||||
// "String") or another binding (`var a = b` keeps b's name) — and
|
||||
// resolution follows it.
|
||||
if (isAnonymousCallableDefinition(value)) {
|
||||
// The binding this definition initializes named it when it was
|
||||
// created, so answer that binding and not the alias the reference
|
||||
// reached it through: `import b from "./m"` keeps the "root" of
|
||||
// `const root = (text) => text;` in `m`.
|
||||
if (ts.isIdentifier(declaration.name)) return declaration.name.text;
|
||||
break;
|
||||
n = declaration.initializer;
|
||||
while (ts.isParenthesizedExpression(n)) n = n.expression;
|
||||
}
|
||||
n = value;
|
||||
}
|
||||
if (ts.isIdentifier(n)) return n.text;
|
||||
if (ts.isPropertyAccessExpression(n)) return n.name.text;
|
||||
if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) && n.name) return n.name.text;
|
||||
if (ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
||||
if (ts.isClassExpression(n) && n.name) return n.name.text;
|
||||
if (ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isClassExpression(n)) {
|
||||
// An anonymous function takes the name of the binding or property it
|
||||
// initializes, through parentheses (`const f = (function () {})`).
|
||||
let outer: ts.Node = n;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-function-name-inference.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-function-name-module-alias/names.js",
|
||||
"<repo>/tests/corpus/js-function-name-module-alias/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// V8 names a function by its definition: an anonymous function or class
|
||||
// expression takes the name of the binding or property it initializes, and
|
||||
// every other value keeps the name it already has. A builtin function bound
|
||||
// to a variable is therefore still `String`, not the variable's name.
|
||||
var FromVar = String;
|
||||
console.log(FromVar.name, FromVar.length, FromVar === String);
|
||||
let FromLet = Number;
|
||||
console.log(FromLet.name, FromLet === Number);
|
||||
const FromConst = Boolean;
|
||||
console.log(FromConst.name);
|
||||
var Chained = FromVar;
|
||||
console.log(Chained.name);
|
||||
let ThroughParens = (Number);
|
||||
console.log(ThroughParens.name);
|
||||
console.log(String.name, Number.name, Boolean.name);
|
||||
|
||||
function declared() {}
|
||||
var Held = declared;
|
||||
console.log(Held.name);
|
||||
|
||||
// Anonymous definitions still take the binding's name.
|
||||
var Anonymous = function () {};
|
||||
console.log(Anonymous.name);
|
||||
let Arrow = () => {};
|
||||
console.log(Arrow.name);
|
||||
const Parenthesized = (function () {});
|
||||
console.log(Parenthesized.name);
|
||||
const Class = class {};
|
||||
console.log(Class.name);
|
||||
var Named = function inner() {};
|
||||
console.log(Named.name);
|
||||
const NamedClass = class InnerClass {};
|
||||
console.log(NamedClass.name);
|
||||
@@ -0,0 +1,7 @@
|
||||
// A function imported under a different name keeps the name of the binding
|
||||
// that created it: V8's SetFunctionName runs once, at the definition.
|
||||
import builder from "./names.js";
|
||||
|
||||
console.log(builder.name);
|
||||
const alias = builder;
|
||||
console.log(alias.name);
|
||||
@@ -0,0 +1,3 @@
|
||||
const root = (text) => text;
|
||||
|
||||
export default root;
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2400, "san": 3600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 4000 }
|
||||
Reference in New Issue
Block a user