diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CofigActualDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CofigActualDef.java index a717bc5c7..c7fed946c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CofigActualDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CofigActualDef.java @@ -1,8 +1,6 @@ package de.peeeq.wurstscript.attributes; -import com.google.common.collect.ImmutableCollection; import de.peeeq.wurstscript.ast.*; -import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.attributes.names.NameLink; import org.eclipse.jdt.annotation.Nullable; @@ -27,16 +25,24 @@ public static NameDef calculate(GlobalVarDef g) { } public static NameDef calculate(FuncDef f) { + return configuredFunctionOrSelf(f); + } + + public static NameDef calculate(ExtensionFuncDef f) { + return configuredFunctionOrSelf(f); + } + + private static NameDef configuredFunctionOrSelf(FunctionDefinition f) { + if (f instanceof FuncDef && f.attrNearestStructureDef() != null) { + return f; + } WPackage p = getConfigPackage(f); if (p != null) { - ImmutableCollection links = p.getElements().lookupFuncsNoConfig(f.getName(), false); - for (NameLink link : links) { - if (hasConfigAnnotation(link.getDef())) { - return link.getDef(); - } + FunctionDefinition configured = ConfigFunctionMatcher.findMatchingFunction(p, f, true); + if (configured != null) { + return configured; } } - // not configured return f; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ConfigFunctionMatcher.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ConfigFunctionMatcher.java new file mode 100644 index 000000000..5be120a3b --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ConfigFunctionMatcher.java @@ -0,0 +1,115 @@ +package de.peeeq.wurstscript.attributes; + +import de.peeeq.wurstscript.ast.*; +import de.peeeq.wurstscript.attributes.names.DefLink; +import de.peeeq.wurstscript.types.VariableBinding; +import de.peeeq.wurstscript.types.WurstType; +import de.peeeq.wurstscript.types.WurstTypeBoundTypeParam; +import org.eclipse.jdt.annotation.Nullable; + +/** Exact ABI matching for package functions replaced through {@code @config}. */ +public final class ConfigFunctionMatcher { + + private ConfigFunctionMatcher() { + } + + public static @Nullable FunctionDefinition findMatchingFunction(WPackage pack, FunctionDefinition function) { + return findMatchingFunction(pack, function, false); + } + + public static @Nullable FunctionDefinition findMatchingFunction(WPackage pack, FunctionDefinition function, + boolean requireConfigAnnotation) { + for (DefLink link : pack.getElements().attrNameLinks().get(function.getName())) { + if (link.getDef() instanceof FunctionDefinition candidate + && isPackageFunction(candidate, pack) + && (!requireConfigAnnotation || candidate.hasAnnotation("@config")) + && matches(function, candidate)) { + return candidate; + } + } + return null; + } + + private static boolean isPackageFunction(FunctionDefinition function, WPackage pack) { + return function.attrNearestPackage() == pack + && (!(function instanceof FuncDef) || function.attrNearestStructureDef() == null); + } + + public static boolean matches(FunctionDefinition first, FunctionDefinition second) { + if (!first.getName().equals(second.getName())) { + return false; + } + if ((first instanceof ExtensionFuncDef) != (second instanceof ExtensionFuncDef)) { + return false; + } + if (!(first instanceof AstElementWithTypeParameters firstGeneric) + || !(second instanceof AstElementWithTypeParameters secondGeneric)) { + return false; + } + TypeParamDefs firstTypeParams = firstGeneric.getTypeParameters(); + TypeParamDefs secondTypeParams = secondGeneric.getTypeParameters(); + if (firstTypeParams.size() != secondTypeParams.size()) { + return false; + } + + VariableBinding alphaMapping = VariableBinding.emptyMapping(); + for (int i = 0; i < firstTypeParams.size(); i++) { + TypeParamDef firstTypeParam = firstTypeParams.get(i); + TypeParamDef secondTypeParam = secondTypeParams.get(i); + alphaMapping = alphaMapping.set(firstTypeParam, + new WurstTypeBoundTypeParam(firstTypeParam, secondTypeParam.attrTyp(), first)); + } + for (int i = 0; i < firstTypeParams.size(); i++) { + TypeParamDef firstTypeParam = firstTypeParams.get(i); + TypeParamDef secondTypeParam = secondTypeParams.get(i); + if (!equalConstraints(firstTypeParam, secondTypeParam, alphaMapping, first)) { + return false; + } + } + + if (first instanceof ExtensionFuncDef firstExtension) { + ExtensionFuncDef secondExtension = (ExtensionFuncDef) second; + if (!equalType(firstExtension.getExtendedType().attrTyp(), + secondExtension.getExtendedType().attrTyp(), alphaMapping, first)) { + return false; + } + } + if (first.getParameters().size() != second.getParameters().size()) { + return false; + } + for (int i = 0; i < first.getParameters().size(); i++) { + if (!equalType(first.getParameters().get(i).attrTyp(), second.getParameters().get(i).attrTyp(), + alphaMapping, first)) { + return false; + } + } + return equalType(first.attrReturnTyp(), second.attrReturnTyp(), alphaMapping, first); + } + + private static boolean equalConstraints(TypeParamDef first, TypeParamDef second, + VariableBinding alphaMapping, Element location) { + if ((first.getTypeParamConstraints() instanceof TypeExprList) + != (second.getTypeParamConstraints() instanceof TypeExprList)) { + return false; + } + if (!(first.getTypeParamConstraints() instanceof TypeExprList firstConstraints)) { + return true; + } + TypeExprList secondConstraints = (TypeExprList) second.getTypeParamConstraints(); + if (firstConstraints.size() != secondConstraints.size()) { + return false; + } + for (int i = 0; i < firstConstraints.size(); i++) { + if (!equalType(firstConstraints.get(i).attrTyp(), secondConstraints.get(i).attrTyp(), + alphaMapping, location)) { + return false; + } + } + return true; + } + + private static boolean equalType(WurstType first, WurstType second, + VariableBinding alphaMapping, Element location) { + return first.setTypeArgs(alphaMapping).equalsType(second, location); + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java index 2eff16b07..f4b2f345e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java @@ -284,7 +284,10 @@ public boolean isVarargMethod() { public FuncLink withConfigDef() { FunctionDefinition def = (FunctionDefinition) this.def.attrConfigActualNameDef(); - return new FuncLink(getVisibility(), getDefinedIn(), getTypeParams(), getReceiverType(), def, parameterNames, parameterTypes, returnType, mapping); + if (def == this.def) { + return this; + } + return FuncLink.create(def, getDefinedIn()).withVisibility(getVisibility()); } public FuncLink hidingPrivate() { 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 810723097..443e4755b 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 @@ -155,7 +155,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs List fromType = new ArrayList<>(4); addMemberMethods(node, receiverType, name, fromType); for (FuncLink cand : fromType) { - DefLink m = matchDefLinkReceiver(cand, receiverType, node, showErrors); + DefLink m = matchDefLinkReceiver(cand.withConfigDef(), receiverType, node, showErrors); if (m instanceof FuncLink) { result.add((FuncLink) m); } @@ -185,7 +185,7 @@ public static ImmutableCollection lookupMemberFuncs(Element node, Wurs if (!(n instanceof FuncLink)) { continue; } - DefLink n2 = matchDefLinkReceiver(n, receiverType, node, false); + DefLink n2 = matchDefLinkReceiver(((FuncLink) n).withConfigDef(), receiverType, node, false); if (n2 != null) { FuncLink f = (FuncLink) n2; result.add(f); 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 816b8de84..7e93250a4 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 @@ -8,6 +8,7 @@ import de.peeeq.wurstscript.attributes.AttrFuncDef; import de.peeeq.wurstscript.attributes.CofigOverridePackages; import de.peeeq.wurstscript.attributes.CompileError; +import de.peeeq.wurstscript.attributes.ConfigFunctionMatcher; import de.peeeq.wurstscript.attributes.ImplicitFuncs; import de.peeeq.wurstscript.attributes.OverloadingResolver; import de.peeeq.wurstscript.attributes.names.DefLink; @@ -805,19 +806,9 @@ private void checkConfigOverride(NameDef e) { + "It is still possible to configure this var but it is not recommended."); } - } else if (e instanceof FuncDef) { - FuncDef funcDef = (FuncDef) e; - Collection funcs = origPackage.getElements().lookupFuncsNoConfig(funcDef.getName(), false); - FuncDef configuredFunc = null; - for (NameLink nameLink : funcs) { - if (nameLink.getDef() instanceof FuncDef) { - FuncDef f = (FuncDef) nameLink.getDef(); - if (equalSignatures(funcDef, f)) { - configuredFunc = f; - break; - } - } - } + } else if (e instanceof FuncDef || e instanceof ExtensionFuncDef) { + FunctionDefinition funcDef = (FunctionDefinition) e; + FunctionDefinition configuredFunc = ConfigFunctionMatcher.findMatchingFunction(origPackage, funcDef); if (configuredFunc == null) { funcDef.addError("Could not find a function " + funcDef.getName() + " with the same signature in the configured package."); @@ -833,22 +824,6 @@ private void checkConfigOverride(NameDef e) { } } - private boolean equalSignatures(FuncDef f, FuncDef g) { - if (f.getParameters().size() != g.getParameters().size()) { - return false; - } - if (!f.attrReturnTyp().equalsType(g.attrReturnTyp(), f)) { - return false; - } - for (int i = 0; i < f.getParameters().size(); i++) { - if (!f.getParameters().get(i).attrTyp().equalsType(g.getParameters().get(i).attrTyp(), f)) { - return false; - } - } - - return true; - } - private void checkExprEmpty(ExprEmpty e) { e.addError("Incomplete expression..."); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ConfigPackageTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ConfigPackageTests.java index 5a00e673e..6ce9c5f99 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ConfigPackageTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ConfigPackageTests.java @@ -2,6 +2,8 @@ import org.testng.annotations.Test; +import static org.testng.Assert.assertFalse; + public class ConfigPackageTests extends WurstScriptTest { @@ -66,6 +68,319 @@ public void configFuncWrongType() { ); } + @Test + public void configOrdinaryFuncOverloadsByExactSignature() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "@configurable function value(int x) returns int", + " return 1", + "@configurable function value(string x) returns int", + " return 2", + "init", + " if value(1) == 10 and value(\"x\") == 20", + " testSuccess()", + "endpackage", + "package Test_config", + "@config function value(string x) returns int", + " return 20", + "@config function value(int x) returns int", + " return 10", + "endpackage" + ); + } + + @Test + public void configExtensionExecutesConfiguredBody() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "@configurable function int.adjust(int x) returns int", + " return this + x", + "init", + " int value = 4", + " if value.adjust(3) == 12", + " testSuccess()", + "endpackage", + "package Test_config", + "@config function int.adjust(int x) returns int", + " return this * x", + "endpackage" + ); + } + + @Test + public void configurableExtensionProducesNoWarning() { + var result = test().setStopOnFirstError(false).lines( + "package Test", + "@configurable function int.adjust(int x) returns int", + " return this + x", + "endpackage", + "package Test_config", + "@config function int.adjust(int x) returns int", + " return this * x", + "endpackage" + ); + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("not marked with @configurable"))); + } + + @Test + public void unannotatedConfiguredExtensionProducesWarning() { + testAssertWarningsLines(false, "not marked with @configurable", + "package Test", + "function int.adjust(int x) returns int", + " return this + x", + "endpackage", + "package Test_config", + "@config function int.adjust(int x) returns int", + " return this * x", + "endpackage" + ); + } + + @Test + public void configExtensionOverloadsByExactSignature() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "@configurable function int.value(int x) returns int", + " return 1", + "@configurable function int.value(string x) returns int", + " return 2", + "init", + " int receiver = 0", + " if receiver.value(1) == 10 and receiver.value(\"x\") == 20", + " testSuccess()", + "endpackage", + "package Test_config", + "@config function int.value(string x) returns int", + " return 20", + "@config function int.value(int x) returns int", + " return 10", + "endpackage" + ); + } + + @Test + public void configOnlyOneExtensionOverload() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "@configurable function int.value(int x) returns int", + " return 1", + "@configurable function int.value(string x) returns int", + " return 2", + "init", + " int receiver = 0", + " if receiver.value(1) == 10 and receiver.value(\"x\") == 2", + " testSuccess()", + "endpackage", + "package Test_config", + "@config function int.value(int x) returns int", + " return 10", + "endpackage" + ); + } + + @Test + public void configExtensionReceiverMismatch() { + testAssertErrorsLines(false, "same signature in the configured package", + "package Test", + "function int.adjust(int x) returns int", + " return x", + "endpackage", + "package Test_config", + "@config function real.adjust(int x) returns int", + " return x", + "endpackage" + ); + } + + @Test + public void configExtensionParameterMismatch() { + testAssertErrorsLines(false, "same signature in the configured package", + "package Test", + "function int.adjust(int x) returns int", + " return x", + "endpackage", + "package Test_config", + "@config function int.adjust(real x) returns int", + " return this", + "endpackage" + ); + } + + @Test + public void configExtensionReturnMismatch() { + testAssertErrorsLines(false, "same signature in the configured package", + "package Test", + "function int.adjust(int x) returns int", + " return x", + "endpackage", + "package Test_config", + "@config function int.adjust(int x) returns real", + " return x", + "endpackage" + ); + } + + @Test + public void configExtensionVarargMismatch() { + testAssertErrorsLines(false, "same signature in the configured package", + "package Test", + "function int.adjust(vararg int xs)", + "endpackage", + "package Test_config", + "@config function int.adjust(int xs)", + "endpackage" + ); + } + + @Test + public void configGenericExtensionWithRenamedTypeParameter() { + test().testLua(true).executeProg().lines( + "package Test", + "native testSuccess()", + "@configurable function T.pick(T other) returns T", + " return this", + "init", + " int value = 1", + " if value.pick(2) == 2", + " testSuccess()", + "endpackage", + "package Test_config", + "@config function U.pick(U other) returns U", + " return other", + "endpackage" + ); + } + + @Test + public void configExtensionAppliesInsideImportingPackage() { + testAssertOkLines(true, + "package Extension", + "@configurable public function int.adjust() returns int", + " return 1", + "endpackage", + "package Caller", + "import Extension", + "public function callAdjust() returns int", + " int value = 0", + " return value.adjust()", + "endpackage", + "package Test", + "import Caller", + "native testSuccess()", + "init", + " if callAdjust() == 2", + " testSuccess()", + "endpackage", + "package Extension_config", + "@config public function int.adjust() returns int", + " return 2", + "endpackage" + ); + } + + @Test + public void configExtensionAppliesToCascade() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "public class Box", + " int value", + "@configurable function Box.setValue(int value)", + " this.value = value", + "init", + " let box = new Box()..setValue(3)", + " if box.value == 6", + " testSuccess()", + "endpackage", + "package Test_config", + "import Test", + "@config function Box.setValue(int value)", + " this.value = value * 2", + "endpackage" + ); + } + + @Test + public void configExtensionAppliesToOperator() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "public class Box", + "@configurable function Box.op_plus(int value) returns int", + " return value", + "init", + " if new Box() + 2 == 4", + " testSuccess()", + "endpackage", + "package Test_config", + "import Test", + "@config function Box.op_plus(int value) returns int", + " return value * 2", + "endpackage" + ); + } + + @Test + public void packageConfigDoesNotOverrideClassMethod() { + testAssertOkLines(true, + "package Test", + "native testSuccess()", + "public class Box", + " function value(int x) returns int", + " return 1", + "@configurable function value(int x) returns int", + " return 2", + "init", + " if new Box().value(0) == 1 and value(0) == 3", + " testSuccess()", + "endpackage", + "package Test_config", + "@config function value(int x) returns int", + " return 3", + "endpackage" + ); + } + + @Test + public void packageConfigCannotTargetClassMethod() { + testAssertErrorsLines(false, "same signature in the configured package", + "package Test", + "class Box", + " function value(int x) returns int", + " return 1", + "endpackage", + "package Test_config", + "@config function value(int x) returns int", + " return 2", + "endpackage" + ); + } + + @Test + public void configExtensionAllowsInitlaterDependencyCycle() { + testAssertOkLines(false, + "package Original", + "@configurable public function int.adjust() returns int", + " return 1", + "endpackage", + "package Dependency", + "import Original", + "public function dependencyValue() returns int", + " return 2", + "endpackage", + "package Original_config", + "import initlater Dependency", + "@config public function int.adjust() returns int", + " return dependencyValue() * 2", + "endpackage" + ); + } + @Test public void configVarCyclic() { testAssertOkLines(true,