microsoft / microsoft/TypeScript

`__setFunctionName` breaks custom static `name` on classes

未关闭
#62,854 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

Bug Help Wanted
主要语言
Go
星标
111k
派生
14.3k
平均合并
2 天 4 小时
30 天内合并 PR
132

描述

### 🔎 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.

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 namedEvaluation.ts 中的 createClassNamedEvaluationHelperBlock() 和 isClassNamedEvaluationHelperBlock() 开始,然后在 playground 中复现所提供的 TypeScript 示例,并检查生成的 JavaScript。完成的标准是:带装饰器和不带装饰器的类都保留非字段的静态名称声明,同时自动类命名仍然有效。

由索引模型根据 Issue 内容生成。

评估

技术栈
javascript, typescript
领域
compilers
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
停滞
描述清晰度
描述清楚
新手友好度
42/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。