Skip to content

Emit expando properties for functions that only become visible late in declaration emit#4535

Open
UditDewan wants to merge 2 commits into
microsoft:mainfrom
UditDewan:fix/expando-late-visibility
Open

Emit expando properties for functions that only become visible late in declaration emit#4535
UditDewan wants to merge 2 commits into
microsoft:mainfrom
UditDewan:fix/expando-late-visibility

Conversation

@UditDewan

Copy link
Copy Markdown

Fixes #4461

Problem

The declaration transformer collects expando property assignments in a prepass over the whole file (#4261), and drops any assignment whose host declaration isn't visible at that point (#3716). But a non-exported host can still become visible during statement transformation: printing the type of an exported declaration can reference it and late-mark its statement visible. The host then gets emitted without its expando properties:

declare function wrap<T>(component: T): T;

function FunctionComponent() { return null; }
FunctionComponent.propTypes = { num: 0 };

export const WrappedFunction = wrap(FunctionComponent);

previously emitted:

declare function FunctionComponent(): null;
export declare const WrappedFunction: typeof FunctionComponent;

dropping propTypes, while TS 6 emits the merged namespace:

declare function FunctionComponent(): null;
declare namespace FunctionComponent {
    var propTypes: {
        num: number;
    };
}
export declare const WrappedFunction: typeof FunctionComponent;

Fix

Instead of dropping assignments for not-yet-visible hosts, defer them; when a statement is popped off the late-marked queue in transformAndReplaceLatePaintedStatements, process its deferred expando assignments first. This matches strada's behavior, where the expando namespace is synthesized while transforming the (possibly late-marked) host statement itself, i.e. after visibility has settled. Hosts that never become visible still emit nothing and never get their property types printed, preserving #3686.

Notes for review

  • No existing baselines change, and the compiler-harness regression test can't actually catch the bug: the harness computes declaration diagnostics before emitting, and that first transform pass late-marks the host in the shared emit resolver as a side effect, so the second (emit) pass already sees it as visible. A real tsgo build runs the transform once and hits the bug. The regression test for the broken case is therefore a tsc CLI test (expando function properties for function only referenced via typeof), which runs emit cold; the compiler test locks the emitted shape and covers the arrow/unused-function variants in TS and JS.
  • Of the issue's table, this fixes FunctionComponentWithText (.js/.tsx) and FunctionComponentMemoized (.tsx). FunctionComponentMemoized/.js also gains .propTypes through typeof FunctionComponent (TS 6: ❌). That cell can't stay ❌ while fixing FunctionComponentWithText/.js, because both flow through the same typeof FunctionComponent reference in corsa's JS declaration emit — TS 6 only distinguished them because its JS emit copied expando members onto each exported alias instead of using typeof. This seems consistent with the documented JS declaration emit differences in CHANGES.md, but happy to adjust if you'd rather gate this for JS files.

Disclosure

This patch was authored with AI assistance (Claude Code), per the CONTRIBUTING.md disclosure request.

…n declaration emit

The declaration transformer collects expando assignments in a prepass
and dropped any whose host declaration wasn't visible at that point.
A non-exported host can still be late-marked visible while printing the
type of an exported declaration (e.g. as 'typeof host'), which then
emitted the host without its expando namespace. Defer such assignments
and process them if their host statement is late-marked visible,
instead of dropping them.

Fixes microsoft#4461
Copilot AI review requested due to automatic review settings July 5, 2026 04:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a declaration-emit gap where expando property assignments (e.g. FunctionComponent.propTypes = ...) were dropped if the host declaration was not yet “visible” during the expando-collection prepass, but later became visible during declaration transformation (e.g. via typeof).

Changes:

  • Defer expando assignments whose host isn’t visible at collection time, and replay them when the host statement is processed from the late-marked visibility queue.
  • Add a compiler regression test covering late-visible expando function properties in both TS and JS inputs.
  • Add a cold-emit CLI (tsc-style) regression scenario + reference baseline to validate the emitted .d.ts shape matches expected merged-namespace output.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/transformers/declarations/transform.go Defers expando assignments for not-yet-visible hosts and processes them when the host is late-marked visible.
internal/execute/tsctests/tsc_test.go Adds a new declaration-emit CLI regression scenario reproducing the late-visibility expando case.
testdata/tests/cases/compiler/declarationEmitLateVisibleExpandoFunction.ts New compiler test case covering TS+JS variants (function + arrow + unused host).
testdata/baselines/reference/compiler/declarationEmitLateVisibleExpandoFunction.js Expected emit baseline for the new compiler test (includes merged namespace for visible hosts).
testdata/baselines/reference/compiler/declarationEmitLateVisibleExpandoFunction.types Expected types baseline for the new compiler test.
testdata/baselines/reference/compiler/declarationEmitLateVisibleExpandoFunction.symbols Expected symbols baseline for the new compiler test.
testdata/baselines/reference/tsc/declarationEmit/expando-function-properties-for-function-only-referenced-via-typeof.js New CLI baseline ensuring cold emit includes expando namespace for late-visible function host.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Behavior difference: Inconsistency when supplying (internal) expando function to another generic function call for exports

2 participants