microsoft / microsoft/TypeScript

`__setFunctionName` breaks custom static `name` on classes

Open
#62,854 2 comments 0 reactions 0 assignees View on GitHub
Bug Help Wanted
Dominant language
Go
Stars
111k
Forks
14.3k
PR merge metrics
PR metrics pending

Description

### šŸ”Ž Search Terms

__setFunctionName
class static name
ES decorators static name
__esDecorate static name

### šŸ•— Version & Regression Information

5.0.4-5.9.3

### āÆ Playground Link

https://tsplay.dev/WJgblw

### šŸ’» Code

```ts
const noop = (Self: any, ctx: ClassDecoratorContext) => {};
const rand = () => 4;

@noop export default class {
static get name() { return 2434; }
}

(@noop class {
static get name() { return 2434; }
});

(@noop class __A {
static get name() { return 2434; }
});

@noop
class __B {
static get name() { return 2434; }
}

var __C = @noop class {
static get name() { return 2434; }
};

var __D = {
[rand()]: @noop class {
static get name() { return 2434; }
}
};

// Unconditionally throws at runtime:
@noop class __E {
static #a = 2434;
static get name() { return this.#a; }
}

// Undecorated classes are broken as well:
var __F = class {
static get name() { return 2434; }
// target≤es2021 => '__F'
// target≄es2022 => 2434
static {}
}
```

### šŸ™ Actual behavior

Static `name` is overridden by `__setFunctionName()`.

### šŸ™‚ Expected behavior

Static `name` should work as defined in the class code.

### Additional information about the issue

Generated code for the ES decorators breaks custom static `name` property on the decorated class. The only syntax that works is plain `static name;` field declaration; other kinds (methods, `get`/`set`/`accessor`) are broken.

The issue reproduces iff the compiler decides to inject the `static { __setFunctionName(this, ...) }` block, which _unconditionally_ overrides the static `name` descriptor on the class object.

The issue also affects undecorated classes (when targeting older environments; see the examples).

> I’m not sure what _exactly_ triggers the `__setFunctionName()` block injection. Sometimes it’s injected even if the class has a correct unambiguous automatic name. Sometimes it **isn’t** injected, even if the class becomes incorrectly named (e.g., when targeting `es3`/`es5`):
>
>
> Missing `__setFunctionName()`
>
> https://tsplay.dev/WKYAMN
>
> ```typescript
> // tests/cases/compiler/blockScopedVariablesUseBeforeDef.ts
> function foo8() {
> let y = class {
> a = x;
> };
> let x;
>
> // @ts-ignore
> console.log(y.name);
> }
>
> // target≤es5 => 'class_###'
> // target≄es2015 => 'y'
> foo8();
> ```
>
>
>
> But either way, it shadows non-field static `name` declarations.

## Possible Fix

At _runtime_, check that the descriptor of `this.name` matches the automatic `name` descriptor shape (i.e., `value:string`, `writable≔enumerable≔false`, `configurable=true`). _The runtime check is necessary to correctly handle static `name` declared with a computed key._

Technically, the necessary and sufficient condition is even simpler—just check for `writable≔false` before `__setFunctionName` to robustly prevent overriding of non-field static `name` declarations:

- At this point, the descriptors are completely defined by the syntax:
- No `static {}` blocks evaluated.
- No class decorators evaluated.
- No class element initializers evaluated.
- So, the static `name` is one of:
- Automatic, _always_ defined. If there’s a static `name` _field_, it’s to be reconfigured at the time of the actual field initialization.
- Method.
- `get`/`set` or `accessor`.
- Methods have `writable≔true`.
- `get`/`set`/`accessor` declarations have `writable≔undefined`.

This matches the correct behavior:

- With a _field_ `static name` declaration, it’s the _automatic_ `name` up to the time of the actual `static name` initialization. The field initialization successfully overrides the automatic descriptor.
- For non-fields, the descriptor matches the declaration from the very beginning of the class. If the `name` is reconfigured here, the actual declaration doesn’t revert the override.

Adjust `createClassNamedEvaluationHelperBlock()` and `isClassNamedEvaluationHelperBlock()` in `namedEvaluation.ts` to produce a code like this:

```typescript
static {
Object.getOwnPropertyDescriptor(this, "name").writable === false &&
__setFunctionName(this, ...);
}
```

> **NB:** With `target≤es5` and `useDefineForClassFields≔false`, the compiler uses plain `C.name=...` assignment to set the static `name`, which has no effect (doesn’t reconfigure the static `name` descriptor), so the injected block—if injected—will call `__setFunctionName()` _unconditionally_. But this is already a compile-time error
>
> ```
> Static property 'name' conflicts with built-in property
> 'Function.name' of constructor function 'A'. (2699)
> ```
>
> so it doesn’t matter here.

Contributor guide

Open the contributing guide

Research direction

Start in namedEvaluation.ts with createClassNamedEvaluationHelperBlock() and isClassNamedEvaluationHelperBlock(), then reproduce the provided TypeScript examples in the playground and inspect the generated JavaScript. Done means decorated and undecorated classes preserve non-field static name declarations while automatic class naming still works.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.