Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix use-before-init error when targeting ES2022 #55028

Merged
merged 7 commits into from
Jul 28, 2023

Conversation

Zzzen
Copy link
Contributor

@Zzzen Zzzen commented Jul 15, 2023

Fixes #50971

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Jul 15, 2023
// foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property
return !(getEmitScriptTarget(compilerOptions) === ScriptTarget.ESNext && useDefineForClassFields
// foo = this.bar is illegal in es2022+useDefineForClassFields when bar is a parameter property
return !(getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2022 && useDefineForClassFields
Copy link
Member

@jakebailey jakebailey Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm pretty sure that this should just be useDefineForClassFields and not check the script target. It seems like getUseDefineForClassFields already has the check for the script target and may have been missed in #42663.

Honestly, I suspect that there are other uses of the useDefineForClassFields which get this wrong or are at least redundant. Skimming, these may be redundant:

  • isBlockScopedNameDeclaredBeforeUse
  • checkAndReportErrorForInvalidInitializer
  • getFirstTransformableStaticClassElement

And these might be wrong and would be fixed like in this PR:

  • checkConstructorDeclarationDiagnostics

@sandersn

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There of course may be a nuance I'm totally missing here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree with you. The checks for the emit target are unnecessary and should be removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emitted code for "useDefineForClassFields": true pre-class fields runs OK here, though: it's a series of Object.defineProperty calls in the constructor that replace what were previously assignments.

This is true first two uses of useDefineForClassFields you point out are the same way. You need both ES2022+ and useDefineForClassFields to emit ES standard class fields, and those--barring inheritance--are the things with different semantics from before.

Maybe it would be a good idea to introduce a new variable emitStandardClassFields that combines ES2022+ and useDefineForClassFields. Actually, useDefineForClassFields is combined with the ES2022 check often enough that it's likely that var emitStandardClassFields should replace var useDefinedForClassFields.

Either way, all the (===ESNext) occurrences need to change to (>=ES2022) now that class fields have been published in ES2022.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be a good idea to introduce a new variable emitStandardClassFields that combines ES2022+ and useDefineForClassFields. Actually, useDefineForClassFields is combined with the ES2022 check often enough that it's likely that var emitStandardClassFields should replace var useDefinedForClassFields.

I'm confused; isn't that what the useDefineForClassFields variable is after #42663?

var useDefineForClassFields = getUseDefineForClassFields(compilerOptions);

// ...
export function getUseDefineForClassFields(compilerOptions: CompilerOptions): boolean {
    return compilerOptions.useDefineForClassFields === undefined ? getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2022 : compilerOptions.useDefineForClassFields;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost, but when { useDefineForClassFields: true, target: "ES2017" }, getUseDefineForClassFields is true but emitStandardClassFields would be false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(emitStandardClassFields would be compilerOptions.useDefineForClassFields !== false && getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2022)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite the matrix of options 😄

Well, in any case, pulling these out to a consistent variable that does the right thing would be super helpful; it'd be nice to not have all of these es version comparisons that we can accidentally miss.

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically I think that this is okay, but I definitely want @sandersn / @rbuckton to double check this as even though my gut tells me that all of these other changed things (in the most recent commit) are actual fixes, I'm not totally confident in knowing the entire semantics.

src/compiler/checker.ts Show resolved Hide resolved
Comment on lines 33 to +36
baz = this.foo; // should error
~~~
!!! error TS2729: Property 'foo' is used before its initialization.
!!! related TS2728 assignParameterPropertyToPropertyDeclarationES2022.ts:11:17: 'foo' is declared here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good!

class C {
p = x
~
!!! error TS2301: Initializer of instance member variable 'p' cannot reference identifier 'x' declared in the constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has the combo of es2017/esnext and useDefineForClassFields=true/false. Before, es2017 always errored, and esnext only errored when useDefineForClassFields=false.

Now, es2017 with useDefineForClassFields=false also no longer errors.

Is that correct? I'm not totally sure. @sandersn

Comment on lines 1 to 2
// @target: esnext, es2021, es2022
// @useDefineForClassFields: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// @target: esnext, es2021, es2022
// @useDefineForClassFields: true
// @target: esnext, es2021, es2022
// @useDefineForClassFields: false, true

This should work and let you eliminate that other file, and make the baselines more clear, I think.

@jakebailey jakebailey requested review from sandersn and rbuckton July 20, 2023 18:37
@jakebailey
Copy link
Member

@typescript-bot test this
@typescript-bot test top100
@typescript-bot user test this
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 20, 2023

Heya @jakebailey, I've started to run the extended test suite on this PR at b2c1019. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 20, 2023

Heya @jakebailey, I've started to run the diff-based top-repos suite on this PR at b2c1019. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 20, 2023

Heya @jakebailey, I've started to run the diff-based user code test suite on this PR at b2c1019. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 20, 2023

Heya @jakebailey, I've started to run the tarball bundle task on this PR at b2c1019. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 20, 2023

Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/155949/artifacts?artifactName=tgz&fileId=D66F597AB441D33AFB66ABFF1F9D781C821FB075B2AA4BF288933CB0F56F809D02&fileName=/typescript-5.2.0-insiders.20230720.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.2.0-pr-55028-5".;

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user test suite comparing main and refs/pull/55028/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Unknown failure"
  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

rxjs-src

/mnt/ts_downloads/rxjs-src/build.sh

  • [NEW] error TS2428: All declarations of 'WeakMap' must have identical type parameters.
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
  • [MISSING] error TS2428: All declarations of 'WeakMap' must have identical type parameters.
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell, tsc almost worked correctly before this PR: it provides an error for ES2022 standard class fields and does not when they're downleveled via Object.defineProperty or simple assignment:

https://www.typescriptlang.org/play?moduleResolution=99&module=1&useDefineForClassFields=true#code/MYGwhgzhAECqDmALaBvAUNT0BWBXCALtALzQBEA7ogJ5kZYAOIAppMydABQCUJAfNAKIAlhAB0TVhGb1MAE2EAnZsCKkhosQA9Z0YAHsAdoUW5V+xZwa4ARiGHBoWgFzRDuALY3mi3uizQAL662GAAbmAAZhwa4jrBaKCQMAASzCAMPqi6wMpgBMw8rjb6+ixghtkBAcoEuIqVBKYyAcHBSVDQAEKK+gDWzJXoOUYmZgQWnHlyRiDU0JFgwGByzK5pGT5+ugEGxmXMYiD68JyxYja48Ny6wQGX8DEi4ovLq2K5rAU8aGgJhswKN1egNDJwAUCNplfEA

The only problem is that (===ESNext) needs to change to (>=ES2022) everywhere in the code.

I also suggested that var useDefineForForClassFields should maybe include the ES2022 check so that it's used consistently everywhere.

// foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property
return !(getEmitScriptTarget(compilerOptions) === ScriptTarget.ESNext && useDefineForClassFields
// foo = this.bar is illegal in es2022+useDefineForClassFields when bar is a parameter property
return !(getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2022 && useDefineForClassFields
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emitted code for "useDefineForClassFields": true pre-class fields runs OK here, though: it's a series of Object.defineProperty calls in the constructor that replace what were previously assignments.

This is true first two uses of useDefineForClassFields you point out are the same way. You need both ES2022+ and useDefineForClassFields to emit ES standard class fields, and those--barring inheritance--are the things with different semantics from before.

Maybe it would be a good idea to introduce a new variable emitStandardClassFields that combines ES2022+ and useDefineForClassFields. Actually, useDefineForClassFields is combined with the ES2022 check often enough that it's likely that var emitStandardClassFields should replace var useDefinedForClassFields.

Either way, all the (===ESNext) occurrences need to change to (>=ES2022) now that class fields have been published in ES2022.

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top-repos suite comparing main and refs/pull/55028/merge:

Everything looks good!

@jakebailey
Copy link
Member

@typescript-bot test this
@typescript-bot test top100
@typescript-bot user test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 21, 2023

Heya @jakebailey, I've started to run the diff-based top-repos suite on this PR at 66b8263. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 21, 2023

Heya @jakebailey, I've started to run the extended test suite on this PR at 66b8263. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 21, 2023

Heya @jakebailey, I've started to run the diff-based user code test suite on this PR at 66b8263. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user test suite comparing main and refs/pull/55028/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Unknown failure"
  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

rxjs-src

/mnt/ts_downloads/rxjs-src/build.sh

  • [NEW] error TS2428: All declarations of 'WeakMap' must have identical type parameters.
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-55028/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
  • [MISSING] error TS2428: All declarations of 'WeakMap' must have identical type parameters.
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top-repos suite comparing main and refs/pull/55028/merge:

Everything looks good!

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish we didn't still need useDefineForClassFields, but I checked the 3 remaining uses and they're all still (reasonably) correct.

Even more wishful thinking: someday it would be nice to drop --useDefineForClassFields, but I think a few big projects may still use it for some time.

Edit: After reading @rbuckton 's comment, I additionally wish that parameter properties could be deprecated. =)

@@ -31739,7 +31741,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
&& !(isAccessExpression(node) && isAccessExpression(node.expression))
&& !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right)
&& !(isMethodDeclaration(valueDeclaration) && getCombinedModifierFlagsCached(valueDeclaration) & ModifierFlags.Static)
&& (compilerOptions.useDefineForClassFields || !isPropertyDeclaredInAncestorClass(prop))) {
&& (useDefineForClassFields || !isPropertyDeclaredInAncestorClass(prop))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useDefineForClassFields is incomplete here; it errors when the property is used before its initialisation even if there's a base property. But this program always prints 1 when targeting both es2017 and es2022, and when useDefineForClassFields is true or false:

class Base {
    x = 1
}
class Ugh extends Base {
    direct = this.x
    x = 2
}
// should print 1 not undefined or 2
console.log(new Ugh().direct)

Removing it entirely shows two related test cases: redeclaredProperty and redefinedPararameterProperty [sic]. Here are test cases that should still error:

class Base {
    x = 1
}
class Ugh extends Base {
    x;
    direct = this.x
}
// should print undefined not 1 or 2 with [[Define]]
console.log(new Ugh().direct)

and

class Base {
    x = 1
}
class Ugh extends Base {
    direct = this.x
    constructor(public x) { }
}
// should print undefined not 1 or 2 with standard class fields.
console.log(new Ugh().direct)

Confusingly, the first case should error whenever useDefineForClassFields is on, but the second should only error when emitStandardClassFields is on.

After typing all that up, I think this is a separate problem. I'll file a separate bug for it.

Edit: Thinking about it a bit more, even if the first example works, it's bad code! You shouldn't rely on lexical order of property initialisation in order to reference a base property's value. I'll let somebody else file the bug, and if nobody has a problem with it, we can leave it as it is.

@rbuckton
Copy link
Member

I'm still of the opinion the underlying issue is with the emit, which I mentioned in the linked bug. I've been meaning to circle back on that after finishing the class fields emit-related changes for ES Decorators following 5.1, but haven't had time yet.

I suppose it's fine to issue an error for now in lieu of actually fixing the emit issue, but I do think we should take the time to fix the emit issue in the future and remove this restriction. If we ever plan to fix this, it would probably be a good idea to either not mark this as fixing #50971, or leave it as fixing #50971 and create a new issue that recommends an emit fix so that we don't lose track of the issue.

@fatcerberus
Copy link

fatcerberus commented Jul 24, 2023

@rbuckton Not sure I can agree with the proposed emit in the linked comment - moving the initializer from a classfield into the constructor observably changes runtime behavior of JS code which violates design goals, and in a way that can’t be justified by inaccurate downleveling (bug isn’t actually being downleveled, it’s still a native classfield).

IMO if I write an initializer on a classfield, ECMAScript says that runs before the constructor, so I just need to accept that behavior is not going to play well with parameter properties. I’d find it even more confusing if the timing of initialization changed depending on whether I have any parameter props or not.

@sandersn
Copy link
Member

@fatcerberus let's have that discussion on a new bug: #55132

@rbuckton Are you OK to merge this? It is at least a fix for an existing restriction, even if that restriction is incorrect.

@sandersn sandersn merged commit 23eabea into microsoft:main Jul 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
6 participants