Skip to content

Return all files from getFilesAffectedBy when a changed file affects global scope#4665

Merged
jakebailey merged 3 commits into
microsoft:mainfrom
martijnwalraven:callboard/global-scope-invalidation
Jul 23, 2026
Merged

Return all files from getFilesAffectedBy when a changed file affects global scope#4665
jakebailey merged 3 commits into
microsoft:mainfrom
martijnwalraven:callboard/global-scope-invalidation

Conversation

@martijnwalraven

Copy link
Copy Markdown
Contributor

Fixes #4664

This is a 6.0/7.0 behavior difference in --build/--incremental
invalidation (in-scope per CONTRIBUTING's bridge-release policy).

What

When a changed file affectsGlobalScope, getFilesAffectedBy now returns
the all-files-excluding-default-library set — the set it already computed and
cached — instead of falling through to the reverse-reference walk. This is
Strada's behavior: getFilesAffectedByUpdatedShapeWhenModuleEmit
(src/compiler/builderState.ts) returns
getAllFilesExcludingDefaultLibraryFile(...) for a global-scope shape
change.

Why

getFilesAffectedBy set hasAllFilesExcludingDefaultLibraryFile and primed
the all-files cache on a global-scope change, but still returned only the
changed file plus its reverse closure bounded by unchanged .d.ts signatures.
handleDtsMayChangeOfAffectedFile takes an early return for every affected
file when that flag is set, skipping the reverse-referencedBy invalidation
that removes dependents' cached semantic diagnostics. Strada's equivalent
early return (src/compiler/builder.ts, handleDtsMayChangeOfAffectedFile)
is safe only because its affected set is all files; tsgo kept the early
return but dropped the all-files set.

Consequences on main (see linked issue for a CLI repro and mechanism detail):

  • a lone global-scope .d.ts content change does not re-check files that use
    the global without importing the changed module — the incremental build
    stays green while a clean build errors;
  • because the flag is handler-global, one global-scope file in a changed
    batch (the shape of a package-manager dependency bump) suppresses dependent
    invalidation for every other changed file in that batch;
  • the buildinfo then records the affected files as fully-checked-clean
    (absence of a semanticDiagnosticsPerFile entry), so the false green is
    durable across subsequent builds.

Test coverage

Three new tsbuild/dependencyUpdate scenarios in
internal/execute/tsctests/tscbuild_test.go:

  • rebuilds transitive dependents when dependency update batch includes a global scope change — dep-A breaking change + dep-B declare global
    change in one batch; the break surfaces in a consumer connected to dep-A
    only through a module whose .d.ts signature does not change; includes a
    no-change follow-up edit showing durability. Failed on unmodified main via
    the harness's incremental-vs-clean diff (incremental missed TS2366).
  • rebuilds transitive dependents when dependency update batch has no global scope change — control arm; behaved correctly before and after.
  • rebuilds files using globals when global scope dependency is updated
    lone global-scope content change violating a non-importing user. Failed on
    unmodified main (incremental missed TS2322).

Baseline impact

15 existing baselines change, uniformly the intended class: after a
global-scope file changes, the other files are now refreshed — recomputed
.d.ts signatures (*refresh* / (computed .d.ts) in program baselines),
cached diagnostics dropped, and files queued for full emit
(affectedFilesPendingEmit gains the sibling files; same-content rewrites
appear in output listings):

  • tsc/incremental/when-global-file-is-added,-the-signatures-are-updated
    the scenario's titular signatures are now actually updated for sibling
    files.
  • tsc|tsbuild|tsbuildWatch/noEmit/*-with-incremental-as-modules (12 files)
    — these scenarios edit a.ts into a non-module (global) file mid-scenario;
    b.ts is now refreshed and queued for emit when that happens.
  • tsc/noEmitOnError/when-declarationMap-changes,
    tsbuildWatch/noEmit/does-not-go-in-loop-when-watching-... — same class
    (the watch scenario's actual concern, not looping, is unchanged).

Performance note: a global-scope change now invalidates and re-checks all
non-library files, like Strada. The all-files set is computed once per
build (cached on the snapshot), and per-file handling stays on the existing
early-return path (removeSemanticDiagnosticsOf + signature update), so the
cost profile matches Strada's for the same transition; builds with no
global-scope change in the batch are unaffected.

Validation

  • go test ./internal/execute/... — green.
  • go test ./... — green (macOS; TypeScript-submodule-gated tests skipped —
    submodule not cloned locally).
  • CLI three-way check on the linked issue's repro: typescript@6.0.3 errors
    on the incremental rebuild, unmodified main exits 0 silently, this branch
    errors identically to 6.0 and to a clean build.

Disclosure

Per CONTRIBUTING's AI-assistance policy: this patch was authored with an AI
coding agent (Claude Code). I have read and understood the change, chose this
specific issue myself, and will shepherd it through review personally.

martijnwalraven and others added 2 commits July 17, 2026 09:53
…global scope change

Three dependencyUpdate scenarios covering reverse-dependency semantic
invalidation when changed files affect the global scope:

- a batched update where one dependency gets a breaking type change
  while another dependency's update also changes the global scope; the
  break surfaces in a consumer that only references the broken
  dependency through an intermediate module whose .d.ts signature does
  not change
- a control arm with the same breaking change but no global scope
  co-change
- a lone global scope content change whose new type is violated by a
  file that uses the global without importing the changed module

The first and third cases currently fail on main with an unexpected
incremental-vs-clean diff: the incremental build reports no errors
while a clean build reports TS2366/TS2322, and the written buildinfo
carries no semanticDiagnosticsPerFile entry or pending marker for the
affected files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…global scope

Ports Strada's getFilesAffectedByUpdatedShapeWhenModuleEmit behavior
(builderState.ts): when a changed file affects the global scope, the
affected set is every file other than the default library files.

getFilesAffectedBy previously set hasAllFilesExcludingDefaultLibraryFile
and primed the all-files cache on a global-scope change, but still
returned only the changed file plus its reverse closure bounded by
unchanged .d.ts signatures. handleDtsMayChangeOfAffectedFile then takes
an early return for every affected file when that flag is set, skipping
the reverse-referencedBy invalidation that removes dependents' cached
semantic diagnostics. Strada's equivalent early return in builder.ts is
safe only because its affected set is all files; with the narrow set,
dependents outside the signature-bounded closure kept their cached-clean
entries and the incremental build reported no errors where a clean build
reports them. Because the flag is handler-global, one global-scope file
in a changed batch also suppressed dependent invalidation for every
other changed file in that batch.

With the all-files set, the early return clears cached diagnostics and
updates signatures for every file, matching Strada. Baseline churn is
uniformly that class: after a global-scope change, sibling files are
refreshed (recomputed .d.ts signatures), their cached diagnostics
dropped, and they are queued for full emit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 09:44

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

Aligns incremental/build invalidation with TypeScript by rechecking all non-default-library files after a global-scope shape change.

Changes:

  • Returns the cached all-files affected set for global changes.
  • Adds dependency-update regression and control scenarios.
  • Updates affected incremental, build, and watch baselines.

Reviewed changes

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

Show a summary per file
File Description
internal/execute/incremental/affectedfileshandler.go Returns all affected non-library files.
internal/execute/tsctests/tscbuild_test.go Adds three regression scenarios.
testdata/baselines/reference/tsc/noEmitOnError/when-declarationMap-changes.js Updates invalidation baseline.
testdata/baselines/reference/tsc/noEmit/syntax-errors-with-incremental-as-modules.js Updates incremental syntax-error baseline.
testdata/baselines/reference/tsc/noEmit/semantic-errors-with-incremental-as-modules.js Updates semantic-error baseline.
testdata/baselines/reference/tsc/noEmit/dts-errors-without-dts-enabled-with-incremental-as-modules.js Updates declaration-error baseline.
testdata/baselines/reference/tsc/noEmit/dts-errors-with-incremental-as-modules.js Updates declaration-emit baseline.
testdata/baselines/reference/tsc/incremental/when-global-file-is-added,-the-signatures-are-updated.js Records refreshed global signatures.
testdata/baselines/reference/tsbuildWatch/noEmit/syntax-errors-with-incremental-as-modules.js Updates watch syntax-error baseline.
testdata/baselines/reference/tsbuildWatch/noEmit/semantic-errors-with-incremental-as-modules.js Updates watch semantic-error baseline.
testdata/baselines/reference/tsbuildWatch/noEmit/dts-errors-without-dts-enabled-with-incremental-as-modules.js Updates watch declaration-error baseline.
testdata/baselines/reference/tsbuildWatch/noEmit/dts-errors-with-incremental-as-modules.js Updates watch declaration-emit baseline.
testdata/baselines/reference/tsbuildWatch/noEmit/does-not-go-in-loop-when-watching-when-no-files-are-emitted-with-incremental.js Records additional refreshed file.
testdata/baselines/reference/tsbuild/noEmit/syntax-errors-with-incremental-as-modules.js Updates build syntax-error baseline.
testdata/baselines/reference/tsbuild/noEmit/semantic-errors-with-incremental-as-modules.js Updates build semantic-error baseline.
testdata/baselines/reference/tsbuild/noEmit/dts-errors-without-dts-enabled-with-incremental-as-modules.js Updates build declaration-error baseline.
testdata/baselines/reference/tsbuild/noEmit/dts-errors-with-incremental-as-modules.js Updates build declaration-emit baseline.
testdata/baselines/reference/tsbuild/dependencyUpdate/rebuilds-transitive-dependents-when-dependency-update-batch-includes-a-global-scope-change.js Covers mixed dependency updates.
testdata/baselines/reference/tsbuild/dependencyUpdate/rebuilds-transitive-dependents-when-dependency-update-batch-has-no-global-scope-change.js Adds the control scenario.
testdata/baselines/reference/tsbuild/dependencyUpdate/rebuilds-files-using-globals-when-global-scope-dependency-is-updated.js Covers non-importing global consumers.

@martijnwalraven

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

Comment thread internal/execute/incremental/affectedfileshandler.go Outdated
Review feedback on microsoft#4665: the Strada original
(getFilesAffectedByUpdatedShapeWhenModuleEmit in builderState.ts)
carries no comment at this check, and the ported code reads fine
without it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jakebailey
jakebailey added this pull request to the merge queue Jul 23, 2026
Merged via the queue into microsoft:main with commit d750f85 Jul 23, 2026
21 checks passed
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.

tsgo --build incremental reports no errors after a dependency update batch that includes a global-scope .d.ts change (clean build errors)

3 participants