Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions Coder.Test/Languages/CallReceiverLiteralTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests that a number literal used as a call's receiver keeps its grouping.
/// </summary>
/// <remarks>
/// <c>-2.5.abs()</c> is <c>-(2.5.abs())</c> in every target, because unary minus binds looser than
/// member access, and <c>5.toString()</c> does not parse in JavaScript or Python.
/// </remarks>
[TestClass]
public class CallReceiverLiteralTests
{
/// <summary>
/// Every generator that writes a receiver in front of the callee. C passes it as an argument instead.
/// </summary>
private static readonly ILanguageGenerator[] Generators =
[
new CSharpGenerator(),
new CppGenerator(),
new GoGenerator(),
new RustGenerator(),
new PythonGenerator(),
new JavaScriptGenerator(),
];

/// <summary>
/// Tests that a negative literal receiver is parenthesised, so the call applies to the negative value.
/// </summary>
[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}");
}
}

/// <summary>
/// Tests that an integer literal receiver is parenthesised, so the dot is not read as a decimal point.
/// </summary>
[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);
}
}

/// <summary>
/// Tests that a receiver that is not a literal is still written bare.
/// </summary>
[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);
}
}
}
30 changes: 30 additions & 0 deletions Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,12 @@
/// <see cref="CallExpression.Callee"/> is written verbatim. Nothing here maps a function's name
/// between languages, and nothing pretends to — see the node's own remarks for why.
/// </para>
/// <para>
/// 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
/// <c>-2.5.abs()</c> is <c>-(2.5.abs())</c>; 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.
/// </para>
/// </remarks>
protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlocker code)
{
Expand All @@ -566,7 +572,20 @@

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(".");
}

Expand Down Expand Up @@ -714,6 +733,17 @@
_ => null,
};

/// <summary>
/// Reports whether a node is a number literal.
/// </summary>
/// <param name="node">The node to inspect.</param>
/// <returns>True for an integer or floating-point literal of any sign; otherwise false.</returns>
private static bool IsNumberLiteral(AstNode? node) => node is LiteralExpression<int>
or LiteralExpression<double>
or LiteralExpression<float>
or LiteralExpression<long>
or AstLeafNode<int>;

/// <summary>
/// Reports whether a node is one of the standard shapes a generator built on these helpers accepts.
/// </summary>
Expand Down Expand Up @@ -896,7 +926,7 @@
string? origin = null;
long offset = 0;

foreach (EnumMember member in enumDecl.Members)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 929 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)
{
if (member.Value is not null)
{
Expand Down
Loading