diff --git a/src/compiler/io/compilerMessage.ml b/src/compiler/io/compilerMessage.ml index 2776cf64395..dd32d80bb1c 100644 --- a/src/compiler/io/compilerMessage.ml +++ b/src/compiler/io/compilerMessage.ml @@ -48,8 +48,23 @@ let add_module_message com (m : module_def) msg p depth message_kind = (e.g. from {!DiagnosticsPrinter.make_missing_fields_message}). During [dms_full_typing], the message is cached in - [m.m_extra.m_cache_bound_objects] unconditionally — the - [RMDiagnostics] filter is checked at replay time, not at store time. + [m.m_extra.m_cache_bound_objects] only when the message position + belongs to the same file as the module itself. This guards against + two related problems: + + 1. Caching a diagnostic in the wrong module — e.g. [displayFields.ml + handle_missing_field_raise] calls this with the target type's module + (e.g. StdTypes for Int) while the message position is in the calling + file. The calling file is always re-typed in diagnostics mode so the + message is regenerated; no caching needed or wanted. + + 2. Shared-DynArray pollution — modules loaded from the HXB binary cache + share their [m_cache_bound_objects] DynArray with the cached entry. + Mutating this DynArray embeds stale diagnostics into the cache that + are replayed on every subsequent compilation even after the original + problem is fixed. + + The [RMDiagnostics] filter is checked at replay time, not at store time. The message is only added to the current message buffer when in diagnostics mode ([RMDiagnostics]), matching the old behavior where @@ -60,8 +75,17 @@ let add_module_message com (m : module_def) msg p depth message_kind = UnresolvedIdentifier) that originate from module typing and should survive across server compilations. *) let add_module_diagnostic com (m : module_def) cm = - if com.display.dms_full_typing then - DynArray.add m.m_extra.m_cache_bound_objects (Message cm); + if com.display.dms_full_typing then begin + (* Only cache messages whose position is in the same file as the + module. A position in a different file means the diagnostic is + about a *usage* of the module (e.g. a field-access in the calling + code), not about the module's own content, so it must not be stored + in the module's cache-bound objects. *) + let msg_fkey = com.part_scope.file_keys#get cm.cm_pos.pfile in + let mod_fkey = Path.UniqueKey.lazy_key m.m_extra.m_file in + if msg_fkey = mod_fkey then + DynArray.add m.m_extra.m_cache_bound_objects (Message cm) + end; if is_diagnostics com then com.part_scope.messages <- cm :: com.part_scope.messages diff --git a/src/context/common.ml b/src/context/common.ml index e2bcf25df5e..2df8abade43 100644 --- a/src/context/common.ml +++ b/src/context/common.ml @@ -398,7 +398,11 @@ let ignore_error com = b let module_warning com m w options msg p = - if com.display.dms_full_typing then begin + (* Only cache messages for freshly-typed modules (m_processed = 0). + Cached modules share their m_cache_bound_objects DynArray with the + binary-cache entry; mutating it would embed stale warnings into the + cache that would then be replayed on every subsequent compilation. *) + if com.display.dms_full_typing && m.m_extra.m_processed = 0 then begin let cm = make_message com.is_macro_context msg p 0 (MKWarning(w, options)) in DynArray.add m.m_extra.m_cache_bound_objects (Message cm) end; diff --git a/tests/server/src/cases/issues/Issue12861.hx b/tests/server/src/cases/issues/Issue12861.hx new file mode 100644 index 00000000000..3be6b368583 --- /dev/null +++ b/tests/server/src/cases/issues/Issue12861.hx @@ -0,0 +1,49 @@ +package cases.issues; + +import haxe.display.Diagnostic; + +// Regression test for https://github.com/HaxeFoundation/haxe/issues/12861 +// Stale diagnostics must be cleared after the underlying problem is fixed. +class Issue12861 extends TestCase { + function test(_) { + // File with a type error: Int has no field charAt + final errContent = getTemplate("diagnostics/Issue12861.hx"); + // Fixed version: use String instead of Int + final okContent = "class Issue12861 { static function main() { var v:String = ''; var _ = v.charAt(0); } }"; + + final args = ["--main", "Issue12861", "--interp", "--no-output"]; + + // --- First diagnostics run: file has the type error --- + vfs.putContent("Issue12861.hx", errContent); + final res1 = runHaxeJson(args, DisplayMethods.Diagnostics, {file: new FsPath("Issue12861.hx")}); + Assert.equals(1, res1.length); + final diags1:Array> = cast res1[0].diagnostics; + // Exactly one MissingFields diagnostic expected (Int has no field charAt) + Assert.equals(1, diags1.length); + Assert.isTrue(diags1.exists(d -> d.kind == MissingFields)); + + // --- Second diagnostics run on the SAME erroneous file (after invalidation) --- + // Without the fix, add_module_diagnostic polluted Int's cached + // m_cache_bound_objects on the first run. The second run would then + // replay that stale message AND add another one, producing 2 entries. + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Issue12861.hx")}); + final res2 = runHaxeJson(args, DisplayMethods.Diagnostics, {file: new FsPath("Issue12861.hx")}); + Assert.equals(1, res2.length); + final diags2:Array> = cast res2[0].diagnostics; + // Must still be exactly 1, not 2 or more (no stacking) + Assert.equals(1, diags2.length); + Assert.isTrue(diags2.exists(d -> d.kind == MissingFields)); + + // --- Third diagnostics run: fix the file --- + // The old error must not persist; the result must be clean. + vfs.putContent("Issue12861.hx", okContent); + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Issue12861.hx")}); + final res3 = runHaxeJson(args, DisplayMethods.Diagnostics, {file: new FsPath("Issue12861.hx")}); + // Without the fix, the stale DKMissingFields from Int's cache was + // replayed here, making res3 still show an error. + if (res3.length > 0) { + final diags3:Array> = cast res3[0].diagnostics; + Assert.isFalse(diags3.exists(d -> d.kind == MissingFields)); + } + } +} diff --git a/tests/server/test/templates/diagnostics/Issue12861.hx b/tests/server/test/templates/diagnostics/Issue12861.hx new file mode 100644 index 00000000000..2878aa05994 --- /dev/null +++ b/tests/server/test/templates/diagnostics/Issue12861.hx @@ -0,0 +1,6 @@ +class Issue12861 { + static function main() { + var v:Int = 0; + var _ = v.charAt(0); // Int has no field charAt + } +}