Skip to content
Draft
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
32 changes: 28 additions & 4 deletions src/compiler/io/compilerMessage.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
6 changes: 5 additions & 1 deletion src/context/common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 49 additions & 0 deletions tests/server/src/cases/issues/Issue12861.hx
Original file line number Diff line number Diff line change
@@ -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<Diagnostic<Dynamic>> = 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<Diagnostic<Dynamic>> = 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<Diagnostic<Dynamic>> = cast res3[0].diagnostics;
Assert.isFalse(diags3.exists(d -> d.kind == MissingFields));
}
}
}
6 changes: 6 additions & 0 deletions tests/server/test/templates/diagnostics/Issue12861.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Issue12861 {
static function main() {
var v:Int = 0;
var _ = v.charAt(0); // Int has no field charAt
}
}
Loading