diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java index 1841a986d..6b3f985a3 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFuncDef.java @@ -6,6 +6,7 @@ import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.attributes.names.FuncLink; +import de.peeeq.wurstscript.attributes.names.NameResolution; import de.peeeq.wurstscript.attributes.names.Visibility; import de.peeeq.wurstscript.types.*; import de.peeeq.wurstscript.utils.Utils; @@ -18,6 +19,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import static de.peeeq.wurstscript.attributes.AttrPossibleFunctionSignatures.*; +import static de.peeeq.wurstscript.attributes.names.NameResolution.lookupMemberFuncs; + /** * this attribute find the variable definition for every variable reference @@ -65,18 +69,102 @@ public static FuncLink calculate(final ExprFuncRef node) { } public static @Nullable FuncLink calculate(final ExprMemberMethod node) { + WurstType recvT = node.getLeft().attrTyp(); + var raw = NameResolution.lookupMemberFuncs(node, recvT, node.getFuncName(), /*showErrors=*/false); - Expr left = node.getLeft(); - WurstType leftType = left.attrTyp(); - String funcName = node.getFuncName(); + java.util.ArrayList visible = new java.util.ArrayList<>(raw.size()); + java.util.ArrayList hidden = new java.util.ArrayList<>(raw.size()); + for (var f : raw) { + if (isVisible(f)) visible.add(f); else hidden.add(f); + } - @Nullable FuncLink result = searchMemberFunc(node, leftType, funcName, argumentTypes(node)); - if (result == null) { - node.addError("The method " + funcName + " is undefined for receiver of type " + leftType); + if (!raw.isEmpty() && visible.isEmpty()) { + // Keep the classic diagnostic the tests look for: + node.addError("The method " + node.getFuncName() + " is not visible here."); + return null; // don’t leak a def to downstream passes/codegen } - return result; + + java.util.List methods = new java.util.ArrayList<>(); + java.util.List exts = new java.util.ArrayList<>(); + for (var f : visible) { + if (isExtension(f)) exts.add(f); else methods.add(f); + } + + if (!exts.isEmpty()) { + exts = keepMostSpecificReceivers(exts, FuncLink::getReceiverType, node); + } + + java.util.ArrayList cands = new java.util.ArrayList<>(methods.size() + exts.size()); + cands.addAll(methods); + cands.addAll(exts); + + var argTypes = AttrFuncDef.argumentTypesPre(node); + + // Pass 1: exact matches + java.util.ArrayList exactLinks = new java.util.ArrayList<>(); + java.util.ArrayList exactSigs = new java.util.ArrayList<>(); + for (var f : cands) { + var sig = FunctionSignature.fromNameLink(f); + var m = sig.matchAgainstArgs(argTypes, node); + if (m != null) { + exactLinks.add(f); + exactSigs.add(m); + } + } + if (!exactLinks.isEmpty()) { + // methods vs others + java.util.ArrayList methodIdxs = new java.util.ArrayList<>(); + for (int i = 0; i < exactLinks.size(); i++) { + if (!isExtension(exactLinks.get(i))) methodIdxs.add(i); + } + if (methodIdxs.size() > 1) { + // filter method candidates by most specific receiver + java.util.ArrayList methSigs = new java.util.ArrayList<>(); + for (int i : methodIdxs) methSigs.add(exactSigs.get(i)); + methSigs = (java.util.ArrayList) keepMostSpecificReceivers( + methSigs, FunctionSignature::getReceiverType, node + ); + // pick the first of the survivors + var chosenSig = methSigs.get(0); + // find corresponding link + for (int i = 0; i < exactSigs.size(); i++) { + if (exactSigs.get(i) == chosenSig) { + return exactLinks.get(i).withTypeArgBinding(node, chosenSig.getMapping()); + } + } + } else if (methodIdxs.size() == 1) { + int i = methodIdxs.get(0); + return exactLinks.get(i).withTypeArgBinding(node, exactSigs.get(i).getMapping()); + } else { + // no methods, only extensions exact → pick first (they were narrowed already) + return exactLinks.get(0).withTypeArgBinding(node, exactSigs.get(0).getMapping()); + } + } + + // Pass 2: best-effort (unchanged) + int bestBad = Integer.MAX_VALUE; + FuncLink best = null; + FunctionSignature bestSig = null; + for (var f : cands) { + var sig = FunctionSignature.fromNameLink(f); + var r = sig.tryMatchAgainstArgs(argTypes, node.getArgs(), node); + if (r.getBadness() < bestBad) { + bestBad = r.getBadness(); + best = f; + bestSig = r.getSig(); + } + } + return best == null ? null : best.withTypeArgBinding(node, bestSig.getMapping()); + } + + + + public static @Nullable FunctionDefinition calculateDef(final ExprMemberMethod node) { + var fl = node.attrFuncLink(); + return fl == null ? null : fl.getDef(); } + public static @Nullable FuncLink calculate(final ExprFunctionCall node) { FuncLink result = searchFunction(node.getFuncName(), node, argumentTypes(node)); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java index ece736bdc..e100bc425 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrFunctionSignature.java @@ -18,11 +18,25 @@ public class AttrFunctionSignature { public static FunctionSignature calculate(StmtCall fc) { Collection sigs = fc.attrPossibleFunctionSignatures(); FunctionSignature sig = filterSigs(sigs, argTypes(fc), fc); + VariableBinding mapping = sig.getMapping(); for (CompileError error : mapping.getErrors()) { fc.getErrorHandler().sendError(error); } - if (mapping.hasUnboundTypeVars()) { + + // If any argument is a closure, let it be typed using the selected signature’s + // expected parameter types before complaining about unbound type variables. + boolean hasClosureArg = false; + if (fc instanceof AstElementWithArgs) { + for (Expr a : ((AstElementWithArgs) fc).getArgs()) { + if (a instanceof ExprClosure) { + hasClosureArg = true; + break; + } + } + } + + if (mapping.hasUnboundTypeVars() && !hasClosureArg) { fc.addError("Cannot infer type for type parameter " + mapping.printUnboundTypeVars()); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java index 2ef6544e3..8ad6a49d7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrModuleInstanciations.java @@ -1,21 +1,41 @@ package de.peeeq.wurstscript.attributes; -import de.peeeq.wurstscript.ast.ModuleDef; -import de.peeeq.wurstscript.ast.ModuleInstanciation; -import de.peeeq.wurstscript.ast.TypeDef; +import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.utils.Utils; import org.eclipse.jdt.annotation.Nullable; -public class AttrModuleInstanciations { +public final class AttrModuleInstanciations { + + private AttrModuleInstanciations() {} public static @Nullable ModuleDef getModuleOrigin(ModuleInstanciation mi) { - TypeDef def = mi.getParent().lookupType(mi.getName()); - if (def instanceof ModuleDef) { - return (ModuleDef) def; - } else { + // NOTE: For ModuleInstanciation the "name" used for resolution has historically been getName(). + // Keep this to preserve prior behavior. + final String name = mi.getName(); + + // 1) Normal path: resolve relative to the lexical parent (old behavior) + final Element parent = mi.getParent(); + if (parent != null) { + TypeDef def = parent.lookupType(name, /*showErrors*/ false); + if (def instanceof ModuleDef) { + return (ModuleDef) def; + } + // Attached but not found -> keep the old error mi.addError("Could not find module origin for " + Utils.printElement(mi)); + return null; + } + + // 2) Detached during incremental build: try the nearest attached scope + final WScope scope = mi.attrNearestScope(); + if (scope != null) { + TypeDef def = scope.lookupType(name, /*showErrors*/ false); + if (def instanceof ModuleDef) { + return (ModuleDef) def; + } } + + // 3) Still not found and we're detached: this can be a transient state, + // so don't emit an error here. Return null and let callers handle gracefully. return null; } - } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java index 42b3f09d3..6b21a85d4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrPossibleFunctionSignatures.java @@ -5,12 +5,12 @@ import com.google.common.collect.Lists; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.attributes.names.FuncLink; -import de.peeeq.wurstscript.types.FunctionSignature; +import de.peeeq.wurstscript.attributes.names.NameResolution; +import de.peeeq.wurstscript.attributes.names.Visibility; +import de.peeeq.wurstscript.types.*; import de.peeeq.wurstscript.types.FunctionSignature.ArgsMatchResult; -import de.peeeq.wurstscript.types.VariableBinding; -import de.peeeq.wurstscript.types.VariablePosition; -import de.peeeq.wurstscript.types.WurstType; +import java.util.ArrayList; import java.util.List; import static de.peeeq.wurstscript.attributes.GenericsHelper.givenBinding; @@ -46,73 +46,243 @@ public static ImmutableCollection calculate(FunctionCall fc) private static ImmutableCollection findBestSignature(StmtCall fc, ImmutableCollection res) { - // Fast path: nothing to consider - if (res.isEmpty()) { - return ImmutableList.of(); - } + if (res.isEmpty()) return ImmutableList.of(); - // Materialize once to a random-access list (cheap for Immutable*) - final ImmutableList sigs = - (res instanceof ImmutableList) - ? (ImmutableList) res - : ImmutableList.copyOf(res); + final ImmutableList sigs = (res instanceof ImmutableList) + ? (ImmutableList) res + : ImmutableList.copyOf(res); - // Compute arg types once final List argTypes = AttrFuncDef.argumentTypesPre(fc); - // --- Pass 1: exact matches only (cheap) --------------------------------- - // Use a single ArrayList and only copy if we actually have matches. - List exact = new java.util.ArrayList<>(sigs.size()); - for (int i = 0, n = sigs.size(); i < n; i++) { - FunctionSignature matched = sigs.get(i).matchAgainstArgs(argTypes, fc); - if (matched != null) { - exact.add(matched); - } + // Pass 1: exact matches + List exact = new ArrayList<>(sigs.size()); + for (FunctionSignature s : sigs) { + FunctionSignature m = s.matchAgainstArgs(argTypes, fc); + if (m != null) exact.add(m); } - if (!exact.isEmpty()) { - return ImmutableList.copyOf(exact); + if (!exact.isEmpty()) return ImmutableList.copyOf(exact); + + // Pass 2: best effort + (maybe) errors + boolean allKnown = true; + for (WurstType t : argTypes) { + if (t instanceof WurstTypeUnknown) { allKnown = false; break; } } - // --- Pass 2: best-effort matches (no exact match) ------------------------ - // We must: - // * find the min-badness result (to emit its errors) - // * return ALL resulting signatures (to preserve current semantics) - final int n = sigs.size(); - FunctionSignature[] inferredSigs = new FunctionSignature[n]; - int bestIdx = -1; - int bestBadness = Integer.MAX_VALUE; - ArgsMatchResult bestResult = null; + int n = sigs.size(), bestIdx = -1, bestBad = Integer.MAX_VALUE; + FunctionSignature[] inferred = new FunctionSignature[n]; + FunctionSignature.ArgsMatchResult best = null; - // Cache args node once final Arguments argsNode = fc.getArgs(); - for (int i = 0; i < n; i++) { - // tryMatchAgainstArgs may also perform type-arg inference; we must keep its result sig - ArgsMatchResult r = sigs.get(i).tryMatchAgainstArgs(argTypes, argsNode, fc); - inferredSigs[i] = r.getSig(); - int b = r.getBadness(); - if (b < bestBadness) { - bestBadness = b; - bestIdx = i; - bestResult = r; + var r = sigs.get(i).tryMatchAgainstArgs(argTypes, argsNode, fc); + inferred[i] = r.getSig(); + if (r.getBadness() < bestBad) { bestBad = r.getBadness(); bestIdx = i; best = r; } + } + + if (allKnown && best != null) { + for (var c : best.getErrors()) { + fc.getErrorHandler().sendError(c); } } - if (bestIdx == -1 || bestResult == null) { - // Shouldn’t happen, but be safe + return ImmutableList.copyOf(inferred); + } + + + public static boolean isExtension(de.peeeq.wurstscript.attributes.names.FuncLink f) { + return f.getDef() instanceof de.peeeq.wurstscript.ast.ExtensionFuncDef; + } + + public static boolean isVisible(de.peeeq.wurstscript.attributes.names.FuncLink f) { + Visibility v = f.getVisibility(); + // NameLinks pre-resolve visibility “relative to the current site”. + // *_OTHER means: *not* visible here. + return v != Visibility.PRIVATE_OTHER && v != Visibility.PROTECTED_OTHER; + } + + public static java.util.List keepMostSpecificReceivers( + java.util.List candidates, + java.util.function.Function recvOf, + de.peeeq.wurstscript.ast.Element site // for isSubtypeOf diagnostics + ) { + if (candidates.size() <= 1) return candidates; + java.util.ArrayList kept = new java.util.ArrayList<>(candidates.size()); + outer: + for (int i = 0; i < candidates.size(); i++) { + var ri = recvOf.apply(candidates.get(i)); + if (ri == null) { kept.add(candidates.get(i)); continue; } // static/global + for (int j = 0; j < candidates.size(); j++) { + if (i == j) continue; + var rj = recvOf.apply(candidates.get(j)); + if (rj == null) continue; + // If rj is a strict subtype of ri, drop ri + boolean rj_le_ri = rj.isSubtypeOf(ri, site); + boolean ri_le_rj = ri.isSubtypeOf(rj, site); + if (rj_le_ri && !ri_le_rj) { + continue outer; + } + } + kept.add(candidates.get(i)); + } + return kept.isEmpty() ? candidates : kept; + } + + + + public static ImmutableCollection calculate(ExprMemberMethod mm) { + // Receiver and method name + final Expr left = mm.getLeft(); + final WurstType leftType = left.attrTyp(); + final String name = mm.getFuncName(); + + // Collect raw member candidates (no errors yet) + final ImmutableCollection raw = + mm.lookupMemberFuncs(leftType, name, /*showErrors*/ false); + + if (raw.isEmpty()) { + // Let downstream handle "not found" return ImmutableList.of(); } - // Emit errors from the best match (same as before) - for (CompileError c : bestResult.getErrors()) { - fc.getErrorHandler().sendError(c); + // Partition by accessibility + final java.util.List visible = new java.util.ArrayList<>(raw.size()); + final java.util.List hidden = new java.util.ArrayList<>(2); + for (FuncLink f : raw) { + de.peeeq.wurstscript.attributes.names.Visibility v = f.getVisibility(); + boolean accessible = (v != de.peeeq.wurstscript.attributes.names.Visibility.PRIVATE_OTHER + && v != de.peeeq.wurstscript.attributes.names.Visibility.PROTECTED_OTHER); + if (accessible) { + visible.add(f); + } else { + hidden.add(f); + } } - // Return ALL candidate signatures (same as previous behavior) - // Avoid another stream/collect - ImmutableList.Builder out = ImmutableList.builderWithExpectedSize(n); - for (int i = 0; i < n; i++) { - out.add(inferredSigs[i]); + if (visible.isEmpty()) { + // Only hidden matches exist → keep the old precise message + if (!hidden.isEmpty()) { + mm.addError( + de.peeeq.wurstscript.utils.Utils.printElement(java.util.Optional.of(hidden.get(0).getDef())) + + " is not visible here."); + } + // Return non-empty to avoid a second generic "not found" message + return ImmutableList.of(FunctionSignature.empty); + } + + // Prepare function signatures: bind receiver + explicit type args + final java.util.List prepared = new java.util.ArrayList<>(visible.size()); + for (FuncLink f : visible) { + FunctionSignature sig = FunctionSignature.fromNameLink(f); + + // Bind type variables using the actual receiver + WurstType recv = sig.getReceiverType(); + if (recv != null) { + VariableBinding m = leftType.matchAgainstSupertype( + recv, mm, sig.getMapping(), VariablePosition.RIGHT); + if (m == null) { + // Should not happen; lookupMemberFuncs already checked. Skip defensively. + continue; + } + sig = sig.setTypeArgs(mm, m); + } + + // Apply explicit type args from the call-site (e.g., c.foo(...)) + VariableBinding explicit = GenericsHelper.givenBinding(mm, sig.getDefinitionTypeVariables()); + sig = sig.setTypeArgs(mm, explicit); + + prepared.add(sig); + } + + if (prepared.isEmpty()) { + return ImmutableList.of(); + } + + // Rank by argument match FIRST (fixes subclass-overload case) + final java.util.List argTypes = AttrFuncDef.argumentTypesPre(mm); + final java.util.List results = + new java.util.ArrayList<>(prepared.size()); + + int minBadness = Integer.MAX_VALUE; + for (FunctionSignature sig : prepared) { + FunctionSignature.ArgsMatchResult r = + sig.tryMatchAgainstArgs(argTypes, mm.getArgs(), mm); + results.add(r); + if (r.getBadness() < minBadness) { + minBadness = r.getBadness(); + } + } + + // Keep only best-badness candidates + final java.util.List best = + new java.util.ArrayList<>(); + for (FunctionSignature.ArgsMatchResult r : results) { + if (r.getBadness() == minBadness) { + best.add(r); + } + } + + // Among equally good matches, prefer the ones that fully infer type vars + boolean anyFullyInferred = false; + for (FunctionSignature.ArgsMatchResult r : best) { + if (!r.getSig().getMapping().hasUnboundTypeVars()) { + anyFullyInferred = true; + break; + } + } + if (anyFullyInferred) { + final java.util.List filtered = new java.util.ArrayList<>(best.size()); + for (FunctionSignature.ArgsMatchResult r : best) { + if (!r.getSig().getMapping().hasUnboundTypeVars()) { + filtered.add(r); + } + } + if (!filtered.isEmpty()) { + best.clear(); + best.addAll(filtered); + } + } + + // Among still-equal matches, prefer the most-specific receiver + if (best.size() > 1) { + final int n = best.size(); + final boolean[] drop = new boolean[n]; + + for (int i = 0; i < n; i++) { + WurstType ri = best.get(i).getSig().getReceiverType(); + if (ri == null) continue; + for (int j = 0; j < n; j++) { + if (i == j || drop[i]) continue; + WurstType rj = best.get(j).getSig().getReceiverType(); + if (rj == null) continue; + + boolean jSubI = rj.isSubtypeOf(ri, mm); + boolean iSubJ = ri.isSubtypeOf(rj, mm); + + // j strictly more specific than i → drop i + if (jSubI && !iSubJ) { + drop[i] = true; + } + } + } + + boolean anyDrop = false; + for (boolean d : drop) { if (d) { anyDrop = true; break; } } + if (anyDrop) { + java.util.List filtered = new java.util.ArrayList<>(n); + for (int i = 0; i < n; i++) { + if (!drop[i]) filtered.add(best.get(i)); + } + if (!filtered.isEmpty()) { + best.clear(); + best.addAll(filtered); + } + } + } + + // IMPORTANT: do NOT emit errors here (defer to AttrFunctionSignature) + ImmutableList.Builder out = ImmutableList.builderWithExpectedSize(best.size()); + for (FunctionSignature.ArgsMatchResult r : best) { + out.add(r.getSig()); } return out.build(); } 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 458a1074d..9fad0ba50 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 @@ -1509,4 +1509,35 @@ public void derivedGenericClassConstructorNewGenerics() { } + @Test + public void overloadsHiddenBySubclassName_onlyZeroArgSeen_errorsOnArgs() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "native println(string s)", + "class A", + " function func(int i)", + " println(\"func_int...\")", + "", + " function func(string s)", + " println(\"func_string...\")", + "", + "class B extends A", + " function func()", + " println(\"func_noarg\")", + "", + " function func(bool b)", + " println(\"func_boolean...\")", + "", + "init", + " let b = new B()", + " b.func()", + " b.func(true)", + " b.func(1)", + " b.func(\"1\")", + " testSuccess()" + ); + } + + }