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; }