From e6a6d78758d2d7577675ada3c6d0f358a21df55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sun, 27 Sep 2026 12:18:25 +0200 Subject: [PATCH 1/2] Always track flaky diagnostics in fourslash; add failing 7016 tests Fourslash now initializes the server with trackFlakyDiagnostics set to panic, so every textDocument/diagnostic request also emits the program and fails if the diagnostics differ before and after emit. This complements the compiler harness's pre/post-emit check, which compares separate programs by diagnostic count only and doesn't catch emit adding a diagnostic to an already-checked program. Add fourslash tests where declaration emit adds a "Could not find a declaration file for module" (7016) suggestion for an untyped import whose error was dropped by checking (plain JS file, @ts-expect-error, and an ES module under NodeNext). --- tsc/internal/fourslash/fourslash.go | 3 +++ .../noFlakyDiagnosticsUntypedModule1_test.go | 26 +++++++++++++++++++ .../noFlakyDiagnosticsUntypedModule2_test.go | 25 ++++++++++++++++++ .../noFlakyDiagnosticsUntypedModule3_test.go | 26 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule1_test.go create mode 100644 tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule2_test.go create mode 100644 tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule3_test.go diff --git a/tsc/internal/fourslash/fourslash.go b/tsc/internal/fourslash/fourslash.go index de4ae802e3f84..f44e20f05bc1a 100644 --- a/tsc/internal/fourslash/fourslash.go +++ b/tsc/internal/fourslash/fourslash.go @@ -368,6 +368,9 @@ const showCodeLensLocationsCommandName = "typescript.showCodeLensLocations" func (f *FourslashTest) initialize(t *testing.T, capabilities *lsproto.ClientCapabilities, runExternalCode bool) { initializationOptions := &lsproto.InitializationOptions{ CodeLensShowLocationsCommandName: new(showCodeLensLocationsCommandName), + // Make every textDocument/diagnostic request also emit the program and fail if the + // diagnostics differ before and after emit, e.g. because the emit resolver added some. + TrackFlakyDiagnostics: new(lsproto.DiagnosticFlakeLogLevelPanic), } if runExternalCode { initializationOptions.RunExternalCode = new(true) diff --git a/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule1_test.go b/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule1_test.go new file mode 100644 index 0000000000000..3d92d1a7886fb --- /dev/null +++ b/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule1_test.go @@ -0,0 +1,26 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestNoFlakyDiagnosticsUntypedModule1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @allowJs: true +// @noImplicitAny: true +// @declaration: true +// @Filename: /node_modules/untyped/index.js +// @noOpen: true +exports.x = 1; +// @Filename: /index.js +import { x } from "untyped"; +x;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/index.js") + f.VerifySuggestionDiagnostics(t, nil) +} diff --git a/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule2_test.go b/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule2_test.go new file mode 100644 index 0000000000000..f0fb4d52fe8f5 --- /dev/null +++ b/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule2_test.go @@ -0,0 +1,25 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestNoFlakyDiagnosticsUntypedModule2(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @noImplicitAny: true +// @declaration: true +// @Filename: /untyped.js +exports.x = 1; +// @Filename: /index.ts +// @ts-expect-error +import { x } from "./untyped"; +x;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/index.ts") + f.VerifySuggestionDiagnostics(t, nil) +} diff --git a/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule3_test.go b/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule3_test.go new file mode 100644 index 0000000000000..7c9e3afc55294 --- /dev/null +++ b/tsc/internal/fourslash/tests/noFlakyDiagnosticsUntypedModule3_test.go @@ -0,0 +1,26 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestNoFlakyDiagnosticsUntypedModule3(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @module: nodenext +// @noImplicitAny: true +// @declaration: true +// @Filename: /untyped.mjs +export const x = 1; +// @Filename: /index.mts +// @ts-expect-error +import { x } from "./untyped.mjs"; +x;` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/index.mts") + f.VerifySuggestionDiagnostics(t, nil) +} From be1f21844e68e31791dd885d3e54934c1c7712d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sun, 27 Sep 2026 12:18:25 +0200 Subject: [PATCH 2/2] Don't report module resolution diagnostics from getExternalModuleFileFromDeclaration getExternalModuleFileFromDeclaration is only used by emit (IsImportRequiredByAugmentation, module transforms) and by type-node reuse in the node builder. It resolved with the specifier as the error node, so for an untyped JS module it re-ran errorOnImplicitAnyModule with moduleNotFoundError == nil and recorded 7016 as a *suggestion*. Checking had already reported 7016 as an error, which was then dropped (plain JS file or @ts-expect-error), but suggestions bypass both filters, so the diagnostic appeared only after emit and tripped the LSP's flaky-diagnostic tracking. Resolve with ignoreErrors so these emit-time queries are free of diagnostic side effects. Fixes #64458 --- tsc/internal/checker/checker.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 845f62365bb84..14bec59efdc95 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -15379,7 +15379,10 @@ func (c *Checker) getExternalModuleFileFromDeclaration(declaration *ast.Node) *a if ast.HasImportAttributes(declaration) { importAttributesType = c.getTypeFromImportAttributes(ast.GetImportAttributes(declaration)) } - moduleSymbol := c.resolveExternalModuleNameWorker(specifier, specifier /*moduleNotFoundError*/, nil, false, false, importAttributesType) // TODO: GH#18217 + // This is only used by emit and type printing, after checking has already reported any + // resolution errors for this specifier. Resolve with ignoreErrors so that these queries + // don't add new diagnostics (e.g. an implicit-any-module suggestion) as a side effect. + moduleSymbol := c.resolveExternalModuleNameWorker(specifier, specifier, nil /*moduleNotFoundError*/, true /*ignoreErrors*/, false /*isForAugmentation*/, importAttributesType) if moduleSymbol == nil { return nil }