microsoft / microsoft/TypeScript

`__setFunctionName` breaks custom static `name` on classes

Aperta
#62,854 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Bug Help Wanted
Lingua principale
Go
Stelle
111k
Fork
14.3k
Merge medio
2g 4h
PR unite (30g)
132

Descrizione

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

// 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:

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.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia in namedEvaluation.ts con createClassNamedEvaluationHelperBlock() e isClassNamedEvaluationHelperBlock(), quindi riproduci gli esempi TypeScript forniti nel playground e ispeziona il JavaScript generato. Il lavoro è completo quando le classi decorate e non decorate preservano le dichiarazioni statiche del nome che non sono campi e l'assegnazione automatica del nome della classe continua a funzionare.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
javascript, typescript
Ambito
compilers
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Ferma
Chiarezza
Specificata chiaramente
Idoneità per principianti
42/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.