diff --git a/Coder.Test/Languages/UnfilledMemberInitialiserTests.cs b/Coder.Test/Languages/UnfilledMemberInitialiserTests.cs
new file mode 100644
index 0000000..601fe80
--- /dev/null
+++ b/Coder.Test/Languages/UnfilledMemberInitialiserTests.cs
@@ -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;
+
+///
+/// Tests that a braced-list member initialiser with no value yet is left out rather than thrown on.
+///
+///
+/// new MemberInitialiser("x") 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.
+///
+[TestClass]
+public class UnfilledMemberInitialiserTests
+{
+ ///
+ /// Tests that every generator writes a list holding an unfilled member, and writes its filled
+ /// sibling.
+ ///
+ /// The generator's language identifier.
+ [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}");
+ }
+
+ ///
+ /// Tests that every generator writes a typeless list whose only element is unfilled.
+ ///
+ /// The generator's language identifier.
+ [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(),
+ ];
+}
diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs
index 772f358..02bc75c 100644
--- a/Coder/Languages/CSharpGenerator.cs
+++ b/Coder/Languages/CSharpGenerator.cs
@@ -411,7 +411,9 @@ private void GenerateConstruction(ConstructionExpression construction, CodeBlock
///
private void WriteBracedList(ConstructionExpression construction, CodeBlocker code)
{
- if (construction.Arguments.Count == 0)
+ IReadOnlyList elements = WrittenElements(construction);
+
+ if (elements.Count == 0)
{
code.Write("{ }");
return;
@@ -425,7 +427,7 @@ private void WriteBracedList(ConstructionExpression construction, CodeBlocker co
code.WriteLine("{");
code.Indent();
- foreach (AstNode argument in construction.Arguments)
+ foreach (AstNode argument in elements)
{
WriteListElement(argument, code);
code.WriteLine(",");
@@ -438,14 +440,14 @@ private void WriteBracedList(ConstructionExpression construction, CodeBlocker co
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(" }");
@@ -461,7 +463,7 @@ private void WriteListElement(AstNode argument, CodeBlocker code)
if (argument is MemberInitialiser designated)
{
code.Write($"{designated.Name} = ");
- GenerateInternal(designated.Value ?? new VariableReference(string.Empty), code);
+ GenerateInternal(designated.Value!, code);
return;
}
diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs
index 81cf7cd..02709a7 100644
--- a/Coder/Languages/JavaScriptGenerator.cs
+++ b/Coder/Languages/JavaScriptGenerator.cs
@@ -225,21 +225,23 @@ private void WriteLiteral(ConstructionExpression construction, CodeBlocker code,
{
code.Write(named ? "{ " : "[");
- for (int index = 0; index < construction.Arguments.Count; index++)
+ IReadOnlyList 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 ? " }" : "]");
diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs
index 5a72e9d..57c879d 100644
--- a/Coder/Languages/LanguageGeneratorBase.cs
+++ b/Coder/Languages/LanguageGeneratorBase.cs
@@ -436,6 +436,23 @@ protected static IEnumerable SelfAndDescendants(AstNode root)
}
}
+ ///
+ /// Gets the elements of a list that are written, leaving out a member named with no value yet.
+ ///
+ /// The list.
+ /// Its elements, without any whose value is null.
+ ///
+ /// 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.
+ ///
+ protected static IReadOnlyList WrittenElements(ConstructionExpression construction)
+ {
+ Ensure.NotNull(construction);
+ return [.. construction.Arguments.Where(argument => argument is not MemberInitialiser { Value: null })];
+ }
+
///
/// Reports whether a file holds a double literal that is not a finite number.
///
diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs
index 55462d6..0fec93e 100644
--- a/Coder/Languages/PythonGenerator.cs
+++ b/Coder/Languages/PythonGenerator.cs
@@ -294,21 +294,23 @@ private void WriteLiteral(ConstructionExpression construction, CodeBlocker code)
code.Write(named ? "{" : "[");
- for (int index = 0; index < construction.Arguments.Count; index++)
+ IReadOnlyList 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 ? "}" : "]");
@@ -321,21 +323,23 @@ private void WriteLiteral(ConstructionExpression construction, CodeBlocker code)
/// The writer to emit into.
private void WriteArguments(ConstructionExpression construction, CodeBlocker code)
{
- for (int index = 0; index < construction.Arguments.Count; index++)
+ IReadOnlyList 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);
}
}
diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs
index 2d9cb61..3495750 100644
--- a/Coder/Languages/StandardLanguageGenerator.cs
+++ b/Coder/Languages/StandardLanguageGenerator.cs
@@ -688,7 +688,9 @@ protected void WriteElementList(
Ensure.NotNull(construction);
Ensure.NotNull(code);
- if (construction.Arguments.Count == 0)
+ IReadOnlyList elements = WrittenElements(construction);
+
+ if (elements.Count == 0)
{
code.Write(empty);
return;
@@ -696,19 +698,19 @@ protected void WriteElementList(
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}");
@@ -727,7 +729,7 @@ protected void WriteElementList(
///
/// Writes a list one element per line.
///
- /// The expression whose arguments to write.
+ /// The elements to write.
/// The writer to emit into.
/// What opens the list.
/// What closes it.
@@ -735,12 +737,12 @@ protected void WriteElementList(
/// 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.
///
- private void WriteStackedList(ConstructionExpression construction, CodeBlocker code, string open, string close)
+ private void WriteStackedList(IReadOnlyList 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(",");
@@ -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;
}