diff --git a/Coder.Test/Languages/CallReceiverLiteralTests.cs b/Coder.Test/Languages/CallReceiverLiteralTests.cs new file mode 100644 index 0000000..9507d32 --- /dev/null +++ b/Coder.Test/Languages/CallReceiverLiteralTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using System.Linq; +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that a number literal used as a call's receiver keeps its grouping. +/// +/// +/// -2.5.abs() is -(2.5.abs()) in every target, because unary minus binds looser than +/// member access, and 5.toString() does not parse in JavaScript or Python. +/// +[TestClass] +public class CallReceiverLiteralTests +{ + /// + /// Every generator that writes a receiver in front of the callee. C passes it as an argument instead. + /// + private static readonly ILanguageGenerator[] Generators = + [ + new CSharpGenerator(), + new CppGenerator(), + new GoGenerator(), + new RustGenerator(), + new PythonGenerator(), + new JavaScriptGenerator(), + ]; + + /// + /// Tests that a negative literal receiver is parenthesised, so the call applies to the negative value. + /// + [TestMethod] + public void ANegativeLiteralReceiverIsParenthesised() + { + CallExpression call = new(Literal.DecimalValue(-2.5), "abs"); + + foreach (ILanguageGenerator generator in Generators) + { + string code = generator.Generate(call).Trim(); + + Assert.StartsWith("(-2.5", code, StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}"); + Assert.EndsWith(").abs()", code, StringComparison.Ordinal, $"{generator.GetType().Name} wrote {code}"); + } + } + + /// + /// Tests that an integer literal receiver is parenthesised, so the dot is not read as a decimal point. + /// + [TestMethod] + public void AnIntegerLiteralReceiverIsParenthesised() + { + CallExpression call = new(Literal.Number(5), "toString"); + + foreach (ILanguageGenerator generator in Generators.Where(g => g is JavaScriptGenerator or PythonGenerator)) + { + Assert.AreEqual("(5).toString()", generator.Generate(call).Trim(), generator.GetType().Name); + } + } + + /// + /// Tests that a receiver that is not a literal is still written bare. + /// + [TestMethod] + public void ANameReceiverIsWrittenBare() + { + CallExpression call = new(new VariableReference("value"), "abs"); + + foreach (ILanguageGenerator generator in Generators) + { + Assert.AreEqual("value.abs()", generator.Generate(call).Trim(), generator.GetType().Name); + } + } +} diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index 38ac6d7..b1218f2 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -558,6 +558,12 @@ protected void GenerateExpressionStatement(ExpressionStatement statement, CodeBl /// is written verbatim. Nothing here maps a function's name /// between languages, and nothing pretends to — see the node's own remarks for why. /// + /// + /// A number literal receiver is parenthesised. A negative one would otherwise negate the call's + /// result rather than its receiver, since unary minus binds looser than member access, so + /// -2.5.abs() is -(2.5.abs()); and an integer one would run the dot into the + /// literal, which JavaScript and Python read as a malformed number rather than a member call. + /// /// protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlocker code) { @@ -566,7 +572,20 @@ protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlock if (callExpr.Receiver is not null) { + bool parenthesise = IsNumberLiteral(callExpr.Receiver); + + if (parenthesise) + { + code.Write("("); + } + GenerateInternal(callExpr.Receiver, code); + + if (parenthesise) + { + code.Write(")"); + } + code.Write("."); } @@ -714,6 +733,17 @@ LiteralExpression floatLit when float.IsNegative(floatLit.Value) => '-', _ => null, }; + /// + /// Reports whether a node is a number literal. + /// + /// The node to inspect. + /// True for an integer or floating-point literal of any sign; otherwise false. + private static bool IsNumberLiteral(AstNode? node) => node is LiteralExpression + or LiteralExpression + or LiteralExpression + or LiteralExpression + or AstLeafNode; + /// /// Reports whether a node is one of the standard shapes a generator built on these helpers accepts. ///