From 640f266dc97a893025fae2ac0fe6429dec2e06c4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 09:43:00 +0200 Subject: [PATCH 01/14] Keep Lua class fields aligned with Jass lifetime semantics --- .../lua/translation/LuaTranslator.java | 11 +- .../validation/WurstValidator.java | 159 +++++++++++++++++- .../tests/wurstscript/tests/ClassesTests.java | 41 +++++ .../tests/LuaBackendAuditTests.java | 2 +- 4 files changed, 201 insertions(+), 12 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index a178d416c..59f149520 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -207,8 +207,9 @@ public LuaVariable initFor(ImClass a) { * per canonical IM field, indexed by that id; class descriptors remain static tables and are * reached through {@link #objectClass}. Allocation therefore creates no per-instance table. * - *

Destroy clears every field slot before putting the id on the free stack. As in the Jass - * backend, a stale reference aliases a later object after that id is recycled; before reuse its + *

Destroy only removes the live-object descriptor before putting the id on the free stack. + * Field storage intentionally retains its value, matching the Jass backend's array-backed + * fields. A stale reference aliases a later object after that id is recycled; before reuse its * descriptor is absent, so virtual dispatch fails and {@code instanceof} is false. Capturing * closures use the same representation and, like Jass closures, retain their id until destroyed. */ @@ -1031,12 +1032,6 @@ private void translateClass(ImClass c) { LuaFunction cleanup = luaClassCleanup.getFor(c); LuaVariable object = LuaAst.LuaVariable("object", LuaAst.LuaNoExpr()); cleanup.getParams().add(object); - for (ImVar field : collectFieldsForAllocation(c)) { - cleanup.getBody().add(LuaAst.LuaAssignment( - LuaAst.LuaExprArrayAccess(LuaAst.LuaExprVarAccess(fieldStorage(field)), - LuaAst.LuaExprlist(LuaAst.LuaExprVarAccess(object))), - LuaAst.LuaExprNull())); - } luaModel.add(cleanup); deferMainInit(LuaAst.LuaAssignment( LuaAst.LuaExprFieldAccess(LuaAst.LuaExprVarAccess(classVar), "__wurst_dealloc"), diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 7e93250a4..8e25d4dd5 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -60,6 +60,7 @@ private enum Phase { LIGHT, HEAVY } private final HashSet trveWrapperFuncs = new HashSet<>(); private final HashMap> wrapperCalls = new HashMap<>(); private final Map> classVarInitOrderCache = new HashMap<>(); + private final Map guaranteedClassFieldInitCache = new IdentityHashMap<>(); /** * When true, the build targets a legacy patch (pre-1.24) whose Blizzard-provided @@ -83,6 +84,7 @@ public void validate(Collection toCheck) { visitedFunctions = 0; heavyFunctions.clear(); heavyBlocks.clear(); + guaranteedClassFieldInitCache.clear(); lightValidation(toCheck); @@ -1747,9 +1749,156 @@ private void checkUninitializedVars(FunctionLike f) { && !f.getSource().getFile().endsWith("war3map.j")) { new DataflowAnomalyAnalysis(Utils.isJassCode(f)).execute(f); } + checkPotentiallyUninitializedClassFields(f); checkJassImplicitNullLocalsReadWithoutExplicitWrite(f); } + /** + * Instance fields without an initializer are reset to the language default when an object is + * allocated, but that value is often accidental. Warn when a method reads such a field and no + * constructor is known to assign it on every construction path. This deliberately stays a + * cheap, local check: it does not attempt interprocedural or path-sensitive reasoning. + */ + private void checkPotentiallyUninitializedClassFields(FunctionLike function) { + ClassDef owner = function.attrNearestClassDef(); + if (owner == null || function instanceof OnDestroyDef) { + return; + } + + Set writtenFields = Collections.newSetFromMap(new IdentityHashMap<>()); + Set warned = Collections.newSetFromMap(new IdentityHashMap<>()); + function.accept(new Element.DefaultVisitor() { + private void checkField(NameRef access) { + NameDef nameDef = access.attrNameDef(); + if (!(nameDef instanceof GlobalVarDef field) || !field.attrIsDynamicClassMember()) { + return; + } + if (isWriteTarget(access)) { + writtenFields.add(field); + return; + } + if (!(field.getInitialExpr() instanceof NoExpr) + || writtenFields.contains(field) + || hasGuaranteedConstructorAssignment(field) + || !warned.add(field)) { + return; + } + access.addWarning("Field '" + field.getName() + + "' has no explicit initializer and is not definitely assigned by every constructor;" + + " this access may observe its default value." + + " Initialize it explicitly in every construction path."); + } + + @Override + public void visit(ExprVarAccess access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberVarDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberVarDotDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberVarQuestionDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberArrayVarDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberArrayVarDotDot access) { + super.visit(access); + checkField(access); + } + }); + } + + private Set collectWrittenDynamicFields(Element root) { + Set result = Collections.newSetFromMap(new IdentityHashMap<>()); + root.accept(new Element.DefaultVisitor() { + private void collect(NameRef access) { + if (!isWriteTarget(access)) { + return; + } + NameDef nameDef = access.attrNameDef(); + if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember()) { + result.add(field); + } + } + + @Override + public void visit(ExprVarAccess access) { + super.visit(access); + collect(access); + } + + @Override + public void visit(ExprMemberVarDot access) { + super.visit(access); + collect(access); + } + + @Override + public void visit(ExprMemberVarDotDot access) { + super.visit(access); + collect(access); + } + + @Override + public void visit(ExprMemberVarQuestionDot access) { + super.visit(access); + collect(access); + } + + @Override + public void visit(ExprMemberArrayVarDot access) { + super.visit(access); + collect(access); + } + + @Override + public void visit(ExprMemberArrayVarDotDot access) { + super.visit(access); + collect(access); + } + }); + return result; + } + + private boolean hasGuaranteedConstructorAssignment(GlobalVarDef field) { + Boolean cached = guaranteedClassFieldInitCache.get(field); + if (cached != null) { + return cached; + } + ClassDef declaringClass = field.attrNearestClassDef(); + if (declaringClass == null || declaringClass.getConstructors().isEmpty()) { + guaranteedClassFieldInitCache.put(field, false); + return false; + } + for (ConstructorDef constructor : declaringClass.getConstructors()) { + if (!collectWrittenDynamicFields(constructor).contains(field)) { + guaranteedClassFieldInitCache.put(field, false); + return false; + } + } + guaranteedClassFieldInitCache.put(field, true); + return true; + } + /** * JASS compatibility shim: we currently synthesize "= null" for uninitialized non-primitive * locals to avoid invalid emitted JASS. Still report likely user bugs early when such a local @@ -1836,11 +1985,15 @@ public void visit(ExprVarAccess varAccess) { } private boolean isWriteTarget(ExprVarAccess varAccess) { - if (!(varAccess.getParent() instanceof StmtSet)) { + return isWriteTarget((Element) varAccess); + } + + private boolean isWriteTarget(Element access) { + if (!(access.getParent() instanceof StmtSet)) { return false; } - StmtSet set = (StmtSet) varAccess.getParent(); - return set.getUpdatedExpr() == varAccess; + StmtSet set = (StmtSet) access.getParent(); + return set.getUpdatedExpr() == access; } private @Nullable StmtSet nearestEnclosingStmtSet(Element e) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index b61b4c7f9..72ba64bc5 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -17,6 +17,47 @@ public class ClassesTests extends WurstScriptTest { private static final String TEST_DIR = "./testscripts/valid/classes/"; private static final String TEST_DIR2 = "./testscripts/concept/"; + @Test + public void warnsWhenClassFieldHasNoInitializerOrConstructorAssignment() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int value", + " function get() returns int", + " return value", + " function readBeforeSet() returns int", + " int oldValue = value", + " value = 42", + " return oldValue" + ); + } + + @Test + public void doesNotWarnWhenEveryConstructorAssignsClassField() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Counter", + " int value", + " construct(int value)", + " this.value = value", + " function get() returns int", + " return value", + " function setAndGet() returns int", + " value = 42", + " return value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned"))); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java index ae565f849..2044833a1 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LuaBackendAuditTests.java @@ -132,7 +132,7 @@ public void classInstancesUseRecycledIntegerIdsAndStaticFieldStorage() throws IO compiled.contains("__wurst_classToIndex(first)")); assertFalse("class casts must not allocate boxed-number identity wrappers", compiled.contains("firstId = __wurst_objectToIndex(first)")); - assertTrue("deallocation must clear reference-bearing field slots before recycling", + assertFalse("deallocation must preserve field values just like Jass storage", compiled.contains("Base_reference_storage[object] = nil")); } From 43f024d9377c8e5260d286888316eddaadf7588a Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 09:56:56 +0200 Subject: [PATCH 02/14] Track class field initialization by receiver --- .../wurstscript/validation/WurstValidator.java | 8 ++++++-- .../tests/wurstscript/tests/ClassesTests.java | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 8e25d4dd5..289d0eb5b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1773,7 +1773,7 @@ private void checkField(NameRef access) { if (!(nameDef instanceof GlobalVarDef field) || !field.attrIsDynamicClassMember()) { return; } - if (isWriteTarget(access)) { + if (isWriteTarget(access) && isCurrentInstanceAccess(access)) { writtenFields.add(field); return; } @@ -1831,7 +1831,7 @@ private Set collectWrittenDynamicFields(Element root) { Set result = Collections.newSetFromMap(new IdentityHashMap<>()); root.accept(new Element.DefaultVisitor() { private void collect(NameRef access) { - if (!isWriteTarget(access)) { + if (!isWriteTarget(access) || !isCurrentInstanceAccess(access)) { return; } NameDef nameDef = access.attrNameDef(); @@ -1899,6 +1899,10 @@ private boolean hasGuaranteedConstructorAssignment(GlobalVarDef field) { return true; } + private boolean isCurrentInstanceAccess(NameRef access) { + return access.attrImplicitParameter() instanceof ExprThis; + } + /** * JASS compatibility shim: we currently synthesize "= null" for uninitialized non-primitive * locals to avoid invalid emitted JASS. Still report likely user bugs early when such a local diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 72ba64bc5..b195b055a 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -58,6 +58,23 @@ public void doesNotWarnWhenEveryConstructorAssignsClassField() { .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned"))); } + @Test + public void warnsWhenConstructorOnlyAssignsOtherInstanceField() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int value", + " construct(Counter other)", + " other.value = 1", + " function get() returns int", + " return value" + ); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From d3db427991fbebe3d2f0049b0d725d6835d34712 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 10:05:55 +0200 Subject: [PATCH 03/14] Complete class field initialization warning coverage --- .../validation/WurstValidator.java | 20 ++++++++-- .../tests/wurstscript/tests/ClassesTests.java | 39 ++++++++++++++++++- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 289d0eb5b..364675b5b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1773,13 +1773,15 @@ private void checkField(NameRef access) { if (!(nameDef instanceof GlobalVarDef field) || !field.attrIsDynamicClassMember()) { return; } - if (isWriteTarget(access) && isCurrentInstanceAccess(access)) { - writtenFields.add(field); + if (isWriteTarget(access)) { + if (isCurrentInstanceAccess(access)) { + writtenFields.add(field); + } return; } if (!(field.getInitialExpr() instanceof NoExpr) || writtenFields.contains(field) - || hasGuaranteedConstructorAssignment(field) + || (!(function instanceof ConstructorDef) && hasGuaranteedConstructorAssignment(field)) || !warned.add(field)) { return; } @@ -1795,6 +1797,12 @@ public void visit(ExprVarAccess access) { checkField(access); } + @Override + public void visit(ExprVarArrayAccess access) { + super.visit(access); + checkField(access); + } + @Override public void visit(ExprMemberVarDot access) { super.visit(access); @@ -1846,6 +1854,12 @@ public void visit(ExprVarAccess access) { collect(access); } + @Override + public void visit(ExprVarArrayAccess access) { + super.visit(access); + collect(access); + } + @Override public void visit(ExprMemberVarDot access) { super.visit(access); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index b195b055a..7f48a4477 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -60,10 +61,9 @@ public void doesNotWarnWhenEveryConstructorAssignsClassField() { @Test public void warnsWhenConstructorOnlyAssignsOtherInstanceField() { - test() + CompilationResult result = test() .setStopOnFirstError(false) .executeProg(false) - .expectWarning("no explicit initializer and is not definitely assigned") .lines( "package Test", "class Counter", @@ -73,6 +73,41 @@ public void warnsWhenConstructorOnlyAssignsOtherInstanceField() { " function get() returns int", " return value" ); + + assertEquals(result.getGui().getWarningList().stream() + .filter(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")) + .count(), 1); + } + + @Test + public void warnsWhenConstructorReadsFieldBeforeAssigningIt() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int value", + " construct()", + " int oldValue = value", + " value = 1" + ); + } + + @Test + public void warnsWhenUnqualifiedArrayFieldHasNoInitializer() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int values[2]", + " function get() returns int", + " return values[0]" + ); } @Test From b916cb3d331c0b313d4e3f3dca631aaa0b0660b7 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 10:15:41 +0200 Subject: [PATCH 04/14] Make field-read validation assignment-order aware --- .../validation/WurstValidator.java | 19 ++++++++--- .../tests/wurstscript/tests/ClassesTests.java | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 364675b5b..5236a6dde 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1774,13 +1774,10 @@ private void checkField(NameRef access) { return; } if (isWriteTarget(access)) { - if (isCurrentInstanceAccess(access)) { - writtenFields.add(field); - } return; } if (!(field.getInitialExpr() instanceof NoExpr) - || writtenFields.contains(field) + || (isCurrentInstanceAccess(access) && writtenFields.contains(field)) || (!(function instanceof ConstructorDef) && hasGuaranteedConstructorAssignment(field)) || !warned.add(field)) { return; @@ -1832,6 +1829,20 @@ public void visit(ExprMemberArrayVarDotDot access) { super.visit(access); checkField(access); } + + @Override + public void visit(StmtSet assignment) { + super.visit(assignment); + if (!(assignment.getUpdatedExpr() instanceof NameRef access) + || !isCurrentInstanceAccess(access) + || !isWriteTarget(access)) { + return; + } + NameDef nameDef = access.attrNameDef(); + if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember()) { + writtenFields.add(field); + } + } }); } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 7f48a4477..19c527df3 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -110,6 +110,38 @@ public void warnsWhenUnqualifiedArrayFieldHasNoInitializer() { ); } + @Test + public void warnsWhenFieldIsReadOnRightHandSideBeforeAssignment() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int value", + " function increment() returns int", + " value = value + 1", + " return value" + ); + } + + @Test + public void warnsWhenOtherInstanceFieldIsReadAfterThisFieldWrite() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int value", + " function getOther(Counter other) returns int", + " value = 1", + " return other.value" + ); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From e610a9ecb107023eb89726d80708faea0999bf5f Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 10:30:45 +0200 Subject: [PATCH 05/14] Handle array and chained constructor initialization --- .../validation/WurstValidator.java | 42 +++++++++++++++++-- .../tests/wurstscript/tests/ClassesTests.java | 37 ++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 5236a6dde..89593fee5 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1839,7 +1839,8 @@ public void visit(StmtSet assignment) { return; } NameDef nameDef = access.attrNameDef(); - if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember()) { + if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember() + && isWholeFieldAccess(access)) { writtenFields.add(field); } } @@ -1850,11 +1851,14 @@ private Set collectWrittenDynamicFields(Element root) { Set result = Collections.newSetFromMap(new IdentityHashMap<>()); root.accept(new Element.DefaultVisitor() { private void collect(NameRef access) { - if (!isWriteTarget(access) || !isCurrentInstanceAccess(access)) { + if (access.attrNearestExprClosure() != null + || !isWriteTarget(access) + || !isCurrentInstanceAccess(access)) { return; } NameDef nameDef = access.attrNameDef(); - if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember()) { + if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember() + && isWholeFieldAccess(access)) { result.add(field); } } @@ -1871,6 +1875,12 @@ public void visit(ExprVarArrayAccess access) { collect(access); } + @Override + public void visit(ExprClosure closure) { + // A closure runs later (and may never run), so writes in its body do not + // initialize the object during construction. + } + @Override public void visit(ExprMemberVarDot access) { super.visit(access); @@ -1915,7 +1925,7 @@ private boolean hasGuaranteedConstructorAssignment(GlobalVarDef field) { return false; } for (ConstructorDef constructor : declaringClass.getConstructors()) { - if (!collectWrittenDynamicFields(constructor).contains(field)) { + if (!constructorAssignsField(constructor, field, Collections.newSetFromMap(new IdentityHashMap<>()))) { guaranteedClassFieldInitCache.put(field, false); return false; } @@ -1928,6 +1938,30 @@ private boolean isCurrentInstanceAccess(NameRef access) { return access.attrImplicitParameter() instanceof ExprThis; } + private boolean isWholeFieldAccess(NameRef access) { + return !(access instanceof AstElementWithIndexes); + } + + private boolean constructorAssignsField(ConstructorDef constructor, GlobalVarDef field, + Set visiting) { + if (!visiting.add(constructor)) { + return false; + } + if (collectWrittenDynamicFields(constructor).contains(field)) { + return true; + } + FunctionCall thisCall = getFirstThisConstructorCall(constructor); + if (thisCall == null) { + return false; + } + ClassOrModule owner = constructor.attrNearestClassOrModule(); + if (owner == null) { + return false; + } + ConstructorDef target = OverloadingResolver.resolveThisCall(owner.getConstructors(), thisCall); + return target != null && target != constructor && constructorAssignsField(target, field, visiting); + } + /** * JASS compatibility shim: we currently synthesize "= null" for uninitialized non-primitive * locals to avoid invalid emitted JASS. Still report likely user bugs early when such a local diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 19c527df3..9090c2dfc 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -142,6 +142,43 @@ public void warnsWhenOtherInstanceFieldIsReadAfterThisFieldWrite() { ); } + @Test + public void warnsWhenOnlyOneArrayElementWasAssigned() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int values[2]", + " function getOther() returns int", + " values[0] = 1", + " return values[1]" + ); + } + + @Test + public void doesNotWarnWhenDelegatingConstructorAssignsClassField() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Counter", + " int value", + " construct(int value)", + " this.value = value", + " construct()", + " this(1)", + " function get() returns int", + " return value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned"))); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From 7ec8dc3cbfa6e15e02bb79d56ecee92cdfc3a7c4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 10:34:21 +0200 Subject: [PATCH 06/14] Keep closure writes out of field read flow --- .../de/peeeq/wurstscript/validation/WurstValidator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 89593fee5..27b7c538f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1777,7 +1777,8 @@ private void checkField(NameRef access) { return; } if (!(field.getInitialExpr() instanceof NoExpr) - || (isCurrentInstanceAccess(access) && writtenFields.contains(field)) + || (!isInNestedClosure(access) && isCurrentInstanceAccess(access) + && writtenFields.contains(field)) || (!(function instanceof ConstructorDef) && hasGuaranteedConstructorAssignment(field)) || !warned.add(field)) { return; @@ -1834,6 +1835,7 @@ public void visit(ExprMemberArrayVarDotDot access) { public void visit(StmtSet assignment) { super.visit(assignment); if (!(assignment.getUpdatedExpr() instanceof NameRef access) + || isInNestedClosure(access) || !isCurrentInstanceAccess(access) || !isWriteTarget(access)) { return; @@ -1938,6 +1940,10 @@ private boolean isCurrentInstanceAccess(NameRef access) { return access.attrImplicitParameter() instanceof ExprThis; } + private boolean isInNestedClosure(NameRef access) { + return access.attrNearestExprClosure() != null; + } + private boolean isWholeFieldAccess(NameRef access) { return !(access instanceof AstElementWithIndexes); } From 601e4e84a37527f2c6dfcd3a9de9693f38bcf60d Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 10:45:22 +0200 Subject: [PATCH 07/14] Validate field initializer and delegated reads --- .../validation/WurstValidator.java | 73 ++++++++++++++++++- .../tests/wurstscript/tests/ClassesTests.java | 37 ++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 27b7c538f..51b985ced 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1767,6 +1767,8 @@ private void checkPotentiallyUninitializedClassFields(FunctionLike function) { Set writtenFields = Collections.newSetFromMap(new IdentityHashMap<>()); Set warned = Collections.newSetFromMap(new IdentityHashMap<>()); + FunctionCall delegatedConstructorCall = function instanceof ConstructorDef + ? getFirstThisConstructorCall((ConstructorDef) function) : null; function.accept(new Element.DefaultVisitor() { private void checkField(NameRef access) { NameDef nameDef = access.attrNameDef(); @@ -1780,6 +1782,8 @@ private void checkField(NameRef access) { || (!isInNestedClosure(access) && isCurrentInstanceAccess(access) && writtenFields.contains(field)) || (!(function instanceof ConstructorDef) && hasGuaranteedConstructorAssignment(field)) + || (delegatedConstructorCall != null && !access.isSubtreeOf(delegatedConstructorCall) + && hasGuaranteedConstructorAssignment(field)) || !warned.add(field)) { return; } @@ -1846,6 +1850,7 @@ && isWholeFieldAccess(access)) { writtenFields.add(field); } } + }); } @@ -1968,6 +1973,70 @@ private boolean constructorAssignsField(ConstructorDef constructor, GlobalVarDef return target != null && target != constructor && constructorAssignsField(target, field, visiting); } + private void checkClassFieldInitializerReads(GlobalVarDef field) { + if (!field.attrIsDynamicClassMember() || !(field.getInitialExpr() instanceof Expr initializer)) { + return; + } + Set warned = Collections.newSetFromMap(new IdentityHashMap<>()); + initializer.accept(new Element.DefaultVisitor() { + private void checkField(NameRef access) { + NameDef nameDef = access.attrNameDef(); + if (!(nameDef instanceof GlobalVarDef referenced) + || !referenced.attrIsDynamicClassMember() + || !(referenced.getInitialExpr() instanceof NoExpr) + || !warned.add(referenced)) { + return; + } + access.addWarning("Field '" + referenced.getName() + + "' is read from a field initializer without an explicit initializer;" + + " this access may observe its default value." + + " Initialize it explicitly before using it."); + } + + @Override + public void visit(ExprVarAccess access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprVarArrayAccess access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberVarDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberVarDotDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberVarQuestionDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberArrayVarDot access) { + super.visit(access); + checkField(access); + } + + @Override + public void visit(ExprMemberArrayVarDotDot access) { + super.visit(access); + checkField(access); + } + }); + } + /** * JASS compatibility shim: we currently synthesize "= null" for uninitialized non-primitive * locals to avoid invalid emitted JASS. Still report likely user bugs early when such a local @@ -3796,7 +3865,9 @@ private void checkVarDef(VarDef v) { } if (v instanceof GlobalVarDef) { - checkClassMemberInitializerOrder((GlobalVarDef) v); + GlobalVarDef field = (GlobalVarDef) v; + checkClassMemberInitializerOrder(field); + checkClassFieldInitializerReads(field); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 9090c2dfc..3d859a8ef 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -179,6 +179,43 @@ public void doesNotWarnWhenDelegatingConstructorAssignsClassField() { .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned"))); } + @Test + public void doesNotWarnAfterDelegatingConstructorBeforeLaterRead() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Counter", + " int value", + " construct(int value)", + " this.value = value", + " construct()", + " this(1)", + " int observed = value", + " value = observed", + " function get() returns int", + " return value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer"))); + } + + @Test + public void warnsWhenFieldInitializerReadsUninitializedField() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("read from a field initializer without an explicit initializer") + .lines( + "package Test", + "class Counter", + " int value", + " int copy = value" + ); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From 70508db28c1f2fe41428a1b68f347a83bcafc941 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 11:08:37 +0200 Subject: [PATCH 08/14] Handle module field initialization guarantees --- .../validation/WurstValidator.java | 47 +++++++++++++++---- .../tests/wurstscript/tests/ClassesTests.java | 41 ++++++++++++++++ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 51b985ced..ae0a2bcc3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1760,7 +1760,7 @@ private void checkUninitializedVars(FunctionLike f) { * cheap, local check: it does not attempt interprocedural or path-sensitive reasoning. */ private void checkPotentiallyUninitializedClassFields(FunctionLike function) { - ClassDef owner = function.attrNearestClassDef(); + ClassOrModule owner = function.attrNearestClassOrModule(); if (owner == null || function instanceof OnDestroyDef) { return; } @@ -1926,12 +1926,12 @@ private boolean hasGuaranteedConstructorAssignment(GlobalVarDef field) { if (cached != null) { return cached; } - ClassDef declaringClass = field.attrNearestClassDef(); - if (declaringClass == null || declaringClass.getConstructors().isEmpty()) { + List constructors = constructorsFor(field); + if (constructors.isEmpty()) { guaranteedClassFieldInitCache.put(field, false); return false; } - for (ConstructorDef constructor : declaringClass.getConstructors()) { + for (ConstructorDef constructor : constructors) { if (!constructorAssignsField(constructor, field, Collections.newSetFromMap(new IdentityHashMap<>()))) { guaranteedClassFieldInitCache.put(field, false); return false; @@ -1965,14 +1965,38 @@ private boolean constructorAssignsField(ConstructorDef constructor, GlobalVarDef if (thisCall == null) { return false; } - ClassOrModule owner = constructor.attrNearestClassOrModule(); - if (owner == null) { - return false; - } - ConstructorDef target = OverloadingResolver.resolveThisCall(owner.getConstructors(), thisCall); + ConstructorDef target = OverloadingResolver.resolveThisCall(constructorsFor(constructor), thisCall); return target != null && target != constructor && constructorAssignsField(target, field, visiting); } + private List constructorsFor(GlobalVarDef field) { + Element current = field; + while (current != null) { + if (current instanceof ModuleInstanciation module) { + return module.getConstructors(); + } + if (current instanceof ClassOrModule owner) { + return owner.getConstructors(); + } + current = current.getParent(); + } + return Collections.emptyList(); + } + + private List constructorsFor(ConstructorDef constructor) { + Element current = constructor; + while (current != null) { + if (current instanceof ModuleInstanciation module) { + return module.getConstructors(); + } + if (current instanceof ClassOrModule owner) { + return owner.getConstructors(); + } + current = current.getParent(); + } + return Collections.emptyList(); + } + private void checkClassFieldInitializerReads(GlobalVarDef field) { if (!field.attrIsDynamicClassMember() || !(field.getInitialExpr() instanceof Expr initializer)) { return; @@ -1993,6 +2017,11 @@ private void checkField(NameRef access) { + " Initialize it explicitly before using it."); } + @Override + public void visit(ExprClosure closure) { + // A closure runs later (and may never run), so its body is not field initialization. + } + @Override public void visit(ExprVarAccess access) { super.visit(access); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 3d859a8ef..8776b6148 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -216,6 +216,47 @@ public void warnsWhenFieldInitializerReadsUninitializedField() { ); } + @Test + public void doesNotWarnWhenModuleConstructorAssignsClassField() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "module Values", + " int value", + " construct()", + " value = 1", + " function get() returns int", + " return value", + "class Counter", + " use Values" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer")), + result.getGui().getWarningList().toString()); + } + + @Test + public void doesNotTreatDeferredClosureInitializerAsImmediateFieldRead() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "interface Reader", + " function read() returns int", + "class Counter", + " int value", + " Reader reader = () -> value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("read from a field initializer")), + result.getGui().getWarningList().toString()); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From e1d95d9a4a484a77b509eec5178df894eb46390d Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 11:21:45 +0200 Subject: [PATCH 09/14] Avoid false field initializer warnings for initialized receivers --- .../validation/WurstValidator.java | 1 + .../tests/wurstscript/tests/ClassesTests.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index ae0a2bcc3..0a3e36814 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -2008,6 +2008,7 @@ private void checkField(NameRef access) { if (!(nameDef instanceof GlobalVarDef referenced) || !referenced.attrIsDynamicClassMember() || !(referenced.getInitialExpr() instanceof NoExpr) + || (!isCurrentInstanceAccess(access) && hasGuaranteedConstructorAssignment(referenced)) || !warned.add(referenced)) { return; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 8776b6148..651bfab12 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -257,6 +257,27 @@ public void doesNotTreatDeferredClosureInitializerAsImmediateFieldRead() { result.getGui().getWarningList().toString()); } + @Test + public void doesNotWarnForExplicitReceiverWithGuaranteedConstructorAssignment() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Source", + " int value", + " construct()", + " value = 1", + "class Holder", + " Source source = new Source()", + " int copy = source.value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("read from a field initializer")), + result.getGui().getWarningList().toString()); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From 66af3cac48645413a29c6cf25f9a4311497b699f Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 11:35:18 +0200 Subject: [PATCH 10/14] Complete receiver-aware field read diagnostics --- .../validation/WurstValidator.java | 5 +-- .../tests/wurstscript/tests/ClassesTests.java | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 0a3e36814..c17c6f13f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1760,8 +1760,7 @@ private void checkUninitializedVars(FunctionLike f) { * cheap, local check: it does not attempt interprocedural or path-sensitive reasoning. */ private void checkPotentiallyUninitializedClassFields(FunctionLike function) { - ClassOrModule owner = function.attrNearestClassOrModule(); - if (owner == null || function instanceof OnDestroyDef) { + if (function instanceof OnDestroyDef) { return; } @@ -1779,7 +1778,7 @@ private void checkField(NameRef access) { return; } if (!(field.getInitialExpr() instanceof NoExpr) - || (!isInNestedClosure(access) && isCurrentInstanceAccess(access) + || (isCurrentInstanceAccess(access) && writtenFields.contains(field)) || (!(function instanceof ConstructorDef) && hasGuaranteedConstructorAssignment(field)) || (delegatedConstructorCall != null && !access.isSubtreeOf(delegatedConstructorCall) diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 651bfab12..d421f58d6 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -278,6 +278,43 @@ public void doesNotWarnForExplicitReceiverWithGuaranteedConstructorAssignment() result.getGui().getWarningList().toString()); } + @Test + public void warnsForUninitializedFieldReadFromPackageFunction() { + test() + .setStopOnFirstError(false) + .executeProg(false) + .expectWarning("no explicit initializer and is not definitely assigned") + .lines( + "package Test", + "class Counter", + " int value", + "function read(Counter counter) returns int", + " return counter.value" + ); + } + + @Test + public void doesNotWarnForClosureReadAfterPriorAssignment() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "interface Reader", + " function read() returns int", + "function consume(Reader reader)", + "class Counter", + " int value", + " construct()", + " value = 1", + " consume(() -> value)" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From 1230debc571ecc5133480364d4df636f5ce6bf28 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 11:47:20 +0200 Subject: [PATCH 11/14] Respect initialized receivers in constructors --- .../validation/WurstValidator.java | 3 ++- .../tests/wurstscript/tests/ClassesTests.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index c17c6f13f..79ae9fd6f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1780,7 +1780,8 @@ private void checkField(NameRef access) { if (!(field.getInitialExpr() instanceof NoExpr) || (isCurrentInstanceAccess(access) && writtenFields.contains(field)) - || (!(function instanceof ConstructorDef) && hasGuaranteedConstructorAssignment(field)) + || ((!isCurrentInstanceAccess(access) || !(function instanceof ConstructorDef)) + && hasGuaranteedConstructorAssignment(field)) || (delegatedConstructorCall != null && !access.isSubtreeOf(delegatedConstructorCall) && hasGuaranteedConstructorAssignment(field)) || !warned.add(field)) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index d421f58d6..4640d7b6c 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -278,6 +278,27 @@ public void doesNotWarnForExplicitReceiverWithGuaranteedConstructorAssignment() result.getGui().getWarningList().toString()); } + @Test + public void doesNotWarnForInitializedExplicitReceiverInsideConstructor() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Source", + " int value", + " construct()", + " value = 1", + "class Holder", + " construct(Source source)", + " int copy = source.value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); + } + @Test public void warnsForUninitializedFieldReadFromPackageFunction() { test() From 84a8570dcaaa3f023b54a3367f65138c03ce0473 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 12:04:11 +0200 Subject: [PATCH 12/14] Complete constructor and closure field flow --- .../validation/WurstValidator.java | 109 ++++++++++++++++-- .../tests/wurstscript/tests/ClassesTests.java | 86 ++++++++++++++ 2 files changed, 188 insertions(+), 7 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 79ae9fd6f..421bde65e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -61,6 +61,7 @@ private enum Phase { LIGHT, HEAVY } private final HashMap> wrapperCalls = new HashMap<>(); private final Map> classVarInitOrderCache = new HashMap<>(); private final Map guaranteedClassFieldInitCache = new IdentityHashMap<>(); + private final Map> moduleFieldCopiesCache = new IdentityHashMap<>(); /** * When true, the build targets a legacy patch (pre-1.24) whose Blizzard-provided @@ -85,6 +86,7 @@ public void validate(Collection toCheck) { heavyFunctions.clear(); heavyBlocks.clear(); guaranteedClassFieldInitCache.clear(); + moduleFieldCopiesCache.clear(); lightValidation(toCheck); @@ -1764,7 +1766,8 @@ private void checkPotentiallyUninitializedClassFields(FunctionLike function) { return; } - Set writtenFields = Collections.newSetFromMap(new IdentityHashMap<>()); + Deque> writtenFieldScopes = new ArrayDeque<>(); + writtenFieldScopes.push(Collections.newSetFromMap(new IdentityHashMap<>())); Set warned = Collections.newSetFromMap(new IdentityHashMap<>()); FunctionCall delegatedConstructorCall = function instanceof ConstructorDef ? getFirstThisConstructorCall((ConstructorDef) function) : null; @@ -1779,9 +1782,14 @@ private void checkField(NameRef access) { } if (!(field.getInitialExpr() instanceof NoExpr) || (isCurrentInstanceAccess(access) - && writtenFields.contains(field)) + && writtenFieldScopes.peek().contains(field)) || ((!isCurrentInstanceAccess(access) || !(function instanceof ConstructorDef)) && hasGuaranteedConstructorAssignment(field)) + || (isCurrentInstanceAccess(access) + && function instanceof ConstructorDef + && delegatedConstructorCall == null + && !access.isSubtreeOf(((ConstructorDef) function).getSuperConstructorCall()) + && initializedBySuperConstructor((ConstructorDef) function, field)) || (delegatedConstructorCall != null && !access.isSubtreeOf(delegatedConstructorCall) && hasGuaranteedConstructorAssignment(field)) || !warned.add(field)) { @@ -1835,11 +1843,19 @@ public void visit(ExprMemberArrayVarDotDot access) { checkField(access); } + @Override + public void visit(ExprClosure closure) { + Set closureScope = Collections.newSetFromMap(new IdentityHashMap<>()); + closureScope.addAll(writtenFieldScopes.peek()); + writtenFieldScopes.push(closureScope); + super.visit(closure); + writtenFieldScopes.pop(); + } + @Override public void visit(StmtSet assignment) { super.visit(assignment); if (!(assignment.getUpdatedExpr() instanceof NameRef access) - || isInNestedClosure(access) || !isCurrentInstanceAccess(access) || !isWriteTarget(access)) { return; @@ -1847,7 +1863,7 @@ public void visit(StmtSet assignment) { NameDef nameDef = access.attrNameDef(); if (nameDef instanceof GlobalVarDef field && field.attrIsDynamicClassMember() && isWholeFieldAccess(access)) { - writtenFields.add(field); + writtenFieldScopes.peek().add(field); } } @@ -1927,17 +1943,27 @@ private boolean hasGuaranteedConstructorAssignment(GlobalVarDef field) { return cached; } List constructors = constructorsFor(field); + if (allConstructorsAssign(constructors, field) + || moduleFieldCopies(field).stream().anyMatch(copy -> + allConstructorsAssign(constructorsFor(copy), copy) + || allConstructorsAssign(enclosingClassConstructors(copy), copy)) + || allConstructorsAssign(enclosingClassConstructors(field), field)) { + guaranteedClassFieldInitCache.put(field, true); + return true; + } + guaranteedClassFieldInitCache.put(field, false); + return false; + } + + private boolean allConstructorsAssign(List constructors, GlobalVarDef field) { if (constructors.isEmpty()) { - guaranteedClassFieldInitCache.put(field, false); return false; } for (ConstructorDef constructor : constructors) { if (!constructorAssignsField(constructor, field, Collections.newSetFromMap(new IdentityHashMap<>()))) { - guaranteedClassFieldInitCache.put(field, false); return false; } } - guaranteedClassFieldInitCache.put(field, true); return true; } @@ -1997,6 +2023,73 @@ private List constructorsFor(ConstructorDef constructor) { return Collections.emptyList(); } + private List enclosingClassConstructors(GlobalVarDef field) { + Element current = field; + while (current != null) { + if (current instanceof ClassDef classDef) { + return classDef.getConstructors(); + } + current = current.getParent(); + } + return Collections.emptyList(); + } + + private List moduleFieldCopies(GlobalVarDef field) { + List cached = moduleFieldCopiesCache.get(field); + if (cached != null) { + return cached; + } + ClassOrModule owner = field.attrNearestClassOrModule(); + if (!(owner instanceof ModuleDef module)) { + cached = Collections.emptyList(); + moduleFieldCopiesCache.put(field, cached); + return cached; + } + int fieldIndex = module.getVars().indexOf(field); + if (fieldIndex < 0) { + cached = Collections.emptyList(); + moduleFieldCopiesCache.put(field, cached); + return cached; + } + List copies = new ArrayList<>(); + prog.accept(new Element.DefaultVisitor() { + @Override + public void visit(ModuleInstanciation instantiation) { + if (instantiation.attrModuleOrigin() == module + && fieldIndex < instantiation.getVars().size()) { + copies.add(instantiation.getVars().get(fieldIndex)); + } + super.visit(instantiation); + } + }); + cached = List.copyOf(copies); + moduleFieldCopiesCache.put(field, cached); + return cached; + } + + private boolean initializedBySuperConstructor(ConstructorDef constructor, GlobalVarDef field) { + ConstructorDef superConstructor = constructor.attrSuperConstructor(); + return superConstructor != null + && constructorAssignsField(superConstructor, field, + Collections.newSetFromMap(new IdentityHashMap<>())); + } + + private boolean initializedBySuperclass(GlobalVarDef initializedField, GlobalVarDef referencedField) { + ClassDef child = initializedField.attrNearestClassDef(); + ClassDef declaringClass = referencedField.attrNearestClassDef(); + if (child == null || declaringClass == null || child == declaringClass) { + return false; + } + WurstTypeClass superType = child.attrTypC().extendedClass(); + while (superType != null) { + if (superType.getClassDef() == declaringClass) { + return hasGuaranteedConstructorAssignment(referencedField); + } + superType = superType.extendedClass(); + } + return false; + } + private void checkClassFieldInitializerReads(GlobalVarDef field) { if (!field.attrIsDynamicClassMember() || !(field.getInitialExpr() instanceof Expr initializer)) { return; @@ -2009,6 +2102,8 @@ private void checkField(NameRef access) { || !referenced.attrIsDynamicClassMember() || !(referenced.getInitialExpr() instanceof NoExpr) || (!isCurrentInstanceAccess(access) && hasGuaranteedConstructorAssignment(referenced)) + || (isCurrentInstanceAccess(access) + && initializedBySuperclass(field, referenced)) || !warned.add(referenced)) { return; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 4640d7b6c..26ae29e7e 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -336,6 +336,92 @@ public void doesNotWarnForClosureReadAfterPriorAssignment() { result.getGui().getWarningList().toString()); } + @Test + public void doesNotWarnForClosureReadAfterClosureAssignment() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "interface Reader", + " function read() returns int", + "function consume(Reader reader)", + "class Counter", + " int value", + " construct()", + " consume() ->", + " value = 1", + " return value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); + } + + @Test + public void doesNotWarnForInheritedFieldAfterSuperclassConstructor() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Base", + " int value", + " construct()", + " value = 1", + "class Child extends Base", + " construct()", + " int copy = value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); + } + + @Test + public void doesNotWarnForInheritedFieldInitializerAfterSuperclassConstructor() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class Base", + " int value", + " construct()", + " value = 1", + "class Child extends Base", + " int copy = value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("read from a field initializer")), + result.getGui().getWarningList().toString()); + } + + @Test + public void doesNotWarnWhenClassConstructorAssignsModuleField() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "module Values", + " int value", + " function get() returns int", + " return value", + "class Counter", + " use Values", + " construct()", + " value = 1" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); + } + @Test public void classes1() throws IOException { testAssertOkFile(new File(TEST_DIR + "Classes_1.wurst"), true); From 74732fffad38751874e69f2b6fafab0fa02a29cc Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 12:16:58 +0200 Subject: [PATCH 13/14] Complete superclass field guarantees and module indexing --- .../validation/WurstValidator.java | 49 +++++++++---------- .../tests/wurstscript/tests/ClassesTests.java | 23 +++++++++ 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 421bde65e..dba006a68 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -62,6 +62,7 @@ private enum Phase { LIGHT, HEAVY } private final Map> classVarInitOrderCache = new HashMap<>(); private final Map guaranteedClassFieldInitCache = new IdentityHashMap<>(); private final Map> moduleFieldCopiesCache = new IdentityHashMap<>(); + private boolean moduleFieldCopiesIndexed; /** * When true, the build targets a legacy patch (pre-1.24) whose Blizzard-provided @@ -87,6 +88,7 @@ public void validate(Collection toCheck) { heavyBlocks.clear(); guaranteedClassFieldInitCache.clear(); moduleFieldCopiesCache.clear(); + moduleFieldCopiesIndexed = false; lightValidation(toCheck); @@ -1988,11 +1990,12 @@ private boolean constructorAssignsField(ConstructorDef constructor, GlobalVarDef return true; } FunctionCall thisCall = getFirstThisConstructorCall(constructor); - if (thisCall == null) { - return false; + if (thisCall != null) { + ConstructorDef target = OverloadingResolver.resolveThisCall(constructorsFor(constructor), thisCall); + return target != null && target != constructor && constructorAssignsField(target, field, visiting); } - ConstructorDef target = OverloadingResolver.resolveThisCall(constructorsFor(constructor), thisCall); - return target != null && target != constructor && constructorAssignsField(target, field, visiting); + ConstructorDef superConstructor = constructor.attrSuperConstructor(); + return superConstructor != null && constructorAssignsField(superConstructor, field, visiting); } private List constructorsFor(GlobalVarDef field) { @@ -2035,36 +2038,32 @@ private List enclosingClassConstructors(GlobalVarDef field) { } private List moduleFieldCopies(GlobalVarDef field) { - List cached = moduleFieldCopiesCache.get(field); - if (cached != null) { - return cached; - } - ClassOrModule owner = field.attrNearestClassOrModule(); - if (!(owner instanceof ModuleDef module)) { - cached = Collections.emptyList(); - moduleFieldCopiesCache.put(field, cached); - return cached; + if (!moduleFieldCopiesIndexed) { + indexModuleFieldCopies(); } - int fieldIndex = module.getVars().indexOf(field); - if (fieldIndex < 0) { - cached = Collections.emptyList(); - moduleFieldCopiesCache.put(field, cached); - return cached; + return moduleFieldCopiesCache.getOrDefault(field, Collections.emptyList()); + } + + private void indexModuleFieldCopies() { + if (moduleFieldCopiesIndexed) { + return; } - List copies = new ArrayList<>(); prog.accept(new Element.DefaultVisitor() { @Override public void visit(ModuleInstanciation instantiation) { - if (instantiation.attrModuleOrigin() == module - && fieldIndex < instantiation.getVars().size()) { - copies.add(instantiation.getVars().get(fieldIndex)); + ModuleDef origin = instantiation.attrModuleOrigin(); + if (origin != null) { + int count = Math.min(origin.getVars().size(), instantiation.getVars().size()); + for (int i = 0; i < count; i++) { + GlobalVarDef originField = origin.getVars().get(i); + moduleFieldCopiesCache.computeIfAbsent(originField, ignored -> new ArrayList<>()) + .add(instantiation.getVars().get(i)); + } } super.visit(instantiation); } }); - cached = List.copyOf(copies); - moduleFieldCopiesCache.put(field, cached); - return cached; + moduleFieldCopiesIndexed = true; } private boolean initializedBySuperConstructor(ConstructorDef constructor, GlobalVarDef field) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index 26ae29e7e..b5de9fb94 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -380,6 +380,29 @@ public void doesNotWarnForInheritedFieldAfterSuperclassConstructor() { result.getGui().getWarningList().toString()); } + @Test + public void doesNotWarnForInheritedFieldAfterGrandparentConstructor() { + CompilationResult result = test() + .setStopOnFirstError(false) + .executeProg(false) + .lines( + "package Test", + "class GrandBase", + " int value", + " construct()", + " value = 1", + "class Base extends GrandBase", + " construct()", + "class Child extends Base", + " construct()", + " int copy = value" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); + } + @Test public void doesNotWarnForInheritedFieldInitializerAfterSuperclassConstructor() { CompilationResult result = test() From 2a300f7cff473a87f7073113a7c25a40ee04e396 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 31 Aug 2026 12:59:21 +0200 Subject: [PATCH 14/14] Limit field warnings and slim pull request CI --- .github/workflows/build.yml | 45 +++++++++---------- .../validation/WurstValidator.java | 9 ++-- .../tests/wurstscript/tests/ClassesTests.java | 38 ++++++++-------- .../wurstscript/tests/WurstScriptTest.java | 5 +++ 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7e8eab5b..91672d04e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,27 +11,15 @@ concurrency: cancel-in-progress: true jobs: - # Emit a wider matrix on master pushes (adds macOS), narrow on PRs (saves minutes) - setup: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - id: set-matrix - shell: bash - run: | - if [[ "${{ github.event_name }}" == "push" ]]; then - echo 'matrix={"include":[{"os":"ubuntu-latest"},{"os":"windows-latest"},{"os":"macos-latest"},{"os":"macos-15-intel"}]}' >> "$GITHUB_OUTPUT" - else - echo 'matrix={"include":[{"os":"ubuntu-latest"},{"os":"windows-latest"}]}' >> "$GITHUB_OUTPUT" - fi - + # Master pushes exercise every supported host. Pull requests use one Linux job + # so that the normal feedback loop does not spend four runner-minutes on the + # same compiler test suite; the full host matrix still runs before release. build: name: Gradle build on ${{ matrix.os }} - needs: setup strategy: fail-fast: false - matrix: ${{ fromJson(needs.setup.outputs.matrix) }} + matrix: + os: ${{ fromJSON(github.event_name == 'push' && '["ubuntu-latest","windows-latest","macos-latest","macos-15-intel"]' || '["ubuntu-latest"]') }} runs-on: ${{ matrix.os }} permissions: checks: write @@ -96,8 +84,10 @@ jobs: - name: Setup Gradle (cache) uses: gradle/actions/setup-gradle@v4 - # ---- FAIL FAST: package first (so jlink issues show immediately) ---- + # Packaging is a release artifact, not a pull-request gate. Keeping it on + # master avoids paying for a second full build on every PR update. - name: Package slim runtime (fail fast) + if: github.event_name == 'push' shell: bash run: ./gradlew packageSlimCompilerDist --no-daemon --stacktrace @@ -105,9 +95,17 @@ jobs: if: runner.os == 'Linux' shell: bash run: | - if [[ -f src/test/resources/lua53 ]]; then - chmod +x src/test/resources/lua53 - fi + set -euo pipefail + # Use Ubuntu's maintained Lua 5.3 build. The checked-in portable + # binary is linked against libreadline.so.6, which is absent from + # current runner images. + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends lua5.3 + lua5.3 -e 'assert(_VERSION == "Lua 5.3")' + luac5.3 -v + # These binaries are tracked with mode 0644 for cross-platform + # checkouts; keep them usable as a fallback for local/older images. + chmod +x src/test/resources/lua53 src/test/resources/luac53 - name: Install Lua compiler (macOS) if: runner.os == 'macOS' @@ -119,14 +117,14 @@ jobs: - name: Run tests shell: bash run: | - if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ github.event_name }}" == "push" && "${{ runner.os }}" == "Linux" ]]; then ./gradlew test jacocoTestReport --no-daemon --stacktrace --quiet else ./gradlew test --no-daemon --stacktrace --quiet fi - name: Upload coverage to Coveralls - if: runner.os == 'Linux' + if: github.event_name == 'push' && runner.os == 'Linux' uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -157,6 +155,7 @@ jobs: retention-days: 14 - name: Upload packaged artifact (per-OS) + if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: wurst-compiler-${{ matrix.os }} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index dba006a68..5d2f55ba3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -1759,12 +1759,13 @@ private void checkUninitializedVars(FunctionLike f) { /** * Instance fields without an initializer are reset to the language default when an object is - * allocated, but that value is often accidental. Warn when a method reads such a field and no - * constructor is known to assign it on every construction path. This deliberately stays a - * cheap, local check: it does not attempt interprocedural or path-sensitive reasoning. + * allocated, but that value is often accidental. Warn when a constructor reads such a field + * before its value is definitely established. Ordinary methods are intentionally out of scope: + * their callers may establish fields through APIs or other construction-time hooks that this + * cheap local check cannot see. */ private void checkPotentiallyUninitializedClassFields(FunctionLike function) { - if (function instanceof OnDestroyDef) { + if (function instanceof OnDestroyDef || !(function instanceof ConstructorDef)) { return; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java index b5de9fb94..a6773e701 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ClassesTests.java @@ -28,12 +28,9 @@ public void warnsWhenClassFieldHasNoInitializerOrConstructorAssignment() { "package Test", "class Counter", " int value", - " function get() returns int", - " return value", - " function readBeforeSet() returns int", + " construct()", " int oldValue = value", - " value = 42", - " return oldValue" + " value = 42" ); } @@ -70,8 +67,7 @@ public void warnsWhenConstructorOnlyAssignsOtherInstanceField() { " int value", " construct(Counter other)", " other.value = 1", - " function get() returns int", - " return value" + " int _observed = this.value" ); assertEquals(result.getGui().getWarningList().stream() @@ -105,8 +101,8 @@ public void warnsWhenUnqualifiedArrayFieldHasNoInitializer() { "package Test", "class Counter", " int values[2]", - " function get() returns int", - " return values[0]" + " construct()", + " int first = values[0]" ); } @@ -120,14 +116,14 @@ public void warnsWhenFieldIsReadOnRightHandSideBeforeAssignment() { "package Test", "class Counter", " int value", - " function increment() returns int", + " construct()", " value = value + 1", - " return value" + " skip" ); } @Test - public void warnsWhenOtherInstanceFieldIsReadAfterThisFieldWrite() { + public void warnsWhenOtherInstanceFieldIsReadInConstructor() { test() .setStopOnFirstError(false) .executeProg(false) @@ -136,9 +132,8 @@ public void warnsWhenOtherInstanceFieldIsReadAfterThisFieldWrite() { "package Test", "class Counter", " int value", - " function getOther(Counter other) returns int", - " value = 1", - " return other.value" + " construct(Counter other)", + " let _observed = other.value" ); } @@ -152,9 +147,9 @@ public void warnsWhenOnlyOneArrayElementWasAssigned() { "package Test", "class Counter", " int values[2]", - " function getOther() returns int", + " construct()", " values[0] = 1", - " return values[1]" + " int observed = values[1]" ); } @@ -300,11 +295,10 @@ public void doesNotWarnForInitializedExplicitReceiverInsideConstructor() { } @Test - public void warnsForUninitializedFieldReadFromPackageFunction() { - test() + public void doesNotWarnForUninitializedFieldReadFromPackageFunction() { + CompilationResult result = test() .setStopOnFirstError(false) .executeProg(false) - .expectWarning("no explicit initializer and is not definitely assigned") .lines( "package Test", "class Counter", @@ -312,6 +306,10 @@ public void warnsForUninitializedFieldReadFromPackageFunction() { "function read(Counter counter) returns int", " return counter.value" ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("no explicit initializer and is not definitely assigned")), + result.getGui().getWarningList().toString()); } @Test diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java index 999452e98..023bac4e4 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/WurstScriptTest.java @@ -757,6 +757,10 @@ protected String getLuaExecutable() { candidates.add("lua53.exe"); candidates.add("lua"); } else { + // Prefer the distribution's versioned Lua 5.3 binary when present. + // The checked-in portable binary may depend on an older system + // readline ABI on newer Linux runner images. + candidates.add("lua5.3"); if (bundledLuaUnix.exists()) { // best effort in case execute bit was lost by checkout settings // (e.g. core.filemode false on some environments) @@ -817,6 +821,7 @@ private String getLuacExecutable() { candidates.add("luac.exe"); candidates.add("luac"); } else { + candidates.add("luac5.3"); if (bundledLuacUnix.exists()) { bundledLuacUnix.setExecutable(true); if (bundledLuacUnix.canExecute()) {