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
81 changes: 81 additions & 0 deletions Coder.Test/Languages/UnfilledMemberInitialiserTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

using ktsu.Coder.Ast;
using ktsu.Coder.Languages;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests that a braced-list member initialiser with no value yet is left out rather than thrown on.
/// </summary>
/// <remarks>
/// <c>new MemberInitialiser("x")</c> is legal, and is what the graph editor holds between adding a
/// member initialiser and filling it in, so the preview generates it on every edit in between.
/// </remarks>
[TestClass]
public class UnfilledMemberInitialiserTests
{
/// <summary>
/// Tests that every generator writes a list holding an unfilled member, and writes its filled
/// sibling.
/// </summary>
/// <param name="language">The generator's language identifier.</param>
[TestMethod]
[DataRow("csharp")]
[DataRow("c")]
[DataRow("cpp")]
[DataRow("go")]
[DataRow("rust")]
[DataRow("python")]
[DataRow("javascript")]
public void UnfilledMember_IsLeftOut(string language)
{
ILanguageGenerator generator = Generators().Single(g => g.LanguageId == language);

ConstructionExpression point = new("Point");
point.Arguments.Add(new MemberInitialiser("unfilled"));
point.Arguments.Add(new MemberInitialiser("y", Literal.Number(2)));

string code = generator.Generate(new VariableDeclaration { Name = "p", Type = "Point", InitialValue = point });

Assert.DoesNotContain("unfilled", code, StringComparison.Ordinal, $"{language} wrote {code}");
Assert.Contains("y", code, StringComparison.Ordinal, $"{language} wrote {code}");
Assert.Contains("2", code, StringComparison.Ordinal, $"{language} wrote {code}");
}

/// <summary>
/// Tests that every generator writes a typeless list whose only element is unfilled.
/// </summary>
/// <param name="language">The generator's language identifier.</param>
[TestMethod]
[DataRow("csharp")]
[DataRow("c")]
[DataRow("cpp")]
[DataRow("go")]
[DataRow("rust")]
[DataRow("python")]
[DataRow("javascript")]
public void OnlyElementUnfilled_IsAnEmptyList(string language)
{
ILanguageGenerator generator = Generators().Single(g => g.LanguageId == language);

ConstructionExpression list = new();
list.Arguments.Add(new MemberInitialiser("unfilled"));

string code = generator.Generate(new VariableDeclaration { Name = "p", Type = "Point", InitialValue = list });

Assert.DoesNotContain("unfilled", code, StringComparison.Ordinal, $"{language} wrote {code}");
}

private static ILanguageGenerator[] Generators() =>
[
new CSharpGenerator(),
new CGenerator(),
new CppGenerator(),
new GoGenerator(),
new RustGenerator(),
new PythonGenerator(),
new JavaScriptGenerator(),
];
}
12 changes: 7 additions & 5 deletions Coder/Languages/CSharpGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@
/// The constants on <see cref="double"/>, which every file can name without a <c>using</c>.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "double.NaN" : value > 0 ? "double.PositiveInfinity" : "double.NegativeInfinity";

Check warning on line 307 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 307 in Coder/Languages/CSharpGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Emits a namespace and its members.
Expand Down Expand Up @@ -411,7 +411,9 @@
/// </remarks>
private void WriteBracedList(ConstructionExpression construction, CodeBlocker code)
{
if (construction.Arguments.Count == 0)
IReadOnlyList<AstNode> elements = WrittenElements(construction);

if (elements.Count == 0)
{
code.Write("{ }");
return;
Expand All @@ -425,7 +427,7 @@
code.WriteLine("{");
code.Indent();

foreach (AstNode argument in construction.Arguments)
foreach (AstNode argument in elements)
{
WriteListElement(argument, code);
code.WriteLine(",");
Expand All @@ -438,14 +440,14 @@

code.Write("{ ");

for (int index = 0; index < construction.Arguments.Count; index++)
for (int index = 0; index < elements.Count; index++)
{
if (index > 0)
{
code.Write(", ");
}

WriteListElement(construction.Arguments[index], code);
WriteListElement(elements[index], code);
}

code.Write(" }");
Expand All @@ -461,7 +463,7 @@
if (argument is MemberInitialiser designated)
{
code.Write($"{designated.Name} = ");
GenerateInternal(designated.Value ?? new VariableReference(string.Empty), code);
GenerateInternal(designated.Value!, code);
return;
}

Expand Down
10 changes: 6 additions & 4 deletions Coder/Languages/JavaScriptGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,21 +225,23 @@ private void WriteLiteral(ConstructionExpression construction, CodeBlocker code,
{
code.Write(named ? "{ " : "[");

for (int index = 0; index < construction.Arguments.Count; index++)
IReadOnlyList<AstNode> elements = WrittenElements(construction);

for (int index = 0; index < elements.Count; index++)
{
if (index > 0)
{
code.Write(", ");
}

if (construction.Arguments[index] is MemberInitialiser member)
if (elements[index] is MemberInitialiser member)
{
code.Write($"{member.Name}: ");
GenerateInternal(member.Value ?? new VariableReference(string.Empty), code);
GenerateInternal(member.Value!, code);
continue;
}

GenerateInternal(construction.Arguments[index], code);
GenerateInternal(elements[index], code);
}

code.Write(named ? " }" : "]");
Expand Down
17 changes: 17 additions & 0 deletions Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
}
}

foreach (object item in expected)

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!emittedPreambleItems!.Contains(item))
{
Expand All @@ -387,7 +387,7 @@
/// Read off the nodes' public properties rather than a list of node types, so a node added to the
/// AST is walked without anybody remembering to add it here.
/// </remarks>
protected static IEnumerable<AstNode> SelfAndDescendants(AstNode root)

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.
{
HashSet<AstNode> visited = new(ReferenceEqualityComparer.Instance);
Stack<AstNode> pending = new();
Expand Down Expand Up @@ -436,6 +436,23 @@
}
}

/// <summary>
/// Gets the elements of a list that are written, leaving out a member named with no value yet.
/// </summary>
/// <param name="construction">The list.</param>
/// <returns>Its elements, without any <see cref="MemberInitialiser"/> whose value is null.</returns>
/// <remarks>
/// A null value is legal, and is what the graph editor holds between adding a member initialiser
/// and filling it in. There is nothing to write for it, and leaving the member out is what every
/// target already does with a member the list does not name. The shape of the list is still
/// decided by every argument, so a list whose only element is unfilled stays a designated one.
/// </remarks>
protected static IReadOnlyList<AstNode> WrittenElements(ConstructionExpression construction)
{
Ensure.NotNull(construction);
return [.. construction.Arguments.Where(argument => argument is not MemberInitialiser { Value: null })];
}

/// <summary>
/// Reports whether a file holds a double literal that is not a finite number.
/// </summary>
Expand Down Expand Up @@ -1128,7 +1145,7 @@
/// the generator adds through <see cref="RequiredImports"/>.
/// </remarks>
protected virtual string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity";

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

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

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Formats a single-precision value so that every target reads it as floating-point.
Expand Down
20 changes: 12 additions & 8 deletions Coder/Languages/PythonGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
/// <c>float("nan")</c> does not.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "float(\"nan\")" : value > 0 ? "float(\"inf\")" : "float(\"-inf\")";

Check warning on line 210 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 210 in Coder/Languages/PythonGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
protected override IEnumerable<string> RequiredImports(SourceFile file)
Expand Down Expand Up @@ -294,21 +294,23 @@

code.Write(named ? "{" : "[");

for (int index = 0; index < construction.Arguments.Count; index++)
IReadOnlyList<AstNode> elements = WrittenElements(construction);

for (int index = 0; index < elements.Count; index++)
{
if (index > 0)
{
code.Write(", ");
}

if (construction.Arguments[index] is MemberInitialiser member)
if (elements[index] is MemberInitialiser member)
{
code.Write($"\"{member.Name}\": ");
GenerateInternal(member.Value ?? new VariableReference(string.Empty), code);
GenerateInternal(member.Value!, code);
continue;
}

GenerateInternal(construction.Arguments[index], code);
GenerateInternal(elements[index], code);
}

code.Write(named ? "}" : "]");
Expand All @@ -321,21 +323,23 @@
/// <param name="code">The writer to emit into.</param>
private void WriteArguments(ConstructionExpression construction, CodeBlocker code)
{
for (int index = 0; index < construction.Arguments.Count; index++)
IReadOnlyList<AstNode> elements = WrittenElements(construction);

for (int index = 0; index < elements.Count; index++)
{
if (index > 0)
{
code.Write(", ");
}

if (construction.Arguments[index] is MemberInitialiser member)
if (elements[index] is MemberInitialiser member)
{
code.Write($"{member.Name}=");
GenerateInternal(member.Value ?? new VariableReference(string.Empty), code);
GenerateInternal(member.Value!, code);
continue;
}

GenerateInternal(construction.Arguments[index], code);
GenerateInternal(elements[index], code);
}
}

Expand Down
18 changes: 10 additions & 8 deletions Coder/Languages/StandardLanguageGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -688,27 +688,29 @@ protected void WriteElementList(
Ensure.NotNull(construction);
Ensure.NotNull(code);

if (construction.Arguments.Count == 0)
IReadOnlyList<AstNode> elements = WrittenElements(construction);

if (elements.Count == 0)
{
code.Write(empty);
return;
}

if (SpansLines(construction))
{
WriteStackedList(construction, code, open, close);
WriteStackedList(elements, code, open, close);
return;
}

code.Write($"{open}{ListPadding}");
for (int index = 0; index < construction.Arguments.Count; index++)
for (int index = 0; index < elements.Count; index++)
{
if (index > 0)
{
code.Write(", ");
}

WriteListElement(construction.Arguments[index], code);
WriteListElement(elements[index], code);
}

code.Write($"{ListPadding}{close}");
Expand All @@ -727,20 +729,20 @@ protected void WriteElementList(
/// <summary>
/// Writes a list one element per line.
/// </summary>
/// <param name="construction">The expression whose arguments to write.</param>
/// <param name="elements">The elements to write.</param>
/// <param name="code">The writer to emit into.</param>
/// <param name="open">What opens the list.</param>
/// <param name="close">What closes it.</param>
/// <remarks>
/// A trailing comma after the last element, which every target language allows in a list and
/// which keeps adding a row to a generated table from touching the row above it in the diff.
/// </remarks>
private void WriteStackedList(ConstructionExpression construction, CodeBlocker code, string open, string close)
private void WriteStackedList(IReadOnlyList<AstNode> elements, CodeBlocker code, string open, string close)
{
code.WriteLine(open);
code.Indent();

foreach (AstNode argument in construction.Arguments)
foreach (AstNode argument in elements)
{
WriteListElement(argument, code);
code.WriteLine(",");
Expand All @@ -764,7 +766,7 @@ protected virtual void WriteListElement(AstNode argument, CodeBlocker code)
if (argument is MemberInitialiser designated)
{
WriteDesignator(designated.Name ?? string.Empty, code);
WriteListValue(designated.Value ?? new VariableReference(string.Empty), code);
WriteListValue(designated.Value!, code);
return;
}

Expand Down
Loading