Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ func canReplaceFileInProgram(file1 *ast.SourceFile, file2 *ast.SourceFile) bool
return file2 != nil &&
file1.ParseOptions() == file2.ParseOptions() &&
file1.UsesUriStyleNodeCoreModules == file2.UsesUriStyleNodeCoreModules &&
(file1.ExternalModuleIndicator != nil) == (file2.ExternalModuleIndicator != nil) &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't we need to check that they're equal in some way?

Copy link
Copy Markdown
Member

@DanielRosenwasser DanielRosenwasser May 20, 2026

Choose a reason for hiding this comment

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

I don't think so - my understanding is that these are used to determine if the program is sufficiently structurally the same to avoid rebuilding a full program - not that the files themselves are "equal" before/after. Below, we check that the rest of the structure is equal (e.g. same imports, global modifications, etc.)

From program.go's perspective, the external module indicator probably should not be used to determine anything about the file other than "is this a module, or is this a global?"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually, I'm not sure I understand why this fix works at all. These are actually a pure function of the file contents + external module indicator options (which is just the file path+reads), which are part of the cache key for files, so when would these change and us not invalidate, or, why would reusing them ever be wrong

Copy link
Copy Markdown
Member

@DanielRosenwasser DanielRosenwasser May 20, 2026

Choose a reason for hiding this comment

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

I'm not sure which portion you're thinking of, but basically here's what's happening.

First off, every Program includes processedFiles which contains a map of filePath -> synthesizedStringLiteralsToBeUsedAsImportSpecifiers. This seems to me to be calculated at parse-time, but this state is located on the program, not on the file. So as far as I understand, the parse cache wouldn't really come into play here.

Now when a file change comes in, the idea is we try to reuse a lot of the program state for the new Program. UpdateProgram does this via canReplaceFileInProgram. The checks here ensure that we cannot reuse program state related to locating tslib for a script -> module transition.

(file1.CommonJSModuleIndicator != nil) == (file2.CommonJSModuleIndicator != nil) &&
Comment on lines +346 to +347
slices.EqualFunc(file1.Imports(), file2.Imports(), equalModuleSpecifiers) &&
slices.EqualFunc(file1.ModuleAugmentations, file2.ModuleAugmentations, equalModuleAugmentationNames) &&
slices.Equal(file1.AmbientModuleNames, file2.AmbientModuleNames) &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestImportHelpersAfterScriptBecomesDecoratedModule(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")

const content = `// @Filename: /tsconfig.json
{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"experimentalDecorators": true,
"importHelpers": true
},
"files": ["foo.ts"]
}

// @Filename: /foo.ts
declare function dec(value: Function): void;
/*insert*/class C {}

// @Filename: /node_modules/tslib/package.json
{ "name": "tslib", "typings": "tslib.d.ts" }

// @Filename: /node_modules/tslib/tslib.d.ts
export declare function __decorate(...args: any[]): any;

// @Filename: /node_modules/tslib/tslib.js
exports.__decorate = function () {};
`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()

f.GoToFile(t, "/foo.ts")
f.VerifyNumberOfErrorsInCurrentFile(t, 0)
f.Replace(t, f.MarkerByName(t, "insert").Position, 0, `@dec
export `)
// The second diagnostics request forces external helper resolution after the edit.
f.VerifyNumberOfErrorsInCurrentFile(t, 0)
}
Loading