microsoft / microsoft/TypeScript
`__setFunctionName` breaks custom static `name` on classes
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 111k
- Forks
- 14.3k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 132
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up ā it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
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