diff --git a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 index e3681dec8..4c0b013b3 100644 --- a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 +++ b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 @@ -398,7 +398,7 @@ argumentList: ; exprFunctionCall: - funcName=ID typeArgs argumentList + funcName=(ID|THIS) typeArgs argumentList ; exprNewObject:'new' className=ID typeArgs argumentList?; 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 e653e04fa..b28e21b90 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 @@ -166,6 +166,9 @@ public static FuncLink calculate(final ExprFuncRef node) { public static @Nullable FuncLink calculate(final ExprFunctionCall node) { + if (isConstructorThisCall(node)) { + return null; + } FuncLink result = searchFunction(node.getFuncName(), node, argumentTypes(node)); if (result == null) { @@ -181,6 +184,20 @@ public static FuncLink calculate(final ExprFuncRef node) { return result; } + private static boolean isConstructorThisCall(ExprFunctionCall node) { + if (!node.getFuncName().equals("this")) { + return false; + } + Element current = node; + while (current != null) { + if (current instanceof ConstructorDef) { + return true; + } + current = current.getParent(); + } + return false; + } + private static @Nullable FuncLink getExtensionFunction(Expr left, Expr right, WurstOperator op) { String funcName = op.getOverloadingFuncName(); if (funcName == null || nativeOperator(op, left.attrTyp(), right.attrTyp(), left)) { 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 880fd82b7..58572a791 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 @@ -64,6 +64,9 @@ private static FunctionSignature filterSigs( Collection sigs, List argTypes, StmtCall location) { if (sigs.isEmpty()) { + if (location instanceof ExprFunctionCall && isConstructorThisCall((ExprFunctionCall) location)) { + return FunctionSignature.empty; + } if (!isInitTrigFunc(location)) { if (location instanceof ExprMemberMethodDot) { ExprMemberMethodDot emmd = (ExprMemberMethodDot) location; @@ -117,6 +120,20 @@ private static FunctionSignature filterSigs( return candidates.get(0); } + private static boolean isConstructorThisCall(ExprFunctionCall call) { + if (!call.getFuncName().equals("this")) { + return false; + } + Element current = call; + while (current != null) { + if (current instanceof ConstructorDef) { + return true; + } + current = current.getParent(); + } + return false; + } + private static List filterByIfNotDefinedAnnotation(List candidates) { List list = new ArrayList<>(); for (FunctionSignature sig : candidates) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/OverloadingResolver.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/OverloadingResolver.java index ea8f5ce4c..6b96df6be 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/OverloadingResolver.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/OverloadingResolver.java @@ -196,5 +196,35 @@ void handleError(List hints) { }.resolve(constructors, node).orElse(null); } + public static @Nullable ConstructorDef resolveThisCall(List constructors, final FunctionCall node) { + return new OverloadingResolver() { + + @Override + int getParameterCount(ConstructorDef f) { + return f.getParameters().size(); + } + + @Override + WurstType getParameterType(ConstructorDef f, int i) { + return f.getParameters().get(i).attrTyp(); + } + + @Override + int getArgumentCount(FunctionCall c) { + return c.getArgs().size(); + } + + @Override + WurstType getArgumentType(FunctionCall c, int i) { + return c.getArgs().get(i).attrTyp(); + } + + @Override + void handleError(List hints) { + node.addError("No suitable constructor found. \n" + Utils.join(hints, ", \n")); + } + }.resolve(constructors, node).orElse(null); + } + } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/PossibleFuncDefs.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/PossibleFuncDefs.java index 4365ccee7..21218a267 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/PossibleFuncDefs.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/PossibleFuncDefs.java @@ -50,9 +50,26 @@ public static ImmutableCollection calculate(final ExprMemberMethod nod } public static ImmutableCollection calculate(final ExprFunctionCall node) { + if (isConstructorThisCall(node)) { + return ImmutableList.of(); + } return searchFunction(node.getFuncName(), node); } + private static boolean isConstructorThisCall(ExprFunctionCall node) { + if (!node.getFuncName().equals("this")) { + return false; + } + Element current = node; + while (current != null) { + if (current instanceof ConstructorDef) { + return true; + } + current = current.getParent(); + } + return false; + } + private static ImmutableCollection getExtensionFunction(Expr left, Expr right, WurstOperator op) { String funcName = op.getOverloadingFuncName(); if (funcName == null || nativeOperator(left.attrTyp(), right.attrTyp(), left)) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/gui/WurstGui.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/gui/WurstGui.java index 209e93715..57c9df68d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/gui/WurstGui.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/gui/WurstGui.java @@ -21,9 +21,22 @@ public abstract class WurstGui { public abstract void showInfoMessage(String message); public void sendError(CompileError err) { + if (shouldSuppressWarning(err)) { + return; + } errors.add(err); } + private boolean shouldSuppressWarning(CompileError err) { + if (err.getErrorType() != ErrorType.WARNING) { + return false; + } + String file = err.getSource().getFile(); + String normalized = file.replace('\\', '/').toLowerCase(); + return normalized.contains("/_build/dependencies/") + || normalized.startsWith("_build/dependencies/"); + } + public void clearErrors() { errors.clear(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java index 40dd1a8c8..f708c5285 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ClassTranslator.java @@ -2,6 +2,7 @@ import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.attributes.OverloadingResolver; import de.peeeq.wurstscript.jassIm.Element.DefaultVisitor; import de.peeeq.wurstscript.jassIm.*; import de.peeeq.wurstscript.types.*; @@ -382,36 +383,75 @@ private void createConstructFunc(ConstructorDef constr) { ConstructorDef trace = constr; ImFunction f = translator.getConstructFunc(constr); ImVar thisVar = translator.getThisVar(constr); - ConstructorDef superConstr = constr.attrSuperConstructor(); - if (superConstr != null) { - // call super constructor - ImFunction superConstrFunc = translator.getConstructFunc(superConstr); - ImExprs arguments = ImExprs(ImVarAccess(thisVar)); - for (Expr a : superArgs(constr)) { - arguments.add(a.imTranslateExpr(translator, f)); + int firstRelevantIndex = firstRelevantStatementIndex(constr); + ExprFunctionCall thisCall = null; + if (firstRelevantIndex >= 0 && constr.getBody().get(firstRelevantIndex) instanceof ExprFunctionCall) { + ExprFunctionCall first = (ExprFunctionCall) constr.getBody().get(firstRelevantIndex); + if (first.getFuncName().equals("this")) { + thisCall = first; } - ImTypeArguments typeArgs = ImTypeArguments(); - ClassDef classDef = constr.attrNearestClassDef(); - assert classDef != null; - WurstType extendedType = classDef.getExtendedClass().attrTyp(); - if (extendedType instanceof WurstTypeClass) { - WurstTypeClass extendedTypeC = (WurstTypeClass) extendedType; - for (WurstTypeBoundTypeParam bt : extendedTypeC.getTypeParameters()) { - if (bt.isTemplateTypeParameter()) { - typeArgs.add(bt.imTranslateToTypeArgument(translator)); + } + int bodyStartIndex = 0; + if (thisCall != null) { + ConstructorDef calledConstr = OverloadingResolver.resolveThisCall( + constr.attrNearestClassOrModule().getConstructors(), + thisCall + ); + if (calledConstr != null && calledConstr != constr) { + ImFunction calledConstrFunc = translator.getConstructFunc(calledConstr); + ImExprs arguments = ImExprs(ImVarAccess(thisVar)); + for (Expr a : thisCall.getArgs()) { + arguments.add(a.imTranslateExpr(translator, f)); + } + f.getBody().add(ImFunctionCall(trace, calledConstrFunc, classTypeArgs(), arguments, false, CallType.NORMAL)); + bodyStartIndex = firstRelevantIndex + 1; + } + } else { + ConstructorDef superConstr = constr.attrSuperConstructor(); + if (superConstr != null) { + // call super constructor + ImFunction superConstrFunc = translator.getConstructFunc(superConstr); + ImExprs arguments = ImExprs(ImVarAccess(thisVar)); + for (Expr a : superArgs(constr)) { + arguments.add(a.imTranslateExpr(translator, f)); + } + ImTypeArguments typeArgs = ImTypeArguments(); + ClassDef classDef = constr.attrNearestClassDef(); + assert classDef != null; + WurstType extendedType = classDef.getExtendedClass().attrTyp(); + if (extendedType instanceof WurstTypeClass) { + WurstTypeClass extendedTypeC = (WurstTypeClass) extendedType; + for (WurstTypeBoundTypeParam bt : extendedTypeC.getTypeParameters()) { + if (bt.isTemplateTypeParameter()) { + typeArgs.add(bt.imTranslateToTypeArgument(translator)); + } } } + f.getBody().add(ImFunctionCall(trace, superConstrFunc, typeArgs, arguments, false, CallType.NORMAL)); } - f.getBody().add(ImFunctionCall(trace, superConstrFunc, typeArgs, arguments, false, CallType.NORMAL)); + // call classInitFunc: + f.getBody().add(ImFunctionCall(trace, classInitFunc, classTypeArgs(), JassIm.ImExprs(JassIm.ImVarAccess(thisVar)), false, CallType.NORMAL)); } - // call classInitFunc: + // constructor user code + f.getBody().addAll(translator.translateStatements(f, constr.getBody().subList(bodyStartIndex, constr.getBody().size()))); + } + + private int firstRelevantStatementIndex(ConstructorDef constr) { + for (int i = 0; i < constr.getBody().size(); i++) { + WStatement s = constr.getBody().get(i); + if (!(s instanceof StartFunctionStatement) && !(s instanceof EndFunctionStatement)) { + return i; + } + } + return -1; + } + + private ImTypeArguments classTypeArgs() { ImTypeArguments typeArguments = JassIm.ImTypeArguments(); for (ImTypeVar tv : imClass.getTypeVariables()) { typeArguments.add(JassIm.ImTypeArgument(JassIm.ImTypeVarRef(tv), Collections.emptyMap())); } - f.getBody().add(ImFunctionCall(trace, classInitFunc, typeArguments, JassIm.ImExprs(JassIm.ImVarAccess(thisVar)), false, CallType.NORMAL)); - // constructor user code - f.getBody().addAll(translator.translateStatements(f, constr.getBody())); + return typeArguments; } private void translateClassInitFunc() { 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 7facfae8b..7a4cff015 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 @@ -6,6 +6,7 @@ import de.peeeq.wurstscript.attributes.CofigOverridePackages; import de.peeeq.wurstscript.attributes.CompileError; import de.peeeq.wurstscript.attributes.ImplicitFuncs; +import de.peeeq.wurstscript.attributes.OverloadingResolver; import de.peeeq.wurstscript.attributes.names.DefLink; import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.attributes.names.NameLink; @@ -53,6 +54,7 @@ private enum Phase { LIGHT, HEAVY } private @Nullable Element lastElement = null; private final HashSet trveWrapperFuncs = new HashSet<>(); private final HashMap> wrapperCalls = new HashMap<>(); + private final Map> classVarInitOrderCache = new HashMap<>(); public WurstValidator(WurstModel root) { this.prog = root; @@ -385,6 +387,7 @@ private void check(Element e) { checkPackageName((CompilationUnit) e); if (e instanceof ConstructorDef) { checkConstructor((ConstructorDef) e); + checkThisConstructorCall((ConstructorDef) e); checkConstructorSuperCall((ConstructorDef) e); } if (e instanceof ExprBinary) @@ -1613,6 +1616,9 @@ private void checkUninitializedVars(FunctionLike f) { private void checkCall(StmtCall call) { + if (call instanceof ExprFunctionCall && isConstructorThisCall((ExprFunctionCall) call)) { + return; + } String funcName; if (call instanceof FunctionCall) { FunctionCall fcall = (FunctionCall) call; @@ -1725,6 +1731,9 @@ private void checkAnnotation(Annotation a) { private void visit(ExprFunctionCall stmtCall) { String funcName = stmtCall.getFuncName(); + if (isConstructorThisCall(stmtCall)) { + return; + } // calculating the exprType should reveal most errors: stmtCall.attrTyp(); @@ -2051,6 +2060,9 @@ public VariableBinding case_TypeExprSimple(TypeExprSimple e) { @Override public VariableBinding case_ExprFunctionCall(ExprFunctionCall e) { + if (isConstructorThisCall(e)) { + return null; + } return e.attrTyp().getTypeArgBinding(); } @@ -2146,6 +2158,9 @@ public static boolean isTypeParamNewGeneric(TypeParamDef tp) { } private void checkFuncRef(FuncRef ref) { + if (isConstructorThisCall(ref)) { + return; + } if (ref.getFuncName().isEmpty()) { ref.addError("Missing function name."); } @@ -2417,12 +2432,17 @@ private void checkConstructor(ConstructorDef d) { d.getParameters().addError("Module constructors must not have parameters."); } } + FunctionCall thisCall = getFirstThisConstructorCall(d); StructureDef s = d.attrNearestStructureDef(); if (s instanceof ClassDef) { ClassDef c = (ClassDef) s; WurstTypeClass ct = c.attrTypC(); WurstTypeClass extendedClass = ct.extendedClass(); if (extendedClass != null) { + if (thisCall != null) { + // Delegating constructors call another constructor which then handles super(). + return; + } // Use the *bound* super-constructor signature ConstructorDef sc = d.attrSuperConstructor(); if (sc == null) { @@ -2862,6 +2882,59 @@ private void checkVarDef(VarDef v) { v.addError("Initial value of variable " + v.getName() + " is 'null'. Specify a concrete type."); } + if (v instanceof GlobalVarDef) { + checkClassMemberInitializerOrder((GlobalVarDef) v); + } + + } + + private void checkClassMemberInitializerOrder(GlobalVarDef v) { + if (!v.attrIsDynamicClassMember()) { + return; + } + if (!(v.getInitialExpr() instanceof Expr)) { + return; + } + ClassDef owner = v.attrNearestClassDef(); + if (owner == null) { + return; + } + Map order = classVarInitOrder(owner); + Integer currentPos = order.get(v); + if (currentPos == null) { + return; + } + Expr initExpr = (Expr) v.getInitialExpr(); + for (NameDef used : initExpr.attrReadVariables()) { + if (!(used instanceof GlobalVarDef)) { + continue; + } + GlobalVarDef usedVar = (GlobalVarDef) used; + if (usedVar == v || !usedVar.attrIsDynamicClassMember()) { + continue; + } + if (usedVar.attrNearestClassDef() != owner) { + continue; + } + Integer usedPos = order.get(usedVar); + if (usedPos != null && usedPos > currentPos) { + v.addError("Class variable <" + usedVar.getName() + "> is used before it is initialized."); + return; + } + } + } + + private Map classVarInitOrder(ClassDef classDef) { + return classVarInitOrderCache.computeIfAbsent(classDef, cd -> { + Map order = new IdentityHashMap<>(); + int index = 0; + for (GlobalVarDef var : cd.getVars()) { + if (var.attrIsDynamicClassMember()) { + order.put(var, index++); + } + } + return order; + }); } private void checkLocalShadowing(LocalVarDef v) { @@ -2886,6 +2959,78 @@ private void checkConstructorSuperCall(ConstructorDef c) { } } + private void checkThisConstructorCall(ConstructorDef c) { + FunctionCall firstThisCall = getFirstThisConstructorCall(c); + int firstRelevantIndex = firstRelevantStatementIndex(c); + for (int i = 0; i < c.getBody().size(); i++) { + WStatement s = c.getBody().get(i); + if (s instanceof FunctionCall) { + FunctionCall call = (FunctionCall) s; + if (isConstructorThisCall(call) && i != firstRelevantIndex) { + call.addError("Constructor call this(...) must be the first statement."); + } + } + } + if (firstThisCall == null) { + return; + } + if (c.getSuperConstructorCall() instanceof SomeSuperConstructorCall) { + c.addError("Cannot call super(...) and this(...) in the same constructor."); + return; + } + ClassOrModule owner = c.attrNearestClassOrModule(); + if (owner == null) { + return; + } + ConstructorDef target = OverloadingResolver.resolveThisCall(owner.getConstructors(), firstThisCall); + if (target == c) { + firstThisCall.addError("Constructor cannot call itself using this(...)."); + } + } + + private @Nullable FunctionCall getFirstThisConstructorCall(ConstructorDef c) { + int i = firstRelevantStatementIndex(c); + if (i >= 0 && c.getBody().get(i) instanceof FunctionCall) { + FunctionCall call = (FunctionCall) c.getBody().get(i); + if (isConstructorThisCall(call)) { + return call; + } + } + return null; + } + + private int firstRelevantStatementIndex(ConstructorDef c) { + for (int i = 0; i < c.getBody().size(); i++) { + WStatement s = c.getBody().get(i); + if (!(s instanceof StartFunctionStatement) && !(s instanceof EndFunctionStatement)) { + return i; + } + } + return -1; + } + + private boolean isConstructorThisCall(FunctionCall call) { + return call.getFuncName().equals("this") && nearestEnclosingConstructor(call) != null; + } + + private boolean isConstructorThisCall(FuncRef ref) { + if (ref instanceof FunctionCall) { + return isConstructorThisCall((FunctionCall) ref); + } + return false; + } + + private @Nullable ConstructorDef nearestEnclosingConstructor(Element e) { + Element current = e; + while (current != null) { + if (current instanceof ConstructorDef) { + return (ConstructorDef) current; + } + current = current.getParent(); + } + return null; + } + private void checkParameter(WParameter param) { if (param.attrTyp() instanceof WurstTypeArray) { param.addError("Cannot use arrays as parameters."); 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 fc376fbbb..4f72ec463 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 @@ -162,6 +162,53 @@ public void test_init_order_globals_warning_jass() { "endpackage"); } + @Test + public void classVarInitOrderShouldError_770() { + testAssertErrorsLines(false, "used before it is initialized", + "package test", + "class B", + " var i = 0", + " function get() returns int", + " return i", + "class A", + " private var foo = b.get()", + " private var b = new B()", + "endpackage"); + } + + @Test + public void dependencyDiagnosticsAreMuted() { + CompilationResult res = test() + .setStopOnFirstError(false) + .withCu(compilationUnit("_build/dependencies/dep/Dep.wurst", + "package dep", + "init", + " if true", + "endpackage")) + .run(); + + Assert.assertTrue(res.getGui().getErrorList().isEmpty(), "Expected no errors."); + Assert.assertTrue(res.getGui().getWarningList().isEmpty(), + "Expected no warnings from _build/dependencies sources."); + } + + @Test + public void projectDiagnosticsStayVisible() { + CompilationResult res = test() + .setStopOnFirstError(false) + .withCu(compilationUnit("wurst/Local.wurst", + "package local", + "init", + " if true", + "endpackage")) + .run(); + + Assert.assertTrue(res.getGui().getErrorList().isEmpty(), "Expected no errors."); + Assert.assertTrue(res.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("empty then-block")), + "Expected warning for project sources."); + } + @Test public void test_for_from() { 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 5650f2376..e1da224f8 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 @@ -968,6 +968,216 @@ public void constructor_overloading() { ); } + @Test + public void constructor_chaining_basic() throws IOException { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class A", + " int x = 0", + " construct()", + " this(3)", + " construct(int i)", + " x = i", + " init", + " let a = new A()", + " if a.x == 3", + " testSuccess()", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "ClassesTests_constructor_chaining_basic_no_opts.j"); + String jass = Files.readString(output.toPath(), StandardCharsets.UTF_8); + assertTrue(jass.contains("function construct_A takes integer this returns nothing"), + "Expected delegating constructor function in generated jass."); + assertTrue(jass.contains("function construct_A2 takes integer this, integer i returns nothing"), + "Expected target constructor overload in generated jass."); + assertTrue(jass.contains("call construct_A2(this, 3)"), + "Expected delegating constructor to call target constructor."); + assertFalse(jass.contains("function construct_A takes integer this returns nothing\n\tcall A_init(this)"), + "Delegating constructor must not emit duplicate class init."); + assertTrue(countOccurrences(jass, "call A_init(this)") == 1, + "Expected class init to be emitted exactly once."); + } + + @Test + public void constructor_chaining_vararg() throws IOException { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class A", + " int x = 0", + " construct()", + " this(1, \"a\", \"b\")", + " construct(int i, vararg string xs)", + " x = i", + " for s in xs", + " x++", + " init", + " let a = new A()", + " if a.x == 3", + " testSuccess()", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "ClassesTests_constructor_chaining_vararg_no_opts.j"); + String jass = Files.readString(output.toPath(), StandardCharsets.UTF_8); + assertTrue(jass.contains("call construct_A2_2(this, 1, \"a\", \"b\")"), + "Expected delegating constructor to pass concrete vararg arguments."); + assertTrue(countOccurrences(jass, "call A_init(this)") == 1, + "Expected vararg constructor chain to initialize class exactly once."); + } + + @Test + public void constructor_chaining_must_be_first_statement() { + testAssertErrorsLines(false, "must be the first statement", + "package test", + " class A", + " construct()", + " skip", + " this(1)", + " construct(int i)", + " skip", + "endpackage" + ); + } + + @Test + public void constructor_chaining_self_call() { + testAssertErrorsLines(false, "cannot call itself", + "package test", + " class A", + " construct()", + " this()", + "endpackage" + ); + } + + @Test + public void constructor_chaining_and_super_conflict() { + testAssertErrorsLines(false, "Cannot call super(...) and this(...)", + "package test", + " class A", + " construct(int i)", + " skip", + " class B extends A", + " construct()", + " super(1)", + " this(2)", + " construct(int i)", + " super(i)", + "endpackage" + ); + } + + @Test + public void constructor_chaining_extends_without_direct_super_call() throws IOException { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class A", + " int x = 0", + " construct(int i)", + " x = i", + " class B extends A", + " construct()", + " this(7)", + " construct(int i)", + " super(i)", + " init", + " let b = new B()", + " if b.x == 7", + " testSuccess()", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "ClassesTests_constructor_chaining_extends_without_direct_super_call_no_opts.j"); + String jass = Files.readString(output.toPath(), StandardCharsets.UTF_8); + assertTrue(jass.contains("call construct_B2(this, 7)"), + "Expected delegating constructor in subclass to call target constructor."); + assertTrue(jass.contains("call construct_A(this, i)"), + "Expected target constructor to emit super constructor call."); + assertTrue(jass.contains("call B_init(this)"), + "Expected subclass init call in target constructor."); + assertFalse(jass.contains("function construct_B takes integer this returns nothing\n\tcall B_init(this)"), + "Delegating subclass constructor must not emit duplicate subclass init."); + assertTrue(countOccurrences(jass, "call B_init(this)") == 1, + "Expected subclass init to be emitted exactly once."); + } + + @Test + public void constructor_chaining_timing_and_sideeffects() throws IOException { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " int ticks = 0", + " function tick() returns int", + " ticks++", + " return ticks", + " class A", + " int p = tick()", + " int q = 0", + " construct()", + " this(7)", + " q = tick()", + " p += 100", + " construct(int i)", + " q = tick()", + " p += i", + " init", + " let a = new A()", + " if a.p == 108 and a.q == 3 and ticks == 3", + " testSuccess()", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "ClassesTests_constructor_chaining_timing_and_sideeffects_no_opts.j"); + String jass = Files.readString(output.toPath(), StandardCharsets.UTF_8); + assertTrue(countOccurrences(jass, "call A_init(this)") == 1, + "Expected class init to run exactly once in constructor chain."); + int idxDelegatingCall = jass.indexOf("call construct_A2(this, 7)"); + int idxDelegatingBody = jass.indexOf("set A_q[this] = tick()", idxDelegatingCall); + assertTrue(idxDelegatingCall >= 0 && idxDelegatingBody > idxDelegatingCall, + "Expected delegating constructor body to run after this(...) target call."); + } + + @Test + public void constructor_chaining_subclass_timing_and_order() throws IOException { + testAssertOkLines(true, + "package test", + " native testSuccess()", + " class A", + " int x = 0", + " construct(int i)", + " x = i * 10", + " class B extends A", + " int y = 0", + " construct()", + " this(2)", + " y += 1", + " x += 1", + " construct(int i)", + " super(i)", + " y += 10", + " x += 2", + " init", + " let b = new B()", + " if b.x == 23 and b.y == 11", + " testSuccess()", + "endpackage" + ); + + File output = new File(TEST_OUTPUT_PATH + "ClassesTests_constructor_chaining_subclass_timing_and_order_no_opts.j"); + String jass = Files.readString(output.toPath(), StandardCharsets.UTF_8); + assertTrue(countOccurrences(jass, "call B_init(this)") == 1, + "Expected subclass init to run exactly once in constructor chain."); + int idxSuperCall = jass.indexOf("call construct_A(this, i)"); + int idxBInitCall = jass.indexOf("call B_init(this)"); + int idxYMutation = jass.indexOf("set B_y[this] = B_y[this] + 10"); + assertTrue(idxSuperCall >= 0 && idxBInitCall > idxSuperCall && idxYMutation > idxBInitCall, + "Expected order super(...) -> B_init -> target constructor body in subclass constructor chain."); + } + @Test public void method_private() { testAssertErrorsLines(false, "foo is not visible here", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java index 8406bfd70..0f22679d2 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/StdLib.java @@ -23,7 +23,7 @@ public class StdLib { /** * version to use for the tests */ - private final static String version = "e6463189f754b8794e59ba9d4ac1a91977c8aaac"; + private final static String version = "85f9debf4f53207e1ffcc23ce2bf5e759a07ba06"; /** * flag so that initialization in only done once