diff --git a/de.peeeq.wurstscript/build.gradle b/de.peeeq.wurstscript/build.gradle index 3f0090988..f4fa668bb 100644 --- a/de.peeeq.wurstscript/build.gradle +++ b/de.peeeq.wurstscript/build.gradle @@ -109,7 +109,7 @@ dependencies { implementation 'com.github.albfernandez:juniversalchardet:2.4.0' implementation 'org.xerial:sqlite-jdbc:3.46.1.3' implementation 'com.github.inwc3:jmpq3:e28f6999c0' - implementation 'com.github.inwc3:wc3libs:ac41f780a5' + implementation 'com.github.inwc3:wc3libs:ac41f780a5e2dfc35310be4ed3267f23ab3fea44' implementation 'com.github.wurstscript:wurst-project-config:348fcd4ef5' implementation 'org.slf4j:slf4j-api:2.0.17' implementation 'ch.qos.logback:logback-classic:1.5.20' diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index 27771de5d..5f38f5f97 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -336,7 +336,9 @@ private CompilationUnit loadLibPackage(Function addCompil gui.sendError(new CompileError(new WPos("", null, 0, 0), "Could not find lib-package " + imp + ". Is your dependency present in _build/dependencies?")); return Ast.CompilationUnit(new CompilationUnitInfo(errorHandler), Ast.JassToplevelDeclarations(), Ast.WPackages()); } else { - return addCompilationUnit.apply(file); + CompilationUnit lib = addCompilationUnit.apply(file); + lib.getCuInfo().setLibrary(true); + return lib; } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java index dface9274..c7b21f278 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java @@ -644,6 +644,9 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents, WurstCompilerJassImpl c = getCompiler(gui); CompilationUnit cu = c.parse(filename.toString(), new StringReader(contents)); cu.getCuInfo().setFile(filename.toString()); + if (isUnderDependenciesFolder(filename)) { + cu.getCuInfo().setLibrary(true); + } updateModel(cu, gui); fileHashcodes.put(filename, newHash); if (reportErrors) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java index 3d5963e43..b5814731a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/CodeActionRequest.java @@ -1,10 +1,12 @@ package de.peeeq.wurstio.languageserver.requests; import de.peeeq.wurstio.languageserver.BufferManager; +import de.peeeq.wurstio.languageserver.Convert; import de.peeeq.wurstio.languageserver.ModelManager; import de.peeeq.wurstio.languageserver.WFile; import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.*; +import de.peeeq.wurstscript.attributes.AttrFuncDef; import de.peeeq.wurstscript.attributes.CompilationUnitInfo; import de.peeeq.wurstscript.attributes.names.DefLink; import de.peeeq.wurstscript.attributes.names.FuncLink; @@ -155,6 +157,21 @@ private List> makeWarningQuickFixes(Optional call.getFuncName().equals("toString") && call.getArgs().isEmpty()) + .ifPresent(call -> { + Range receiverRange = Convert.range(call.getLeft()); + Range callRange = Convert.range(call); + TextEdit edit = new TextEdit( + new Range(receiverRange.getEnd(), callRange.getEnd()), ""); + result.add(Either.forRight(makeQuickFix( + "Remove redundant .toString()", + workspaceEdit(filename.getUriString(), edit) + ))); + }); + } + if (hasDiagnosticMessage(msg -> msg.startsWith("The import ") && msg.endsWith(UNUSED_IMPORT_WARNING_SUFFIX))) { int line0 = e.flatMap(elem -> findNearest(elem, WImport.class)) .map(imp -> imp.attrSource().getLine() - 1) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java index 45d8148f3..1a385b49b 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrExprType.java @@ -285,6 +285,21 @@ public static WurstType calculate(final ExprBinary term) { if (leftType instanceof WurstTypeString && rightType instanceof WurstTypeString) { return WurstTypeString.instance(); } + if (term.attrFuncLink() != null) { + return handleOperatorOverloading(term); + } + if (AttrFuncDef.implicitToStringForConcatOperand(term, term.getLeft()) != null + || AttrFuncDef.implicitToStringForConcatOperand(term, term.getRight()) != null) { + return WurstTypeString.instance(); + } + String conversionError = AttrFuncDef.implicitToStringErrorForConcatOperand(term, term.getLeft()); + if (conversionError == null) { + conversionError = AttrFuncDef.implicitToStringErrorForConcatOperand(term, term.getRight()); + } + if (conversionError != null) { + term.addError(conversionError); + return WurstTypeUnknown.instance(); + } if (bothTypesRealOrInt(term)) { return caseMathOperation(term); } else { 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 8e3d6cbfa..74d8940d3 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 @@ -27,6 +27,8 @@ * this attribute find the variable definition for every variable reference */ public class AttrFuncDef { + public static final String REDUNDANT_TO_STRING_WARNING = + "Explicit .toString() is redundant in this string concatenation."; // TODO just use the attr function signature to get the def @@ -67,7 +69,126 @@ public static FuncLink calculate(final ExprFuncRef node) { public static @Nullable FuncLink calculate(ExprBinary node) { - return getExtensionFunction(node.getLeft(), node.getRight(), node.getOp()); + FuncLink overloadedOperator = getExtensionFunction(node.getLeft(), node.getRight(), node.getOp()); + if (overloadedOperator != null && matchesArguments(node, overloadedOperator, + Collections.singletonList(node.getRight().attrTyp()))) { + return overloadedOperator; + } + if (implicitToStringForConcatOperand(node, node.getLeft()) != null + || implicitToStringForConcatOperand(node, node.getRight()) != null) { + return null; + } + return overloadedOperator; + } + + /** Returns the implicit conversion for a non-string operand next to a string in a + expression. */ + public static @Nullable FuncLink implicitToStringForConcatOperand(ExprBinary concat, Expr operand) { + if (concat.getOp() != WurstOperator.PLUS || operand.attrTyp() instanceof WurstTypeString) { + return null; + } + Expr other = concat.getLeft() == operand ? concat.getRight() : concat.getLeft(); + if (!(other.attrTyp() instanceof WurstTypeString)) { + return null; + } + + return findToStringConversion(operand); + } + + /** Resolves the same zero-argument string conversion that an explicit operand.toString() call would use. */ + public static @Nullable FuncLink findToStringConversion(Expr operand) { + return resolveToStringConversion(operand).conversion; + } + + /** Whether replacing the right operand with the given type could expose a left-hand plus overload. */ + public static boolean hasApplicablePlusOverload(Expr leftOperand, WurstType rightType) { + List argumentTypes = Collections.singletonList(rightType); + for (FuncLink candidate : leftOperand.lookupMemberFuncs(leftOperand.attrTyp(), overloadingPlus)) { + if (matchesArguments(leftOperand, candidate, argumentTypes)) { + return true; + } + } + return false; + } + + /** Returns why an otherwise applicable implicit conversion cannot be selected. */ + public static @Nullable String implicitToStringErrorForConcatOperand(ExprBinary concat, Expr operand) { + if (concat.getOp() != WurstOperator.PLUS || operand.attrTyp() instanceof WurstTypeString) { + return null; + } + Expr other = concat.getLeft() == operand ? concat.getRight() : concat.getLeft(); + if (!(other.attrTyp() instanceof WurstTypeString)) { + return null; + } + return resolveToStringConversion(operand).error; + } + + private static ToStringConversionResolution resolveToStringConversion(Expr operand) { + Collection raw = NameResolution.lookupMemberFuncs( + operand, operand.attrTyp(), "toString", false); + List methods = new ArrayList<>(); + List extensions = new ArrayList<>(); + for (FuncLink candidate : raw) { + if (!isVisible(candidate) + || (candidate.getDef() instanceof FuncDef && ((FuncDef) candidate.getDef()).attrIsStatic())) { + continue; + } + FunctionSignature matched = FunctionSignature.fromNameLink(candidate) + .matchAgainstArgs(Collections.emptyList(), operand); + if (matched == null) { + continue; + } + if (isExtension(candidate)) { + if (!extensions.contains(candidate)) { + extensions.add(candidate); + } + } else { + if (!methods.contains(candidate)) { + methods.add(candidate); + } + } + } + + if (!methods.isEmpty()) { + return selectToStringConversion( + keepMostSpecificReceivers(methods, FuncLink::getReceiverType, operand), operand); + } + if (!extensions.isEmpty()) { + return selectToStringConversion( + keepMostSpecificReceivers(extensions, FuncLink::getReceiverType, operand), operand); + } + return new ToStringConversionResolution(null, null); + } + + private static ToStringConversionResolution selectToStringConversion(List candidates, Expr operand) { + if (candidates.size() != 1) { + return new ToStringConversionResolution(null, + "Call to function toString is ambiguous. Alternatives are:\n" + Utils.printAlternatives(candidates)); + } + FuncLink candidate = candidates.get(0); + FunctionSignature matched = FunctionSignature.fromNameLink(candidate) + .matchAgainstArgs(Collections.emptyList(), operand); + if (matched == null) { + return new ToStringConversionResolution(null, null); + } + if (matched.getMapping().hasUnboundTypeVars()) { + return new ToStringConversionResolution(null, + "Cannot infer type for type parameter " + matched.getMapping().printUnboundTypeVars()); + } + if (!matched.getReturnType().isSubtypeOf(WurstTypeString.instance(), operand)) { + return new ToStringConversionResolution(null, null); + } + return new ToStringConversionResolution( + candidate.withTypeArgBinding(operand, matched.getMapping()), null); + } + + private static class ToStringConversionResolution { + private final @Nullable FuncLink conversion; + private final @Nullable String error; + + private ToStringConversionResolution(@Nullable FuncLink conversion, @Nullable String error) { + this.conversion = conversion; + this.error = error; + } } public static @Nullable FuncLink calculate(final ExprMemberMethod node) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java index 4490c5764..303b4ee8a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/CompilationUnitInfo.java @@ -11,6 +11,7 @@ public class CompilationUnitInfo { private de.peeeq.wurstscript.attributes.ErrorHandler cuErrorHandler; private IndentationMode indentationMode = IndentationMode.spaces(4); private TriviaIndex triviaIndex = TriviaIndex.empty(); + private boolean library; public CompilationUnitInfo(ErrorHandler cuErrorHandler) { this.cuErrorHandler = cuErrorHandler; @@ -48,6 +49,14 @@ public void setTriviaIndex(TriviaIndex triviaIndex) { this.triviaIndex = triviaIndex == null ? TriviaIndex.empty() : triviaIndex; } + public boolean isLibrary() { + return library; + } + + public void setLibrary(boolean library) { + this.library = library; + } + public interface IndentationMode { static IndentationMode tabs() { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index 068a365e7..d9235248c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -172,7 +172,12 @@ public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) ImExpr left = e.getLeft().imTranslateExpr(t, f); ImExpr right = e.getRight().imTranslateExpr(t, f); WurstOperator op = e.getOp(); - if (e.attrFuncLink() != null) { + FuncLink overloadedOperator = e.attrFuncLink(); + if (op == WurstOperator.PLUS && overloadedOperator == null) { + left = wrapImplicitToString(e, e.getLeft(), left, t); + right = wrapImplicitToString(e, e.getRight(), right, t); + } + if (overloadedOperator != null) { // overloaded operator ImFunction calledFunc = t.getFuncFor(e.attrFuncDef()); return ImFunctionCall(e, calledFunc, ImTypeArguments(), ImExprs(left, right), false, CallType.NORMAL); @@ -199,6 +204,30 @@ public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) return ImOperatorCall(op, ImExprs(left, right)); } + private static ImExpr wrapImplicitToString(ExprBinary concat, Expr operand, ImExpr translated, + ImTranslator t) { + FuncLink toString = AttrFuncDef.implicitToStringForConcatOperand(concat, operand); + if (toString == null) { + return translated; + } + + FunctionDefinition calledFunc = toString.getDef().attrRealFuncDef(); + FunctionSignature signature = FunctionSignature.fromNameLink(toString); + if (calledFunc instanceof FuncDef + && !((FuncDef) calledFunc).attrIsStatic() + && operand.attrTyp().allowsDynamicDispatch()) { + ImMethod method = t.getMethodFor((FuncDef) calledFunc); + ImTypeArguments typeArguments = getFunctionCallTypeArguments( + t, signature, operand, method.getImplementation().getTypeVariables()); + return ImMethodCall(operand, method, typeArguments, translated, ImExprs(), false); + } + + ImFunction calledImFunc = t.getFuncFor(calledFunc); + ImTypeArguments typeArguments = getFunctionCallTypeArguments( + t, signature, operand, calledImFunc.getTypeVariables()); + return ImFunctionCall(operand, calledImFunc, typeArguments, ImExprs(translated), false, CallType.NORMAL); + } + public static ImExpr translateIntern(ExprUnary e, ImTranslator t, ImFunction f) { return ImOperatorCall(e.getOpU(), ImExprs(e.getRight().imTranslateExpr(t, 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 d5d4dc070..1b56d1ce5 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 @@ -2,12 +2,13 @@ import com.google.common.collect.*; import de.peeeq.wurstscript.WLogger; +import de.peeeq.wurstscript.WurstOperator; import de.peeeq.wurstscript.ast.*; +import de.peeeq.wurstscript.attributes.AttrFuncDef; 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.AttrFuncDef; import de.peeeq.wurstscript.attributes.names.DefLink; import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.attributes.names.NameLink; @@ -333,6 +334,16 @@ private void collectUsedPackages(Set used, Element root) { if (def != null) { used.add(def.getDef().attrNearestPackage()); } + if (def == null) { + FuncLink leftConversion = AttrFuncDef.implicitToStringForConcatOperand(binop, binop.getLeft()); + if (leftConversion != null) { + used.add(leftConversion.getDef().attrNearestPackage()); + } + FuncLink rightConversion = AttrFuncDef.implicitToStringForConcatOperand(binop, binop.getRight()); + if (rightConversion != null) { + used.add(rightConversion.getDef().attrNearestPackage()); + } + } } if (e instanceof Expr) { @@ -2080,12 +2091,58 @@ private void visit(ExprBinary expr) { FunctionSignature sig = FunctionSignature.fromNameLink(def); CallSignature callSig = new CallSignature(expr.getLeft(), Collections.singletonList(expr.getRight())); callSig.checkSignatureCompatibility(sig, "" + expr.getOp(), expr); + } else { + checkNameRefDeprecated(expr, AttrFuncDef.implicitToStringForConcatOperand(expr, expr.getLeft())); + checkNameRefDeprecated(expr, AttrFuncDef.implicitToStringForConcatOperand(expr, expr.getRight())); } } private void visit(ExprMemberMethod stmtCall) { // calculating the exprType should reveal all errors: stmtCall.attrTyp(); + if (stmtCall.attrCompilationUnit().getCuInfo().isLibrary()) { + return; + } + if (!(stmtCall instanceof ExprMemberMethodDot) + || !stmtCall.getFuncName().equals("toString") + || !stmtCall.getArgs().isEmpty() + || !(stmtCall.getParent() instanceof ExprBinary)) { + return; + } + ExprBinary concat = (ExprBinary) stmtCall.getParent(); + if (concat.getOp() != WurstOperator.PLUS) { + return; + } + Expr other = concat.getLeft() == stmtCall ? concat.getRight() : concat.getLeft(); + if (!(other.attrTyp() instanceof WurstTypeString)) { + return; + } + if (stmtCall.getLeft().attrTyp() instanceof WurstTypeString) { + // Removing the explicit call would leave an ordinary string operand, so the + // implicit conversion path would not invoke this potentially non-identity method. + return; + } + Expr replacement = stmtCall.getLeft(); + if (concat.getLeft() == stmtCall + && AttrFuncDef.hasApplicablePlusOverload(replacement, concat.getRight().attrTyp())) { + return; + } + if (concat.getRight() == stmtCall + && AttrFuncDef.hasApplicablePlusOverload(concat.getLeft(), replacement.attrTyp())) { + return; + } + FuncLink explicit = stmtCall.attrFuncLink(); + if (explicit != null + && stmtCall.getLeft() instanceof ExprThis + && explicit.getDef() == stmtCall.attrNearestFuncDef()) { + // Explicit recursive calls on this are lowered statically. Removing the call would + // make the implicit conversion dispatch virtually and could select an override. + return; + } + FuncLink inferred = AttrFuncDef.findToStringConversion(stmtCall.getLeft()); + if (explicit != null && explicit.equals(inferred)) { + stmtCall.addWarning(AttrFuncDef.REDUNDANT_TO_STRING_WARNING); + } } private void visit(ExprNewObject stmtCall) { diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ExpressionTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ExpressionTests.java index 300f936d8..6780129a7 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ExpressionTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ExpressionTests.java @@ -3,6 +3,8 @@ import de.peeeq.wurstio.UtilsIO; import org.testng.annotations.Test; +import static org.testng.Assert.assertFalse; + public class ExpressionTests extends WurstScriptTest { @Test @@ -10,6 +12,190 @@ public void plus() { assertOk("3 + 7 == 10"); } + @Test + public void inferToStringInStringConcatenation() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class Vec", + " function toString() returns string", + " return \"vec\"", + "class FancyVec extends Vec", + " override function toString() returns string", + " return \"fancy\"", + "class Box", + " function toString() returns string", + " return \"box\"", + "function int.toString() returns string", + " if this == 7", + " return \"seven\"", + " return \"other\"", + "init", + " Vec v = new FancyVec", + " let box = new Box", + " if \"before \" + v + \" after\" == \"before fancy after\" and v + \"!\" == \"fancy!\" and \"number \" + 7 == \"number seven\" and box + \"ed\" == \"boxed\"", + " testSuccess()" + ); + } + + @Test + public void redundantToStringInStringConcatenation() { + testAssertWarningsLines(false, "Explicit .toString() is redundant in this string concatenation.", + "package test", + "class Vec", + " function toString() returns string", + " return \"vec\"", + "init", + " let v = new Vec", + " let message = \"value: \" + v.toString()" + ); + } + + @Test + public void explicitToStringIsNotRedundantWhenRemovalExposesPlusOverload() { + CompilationResult result = test().setStopOnFirstError(false).lines( + "package test", + "class A", + " function toString() returns string", + " return \"converted\"", + " function op_plus(string suffix) returns string", + " return \"overloaded\" + suffix", + "init", + " let a = new A", + " let message = a.toString() + \"x\"" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("Explicit .toString() is redundant"))); + } + + @Test + public void rightHandToStringIsNotRedundantWhenRemovalExposesLeftPlusOverload() { + CompilationResult result = test().setStopOnFirstError(false).lines( + "package test", + "class A", + " function toString() returns string", + " return \"converted\"", + "function string.op_plus(A value) returns string", + " return \"overloaded\"", + "init", + " let a = new A", + " let message = \"prefix\" + a.toString()" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("Explicit .toString() is redundant"))); + } + + @Test + public void stringToStringIsNotReportedAsRedundant() { + CompilationResult result = test().setStopOnFirstError(false).lines( + "package test", + "function string.toString() returns string", + " return this + \"!\"", + "init", + " string value = \"value\"", + " let _message = \"prefix: \" + value.toString()" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("Explicit .toString() is redundant"))); + } + + @Test + public void recursiveSelfToStringIsNotReportedAsRedundant() { + CompilationResult result = test().setStopOnFirstError(false).lines( + "package test", + "class Base", + " function toString() returns string", + " return \"base: \" + this.toString()", + "class Child extends Base", + " override function toString() returns string", + " return \"child\"" + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("Explicit .toString() is redundant"))); + } + + @Test + public void inferredToStringCountsAsImportUsage() { + CompilationResult result = test().setStopOnFirstError(false).compilationUnits( + compilationUnit("Conversions.wurst", + "package Conversions", + "public function int.toString() returns string", + " return \"converted\""), + compilationUnit("Test.wurst", + "package Test", + "import Conversions", + "init", + " let message = \"value: \" + 1") + ); + + assertFalse(result.getGui().getWarningList().stream() + .anyMatch(w -> w.getMessage().contains("The import Conversions is never used"))); + } + + @Test + public void inferredToStringRejectsUninferredTypeParameters() { + testAssertErrorsLines(false, "Cannot infer type for type parameter T", + "package test", + "class C", + " function toString() returns string", + " return \"c\"", + "init", + " let message = \"value: \" + new C" + ); + } + + @Test + public void inferredToStringRejectsAmbiguousExtensions() { + test().expectError("Call to function toString is ambiguous").compilationUnits( + compilationUnit("First.wurst", + "package First", + "public function int.toString() returns string", + " return \"first\""), + compilationUnit("Second.wurst", + "package Second", + "public function int.toString() returns string", + " return \"second\""), + compilationUnit("Test.wurst", + "package Test", + "import First", + "import Second", + "init", + " let message = \"value: \" + 1") + ); + } + + @Test + public void inferredToStringPreservesMemberPrecedenceOverExtension() { + testAssertErrorsLines(false, "No operator overloading function for operator + was found", + "package test", + "class C", + " function toString() returns int", + " return 1", + "function C.toString() returns string", + " return \"extension\"", + "init", + " let message = \"value: \" + new C" + ); + } + + @Test + public void inferredToStringReportsDeprecation() { + testAssertWarningsLines(false, " is deprecated. use explicit formatting", + "package test", + "@annotation function annotation()", + "@annotation function deprecated(string _message)", + "class A", + "@deprecated(\"use explicit formatting\") function A.toString() returns string", + " return \"a\"", + "init", + " let _message = \"value: \" + new A" + ); + } + @Test public void real1() { assertOk(".3 + .7 == 1."); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java index f1633079b..788729cec 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/LspNativeFeaturesTests.java @@ -201,6 +201,46 @@ public void codeActionCanReplaceVarWithLetForConstantLocal() throws IOException assertEquals(edit.getNewText(), "let"); } + @Test + public void codeActionCanRemoveRedundantToStringFromConcatenation() throws IOException { + CompletionTestData data = input( + "package test", + "class Vec", + " function toString() returns string", + " return \"vec\"", + "init", + " let v = new Vec", + " let message = \"value: \" + v.toStr|ing()", + "endpackage" + ); + TestContext ctx = createContext(data, data.buffer); + + CodeActionParams params = new CodeActionParams(); + params.setTextDocument(new TextDocumentIdentifier(ctx.uri)); + params.setRange(new Range(new Position(data.line, data.column), new Position(data.line, data.column))); + Diagnostic d = new Diagnostic(); + d.setRange(params.getRange()); + d.setMessage("Explicit .toString() is redundant in this string concatenation."); + params.setContext(new CodeActionContext(Collections.singletonList(d))); + + List codeActions = new CodeActionRequest(params, ctx.bufferManager).execute(ctx.modelManager).stream() + .filter(Either::isRight) + .map(Either::getRight) + .collect(Collectors.toList()); + CodeAction fix = codeActions.stream() + .filter(a -> "Remove redundant .toString()".equals(a.getTitle())) + .findFirst() + .orElseThrow(() -> new AssertionError("expected toString removal quickfix, got: " + + codeActions.stream().map(CodeAction::getTitle).collect(Collectors.toList()))); + + TextEdit edit = allTextEdits(fix.getEdit()).get(0); + String sourceLine = data.buffer.split("\\r?\\n", -1)[data.line]; + int suffixStart = sourceLine.indexOf(".toString()"); + assertEquals(edit.getRange().getStart(), new Position(data.line, suffixStart)); + assertEquals(edit.getRange().getEnd(), new Position(data.line, suffixStart + ".toString()".length())); + assertEquals(edit.getNewText(), ""); + } + @Test public void codeActionCanRemoveUnusedImport() throws IOException { CompletionTestData data = input( diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java index 671df8987..6e67ab622 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/ModelManagerTests.java @@ -36,6 +36,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; public class ModelManagerTests { @@ -1110,6 +1111,18 @@ public void dependencySyncHandlesReplaceMoveRenameDelete() throws Exception { manager.buildProject(); assertEquals(errors.get(fileMain), "", "baseline build with depA should be clean"); assertNotNull(manager.getCompilationUnit(depAFile), "depA CU should be loaded"); + assertTrue(manager.getCompilationUnit(depAFile).getCuInfo().isLibrary(), + "dependency CU should initially be marked as a library"); + + writeFile(depAFile, string( + "package DummyDamage", + "public function foo()", + "public function changedDependency()" + )); + ModelManager.Changes changes = manager.syncDependencyCompilationUnits(); + manager.reconcile(changes); + assertTrue(manager.getCompilationUnit(depAFile).getCuInfo().isLibrary(), + "reparsed dependency CU should remain marked as a library"); // replace: depA is removed and depB provides the same package depAFile.getFile().delete(); @@ -1117,7 +1130,7 @@ public void dependencySyncHandlesReplaceMoveRenameDelete() throws Exception { "package DummyDamage", "public function foo()" )); - ModelManager.Changes changes = manager.syncDependencyCompilationUnits(); + changes = manager.syncDependencyCompilationUnits(); manager.reconcile(changes); assertEquals(manager.getCompilationUnit(depAFile), null, "removed dependency CU must be dropped from model"); assertNotNull(manager.getCompilationUnit(depBFile), "replacement dependency CU should be loaded"); diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OpOverloading.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OpOverloading.java index 0c27bec6b..8b568b3d6 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OpOverloading.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OpOverloading.java @@ -4,6 +4,29 @@ public class OpOverloading extends WurstScriptTest { + @Test + public void plusOverloadTakesPrecedenceOverImplicitToString() { + test().testLua(true).luaOnly(false).executeProg().lines( + "package test", + "native testSuccess()", + "class A", + " function toString() returns string", + " return \"converted\"", + " function op_plus(string suffix) returns string", + " return \"overloaded\" + suffix", + "class B", + " function toString() returns string", + " return \"fallback\"", + " function op_plus(int value) returns string", + " return \"wrong overload\"", + "init", + " let a = new A", + " let b = new B", + " if a + \"x\" == \"overloadedx\" and b + \"x\" == \"fallbackx\"", + " testSuccess()" + ); + } + @Test public void testOverloading1() {