From 0e86588cd0564d80248395f65d293340f37a1ab3 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 20:15:56 +0200 Subject: [PATCH 1/2] correctly resolve nested module usage --- .../attributes/names/NameResolution.java | 142 ++++++++++++++++-- .../validation/WurstValidator.java | 10 +- .../tests/wurstscript/tests/BugTests.java | 44 ++++++ 3 files changed, 172 insertions(+), 24 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java index 6ba2ed0b3..d98cf4f38 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java @@ -267,32 +267,41 @@ public static NameLink lookupMemberVar(Element node, WurstType receiverType, Str scope = nextScope(scope); } + DefLinkMatch bestMatch = null; + for (WScope s : scopes) { Collection links = s.attrNameLinks().get(name); if (links.isEmpty()) continue; - for (DefLink n : links) { - if (!(n instanceof VarLink)) { - continue; - } - DefLink n2 = matchDefLinkReceiver(n, receiverType, node, showErrors); - if (n2 != null) { - if (!showErrors) { - GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(node, name + "@" + receiverType, GlobalCaches.LookupType.MEMBER_VAR); - GlobalCaches.lookupCache.put(key, n2); + DefLinkMatch candidate = findBestMemberVarMatch(links, receiverType, node, showErrors); + if (candidate != null) { + if (bestMatch == null || candidate.distance < bestMatch.distance) { + bestMatch = candidate; + if (bestMatch.distance == 0) { + break; } - return n2; } } } + if (bestMatch != null) { + if (!showErrors) { + GlobalCaches.CacheKey key = new GlobalCaches.CacheKey(node, name + "@" + receiverType, GlobalCaches.LookupType.MEMBER_VAR); + GlobalCaches.lookupCache.put(key, bestMatch.link); + } + return bestMatch.link; + } + if (receiverType instanceof WurstTypeClassOrInterface) { WurstTypeClassOrInterface ct = (WurstTypeClassOrInterface) receiverType; - for (DefLink n : ct.nameLinks().get(name)) { - if (n instanceof VarLink || n instanceof TypeDefLink) { - if (n.getVisibility().isPublic()) { - return n; - } + Collection typeNameLinks = ct.nameLinks().get(name); + DefLinkMatch candidate = findBestMemberVarMatch(typeNameLinks, receiverType, node, showErrors); + if (candidate != null && candidate.link.getVisibility().isPublic()) { + return candidate.link; + } + for (DefLink n : typeNameLinks) { + if (n instanceof TypeDefLink && n.getVisibility().isPublic()) { + return n; } } } @@ -300,6 +309,108 @@ public static NameLink lookupMemberVar(Element node, WurstType receiverType, Str return null; } + private static @Nullable DefLinkMatch findBestMemberVarMatch(Collection links, WurstType receiverType, Element node, boolean showErrors) { + DefLink bestLink = null; + int bestDistance = Integer.MAX_VALUE; + + for (DefLink n : links) { + if (!(n instanceof VarLink)) { + continue; + } + DefLink matched = matchDefLinkReceiver(n, receiverType, node, showErrors); + if (matched == null) { + continue; + } + int distance = receiverDistance(receiverType, matched.getReceiverType(), node); + if (distance < bestDistance) { + bestLink = matched; + bestDistance = distance; + if (distance == 0) { + break; + } + } + } + + if (bestLink == null) { + return null; + } + return new DefLinkMatch(bestLink, bestDistance); + } + + private static int receiverDistance(WurstType receiverType, @Nullable WurstType candidateType, Element node) { + if (candidateType == null) { + return Integer.MAX_VALUE; + } + + if (receiverType.equalsType(candidateType, node)) { + return 0; + } + + ClassDef receiverClass = owningClass(receiverType); + ClassDef candidateClass = owningClass(candidateType); + if (receiverClass != null && candidateClass != null) { + int distance = inheritanceDistance(receiverClass, candidateClass); + if (distance >= 0) { + return distance; + } + } + + return Integer.MAX_VALUE / 2; + } + + private static int inheritanceDistance(ClassDef start, ClassDef target) { + int distance = 0; + ClassDef current = start; + while (current != null) { + if (current == target) { + return distance; + } + OptTypeExpr extended = current.getExtendedClass(); + if (!(extended instanceof TypeExpr)) { + break; + } + WurstType extendedType = ((TypeExpr) extended).attrTyp(); + if (!(extendedType instanceof WurstTypeClass)) { + break; + } + current = ((WurstTypeClass) extendedType).getClassDef(); + distance++; + } + return -1; + } + + private static @Nullable ClassDef owningClass(WurstType type) { + if (type instanceof WurstTypeClass) { + return ((WurstTypeClass) type).getClassDef(); + } + if (type instanceof WurstTypeClassOrInterface) { + ClassOrInterface def = ((WurstTypeClassOrInterface) type).getDef(); + if (def instanceof ClassDef) { + return (ClassDef) def; + } + return null; + } + if (type instanceof WurstTypeModuleInstanciation) { + NamedScope inst = ((WurstTypeModuleInstanciation) type).getDef(); + return inst.attrNearestClassDef(); + } + if (type instanceof WurstTypeModule) { + ModuleDef moduleDef = ((WurstTypeModule) type).getDef(); + return moduleDef.attrNearestClassDef(); + } + return null; + } + + private static final class DefLinkMatch { + private final DefLink link; + private final int distance; + + private DefLinkMatch(DefLink link, int distance) { + this.link = link; + this.distance = distance; + } + } + public static DefLink matchDefLinkReceiver(DefLink n, WurstType receiverType, Element node, boolean showErrors) { WurstType n_receiverType = n.getReceiverType(); if (n_receiverType == null) { @@ -339,6 +450,7 @@ public static DefLink matchDefLinkReceiver(DefLink n, WurstType receiverType, El scope = nextScope(scope); } + for (WScope s : scopes) { ImmutableCollection links = s.attrTypeNameLinks().get(name); if (links.isEmpty()) continue; 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 da9a5b204..2958c2281 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 @@ -1257,15 +1257,7 @@ private void checkAssignment(boolean isJassCode, Element pos, WurstType leftType if (rightType instanceof WurstTypeVoid) { if (pos.attrNearestPackage() instanceof WPackage) { WPackage pack = (WPackage) pos.attrNearestPackage(); - if (pack != null && !pack.getName().equals("WurstREPL")) { // allow - // assigning - // nothing - // to - // a - // variable - // in - // the - // Repl + if (pack != null && !pack.getName().equals("WurstREPL")) { // allow assigning nothing to a variable in the Repl pos.addError("Function or expression returns nothing. Cannot assign nothing to a variable."); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java index 59b8a35d9..97633a923 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/BugTests.java @@ -1636,5 +1636,49 @@ public void moduleInOtherPackage() { ); } + @Test + public void linkedListModule_perClassStatics_andTyping_ok() { + testAssertOkLinesWithStdLib(true, + "package Test", + "import LinkedListModule", + "", + "native println(string s)", + "", + "class A", + " use LinkedListModule", + "", + "class B extends A", + " use LinkedListModule", + "", + "class C extends A", + "", + "class D extends C", + " use LinkedListModule", + "", + "function doSmth()", + " // Iteration should compile for any class that uses LinkedListModule:", + " for a in A", + " // ok: iterates A list", + " for b in B", + " // ok: iterates B list (distinct from A)", + "", + " // Accessing class statics should be typed to that concrete class:", + " B b2 = B.first // must typecheck as B, not A", + " A a2 = A.first // must typecheck as A", + "", + " // D extends C but only D uses the module; typing must be D:", + " var d = D.first // type D", + " while d != null", + " d = d.next // type D", + "", + "init", + " // We don’t rely on runtime behavior here—this test targets typing/name resolution.", + " // If all lines above typecheck and run, we count it as success:", + " doSmth()", + " testSuccess()" + ); + } + + } From 057a713e5069c99a5f6649003a9bc1d7ce4a8245 Mon Sep 17 00:00:00 2001 From: Frotty Date: Wed, 22 Oct 2025 21:21:30 +0200 Subject: [PATCH 2/2] allow accessing protected variables in a subtype class --- .../de/peeeq/wurstscript/attributes/names/NameResolution.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java index d98cf4f38..5723b1fec 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameResolution.java @@ -423,7 +423,7 @@ public static DefLink matchDefLinkReceiver(DefLink n, WurstType receiverType, El if (showErrors) { if (n.getVisibility() == Visibility.PRIVATE_OTHER) { node.addError(Utils.printElement(n.getDef()) + " is private and cannot be used here."); - } else if (n.getVisibility() == Visibility.PROTECTED_OTHER) { + } else if (n.getVisibility() == Visibility.PROTECTED_OTHER && !receiverType.isSubtypeOf(n_receiverType, node)) { node.addError(Utils.printElement(n.getDef()) + " is protected and cannot be used here."); } }