Fix false-positive TS2354 for native private class field access with importHelpers at dated targets - #4841
Conversation
…ted targets Port of the fix for microsoft/TypeScript#63728 to this codebase, since the checks in question (setNodeLinksForPrivateIdentifierScope, checkPropertyAccessExpressionOrQualifiedName, checkInExpression in internal/checker/checker.go) were carried over unchanged from the classic compiler's checker.ts by the private-field-helpers porting work, and exhibit the identical bug. With importHelpers: true and a dated target (ES2022 through ES2025), the checks required tslib for private-identifier access via: c.languageVersion < LanguageFeatureMinimumTarget.PrivateNamesAndClassStaticBlocks || c.languageVersion < LanguageFeatureMinimumTarget.ClassAndClassElementDecorators || !c.compilerOptions.GetUseDefineForClassFields() Since decorators have never been assigned a dated ECMAScript edition, ClassAndClassElementDecorators is pinned to the ESNext sentinel, so the middle clause is true for every dated target unconditionally, regardless of whether the file uses decorators at all. The actual emitter (estransforms/classfields.go's shouldTransformPrivateElementsOrClassStaticBlocks) only checks languageVersion < ES2022, with no decorator-related gating, so this required tslib to be resolvable even though the emitted JS for plain private field/method/accessor access never references it. The !GetUseDefineForClassFields() clause has the same problem: private fields are always emitted using 'define' semantics regardless of that option (it only affects public fields), so it also doesn't correspond to any real difference in whether tslib is needed - verified by emitting with --useDefineForClassFields false --target es2022 and confirming the output is fully native with zero tslib references. A fourth call site with the same ClassAndClassElementDecorators check, inside getFirstTransformableStaticClassElement, is left unchanged: it's used only to decide whether a *decorated* class needs the __setFunctionName helper for hoisting its static private/static-block elements, which is a genuine, verified coupling between decorators and private statics (confirmed by emitting a decorated class with a static private method and observing __setFunctionName really is called). Note on scope: per CONTRIBUTING.md, this repo is currently only accepting 6.0/7.0-difference or crash fixes. This bug is not a 6.0/7.0 difference - it reproduces identically in the classic compiler (see microsoft/TypeScript#63728, and companion fix microsoft/TypeScript#63729) and this port, since the checker logic was ported over unchanged. Submitting here anyway since this is shared logic both compilers carry, and the fix keeps their behavior in sync; happy to close this if it's considered out-of-scope for the 7.0 bridge period. Related to microsoft/TypeScript#63728. --- Disclosure: This PR was authored with AI assistance (GitHub Copilot CLI), directed by a specific human operator (astegmaier) investigating and fixing this one specific, previously-filed issue. It is not part of a bulk or queue-driven workflow, and I will personally shepherd it through review and respond to feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Ryan Cavanaugh (@RyanCavanaugh) - this was the bug fix that we just discussed. Disclosure: I put a lot of human effort into making the simplified repro for the original bug (https://github.com/astegmaier/typescript-private-field-tslib-repro). But this PR was agent-made -- by pointing it at the reproduction -- but it seems simple and sensible enough (really just three small lines) that it was worth submitting. |
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/checker/checker.go:11272
- Decorator lowering is another path that emits these helpers even at ES2022+.
internal/transformers/estransforms/esdecorator.go:632-635marks class-decorated classes with static private elements for forced transformation, andclassfields.go:1064-1071/1495-1518then rewrites their reads and writes to__classPrivateFieldGet/__classPrivateFieldSet. With this target-only guard, the checker no longer validates those imported helpers, so a partial or incompatibletslibcan type-check but fail in emitted code. Please retain helper checking when decorator lowering forces the referenced static private element to transform, and add a decorated-static-private regression case.
internal/checker/checker.go:13083 - This also drops required helper validation for
#x in valuewhen#xis static and its class is class-decorated. The decorator transform setsEFTransformPrivateStaticElementsregardless of target (esdecorator.go:632-635), after whichclassfields.go:1606-1608emits__classPrivateFieldIn. At ES2022+ this guard now skips checking thattslibexports the emitted helper. Please include the decorator-forced static-private path in this condition as well.
…elements Follow-up to the earlier fix in this branch for microsoft/TypeScript#63728: that fix simplified checkPropertyAccessExpressionOrQualifiedName, checkInExpression, and setNodeLinksForPrivateIdentifierScope to only require tslib's classPrivateField* helpers below ES2022, since the actual emitter doesn't need them for plain private field/method/accessor access at ES2022+. However, that simplification missed one real exception, caught by an automated PR review: when a class has a native (non-legacy) class decorator AND at least one static private/auto-accessor element, decorator lowering (estransforms/esdecorator.go's hasStaticPrivateClassElements / shouldTransformPrivateStaticElementsInClass) hoists ALL of that class's static private/auto-accessor elements out of the class body into a wrapping closure. That hoisting forces accesses to those specific elements to use the classPrivateFieldGet/Set/In helpers from tslib, even at ES2022+ - so the checker needs to keep validating tslib exports them in this narrow case. Instance private fields in the same decorated class are unaffected and stay fully native. Adds isStaticPrivateElementOfDecoratedClass, mirroring newESDecoratorTransformer's exact skip condition (decorator lowering only doesn't happen for !legacyDecorators when target >= ESNext with useDefineForClassFields true), and ORs it into the two checks that validate these specific helpers (checkPropertyAccessExpressionOrQualifiedName, checkInExpression). setNodeLinksForPrivateIdentifierScope is intentionally left alone: its NodeCheckFlagsContainsClassWithPrivateIdentifiers flag has exactly one consumer (checkWeakMapSetCollision), which independently gates on languageVersion <= ES2021, making any change there dead code for the ES2022+ scenario this follow-up addresses. Verified with two independent review passes plus manual testing: - Confirmed via actual JS emission that decorated classes' static private field/method/accessor access and '#x in obj' checks call tslib_1.__classPrivateFieldGet/Set/In, while instance private field access in the same decorated class stays fully native. - Confirmed the fix correctly requires the helpers for static private fields, methods, get/set accessors, and auto-accessors in decorated classes; correctly leaves instance fields, non-decorated classes, and legacy (experimentalDecorators) classes unaffected; and correctly follows the useDefineForClassFields/ESNext edge case (decorator lowering is skipped, and no helpers are needed, only at target >= ESNext with useDefineForClassFields true). - Ran the full Go test suite (go test ./...) and the submodule conformance suite filtered to privateName|esDecorators|importHelpers|classField|classStaticBlock - all passing. - hereby lint and hereby format are clean. - Investigated two additional discrepancies surfaced during review (a missing TS2354 for decorator-only helpers, and an emit panic for static auto-accessors, both specific to target: esnext with useDefineForClassFields: false) and confirmed both are pre-existing in main, unrelated to any change in this branch, and out of scope for this fix. Adds three new compiler tests: - importHelpersRequiredForDecoratedStaticPrivateElements.ts: a decorated class with an instance field plus static field/method/get-set-accessor/auto-accessor and a static '#x in obj' check, with a tslib stub missing the classPrivateField* helpers - confirms exactly the expected 3 errors (one per helper) anchored at the static accesses, with zero errors for the instance field. - importHelpersRequiredForDecoratedStaticPrivateElementsUseDefineForClassFieldsFalse.ts: same shape at target: esnext with useDefineForClassFields: false, confirming decorator lowering (and thus the helper requirement) still applies in that specific target/option combination. - importHelpersNotRequiredForDecoratedStaticPrivateElementsAtESNext.ts: the same shape at target: esnext with the default useDefineForClassFields: true, confirming zero errors and fully native emission when decorator lowering is skipped entirely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
In response to copilot feedback (agent generated): I verified this was a real regression (confirmed via actual JS emission that decorated classes' static private elements do call |
Related to microsoft/TypeScript#63728, and a Go port of the companion fix microsoft/TypeScript#63729.
The bug
With
importHelpers: trueand a datedtarget(e.g.ES2022–ES2025),tsgoincorrectly reportsTS2354: This syntax requires an imported helper but module 'tslib' cannot be foundfor plain, fully-native private field/method/accessor access (this.#x,#x in obj, static private fields/methods, private accessors, static blocks, private fields in generics/closures/class expressions/inheritance) — even though the emitted JS for all of these is 100% native ES2022+ syntax that never referencestslib.Root cause
setNodeLinksForPrivateIdentifierScope,checkPropertyAccessExpressionOrQualifiedName, andcheckInExpressionininternal/checker/checker.gogate the private-identifier helper-requirement check on:ClassAndClassElementDecoratorsis pinned to theESNextsentinel because TC39 decorators have never been assigned to a dated ECMAScript edition. That makeslanguageVersion < ClassAndClassElementDecoratorstruefor every dated target, forever — regardless of whether the file uses decorators at all. The actual emitter (estransforms/classfields.go'sshouldTransformPrivateElementsOrClassStaticBlocks) only checkslanguageVersion < ES2022, with no decorator-related gating, so this term in the checker never corresponded to real emit behavior.!c.compilerOptions.GetUseDefineForClassFields()has the same problem: private fields are always emitted using "define" semantics regardless ofuseDefineForClassFields(that option only affects public fields), so it also doesn't correspond to any real difference in whethertslibis needed for private-field access. Verified by emitting with--useDefineForClassFields false --target es2022: output is fully native with notslibreferences, yet the checker still errored before this fix.The fix
Removes both spurious clauses from the 3 call sites above, leaving just
c.languageVersion < LanguageFeatureMinimumTarget.PrivateNamesAndClassStaticBlocks.A 4th call site with the same
ClassAndClassElementDecoratorscheck, insidegetFirstTransformableStaticClassElement, is intentionally left unchanged. That one only matters when a class is decorated, to decide if the decorator transform's hoisting of static private/static-block elements into an IIFE needs the__setFunctionNamehelper. I verified (by emitting@dec class C { static #foo() {} }at ES2022) that this really is required in the actual output, so this clause is a genuine, necessary coupling and was left in place.Testing
testdata/tests/cases/compiler/importHelpersNoHelpersForPrivateFieldsAtES2022.ts— target ES2022,importHelpers: true, notslibpresent, covering instance/static private fields, private methods, static private methods, private accessors, private auto-accessors (instance + static), static blocks, and#x in obj. Asserts zero errors; baseline.jsconfirms the emit never referencestslib.testdata/tests/cases/compiler/importHelpersNoHelpersForPrivateFieldsAtES2022UseDefineForClassFieldsFalse.ts— same coverage withuseDefineForClassFields: false, confirming that option no longer triggers the false positive either.go test ./...); all pass except one unrelated, pre-existing flaky test ininternal/fswatch(macOSfseventstiming test, confirmed to fail/pass independently of this change by running it in isolation both with and without the fix).TestSubmodule) filtered toprivateName|esDecorators|importHelpers|classField|classStaticBlock— all pass, including the decorator+static-private-method case that verifies__setFunctionNameis still correctly required.hereby lintandhereby formatboth clean.tsgobinary that decorators andusingdeclarations still correctly requiretslibat dated targets (these are real transforms, not part of this bug), and that all "confirmed bug" scenarios from the original repro (https://github.com/astegmaier/typescript-private-field-tslib-repro) now compile cleanly with zero references totslibin the emitted output.Update: follow-up fix for a regression found by automated review
The automated Copilot PR reviewer correctly caught a real gap in the fix above: removing the
ClassAndClassElementDecoratorsclause fromcheckPropertyAccessExpressionOrQualifiedNameandcheckInExpressionwent slightly too far. When a class has a native (non-legacy) class decorator and at least one static private/auto-accessor element, decorator lowering (estransforms/esdecorator.go'shasStaticPrivateClassElements/shouldTransformPrivateStaticElementsInClass) hoists all of that class's static private/auto-accessor elements out of the class body into a wrapping closure — which forces accesses to them to use the__classPrivateFieldGet/Set/Inhelpers fromtslib, even at ES2022+. The simplified checks stopped validating thattslibactually exports those helpers in this narrow scenario, which could let an incompletetslibsilently pass type-checking and then fail at runtime. Instance private fields in the same decorated class are unaffected and stay fully native (confirmed by direct JS emission).I verified this regression directly: reverted to the pre-fix checker, confirmed it correctly errored (
TS2343: ... '__classPrivateFieldGet' ... does not exist in 'tslib') for this scenario, then confirmed the (until-now) fixed checker silently accepted it — a real regression, not a false alarm.The follow-up fix adds
isStaticPrivateElementOfDecoratedClass, which mirrorsnewESDecoratorTransformer's exact skip condition (!legacyDecorators && (target < ESNext || !useDefineForClassFields)), and ORs it back into the two checks (checkPropertyAccessExpressionOrQualifiedName,checkInExpression) that validate theclassPrivateField*helpers specifically.setNodeLinksForPrivateIdentifierScopeis intentionally left alone — its only consumer (checkWeakMapSetCollision) independently gates onlanguageVersion <= ES2021, making any change there dead code for this scenario.Additional verification for the follow-up
#x in objchecks, really do calltslib_1.__classPrivateFieldGet/Set/In, while instance private field access in the same decorated class stays fully native — in every target/option combination that matters (es2022;esnextwithuseDefineForClassFieldstrueandfalse; legacyexperimentalDecorators, where this combination is grammatically forbidden anyway).TS2354for the decorator's own helpers, and an emit panic for static auto-accessors — both specific totarget: esnextwithuseDefineForClassFields: false): both are confirmed pre-existing inmain, unrelated to any change in this branch, and out of scope for this fix.go test ./...) and the submodule conformance suite filtered toprivateName|esDecorators|importHelpers|classField|classStaticBlock— all passing.hereby lintandhereby formatclean.New tests
importHelpersRequiredForDecoratedStaticPrivateElements.ts— a decorated class with an instance field plus static field/method/get-set-accessor/auto-accessor and a static#x in objcheck, with atslibstub that provides the decorator helpers but omits theclassPrivateField*ones. Confirms exactly the 3 expected errors (one per helper), anchored at the static accesses, with zero errors for the instance field.importHelpersRequiredForDecoratedStaticPrivateElementsUseDefineForClassFieldsFalse.ts— same shape attarget: esnextwithuseDefineForClassFields: false, confirming decorator lowering (and the helper requirement) still applies in that specific combination.importHelpersNotRequiredForDecoratedStaticPrivateElementsAtESNext.ts— the same shape attarget: esnextwith the defaultuseDefineForClassFields: true, confirming zero errors and fully native emission when decorator lowering is skipped entirely.