From ae81e493919e5650a9017945c9e368d89e569c1c Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Fri, 9 Oct 2026 04:30:46 +0000 Subject: [PATCH] fix: clone a variable reference that has no name yet [patch] VariableReference.Clone() went through the validating constructor, which throws on the empty name a new AssignmentStatement's target or a YAML node with no name holds. C, C++, Rust and Go clone the whole class before writing it, so any class with a property and an unfilled assignment threw out of generation, and crashed the editor's preview. Clone through the parameterless constructor instead, and check every node type clones from its parameterless constructor. Fixes #161 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ACXyQBPWSqFJuF51Mx6FNh --- Coder.Test/Ast/UnfilledNodeCloneTests.cs | 92 ++++++++++++++++++++++++ Coder/Ast/VariableReference.cs | 5 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 Coder.Test/Ast/UnfilledNodeCloneTests.cs diff --git a/Coder.Test/Ast/UnfilledNodeCloneTests.cs b/Coder.Test/Ast/UnfilledNodeCloneTests.cs new file mode 100644 index 0000000..fa54519 --- /dev/null +++ b/Coder.Test/Ast/UnfilledNodeCloneTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Ast; + +using System.Reflection; +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that a node nobody has filled in yet can still be cloned. +/// +/// +/// A node dropped in the editor, or read from YAML with a key missing, holds whatever its +/// parameterless constructor gave it. The generators clone whole types before they write them, so a +/// clone that validates what it copies turns a half-built AST into an exception out of generation. +/// +[TestClass] +public class UnfilledNodeCloneTests +{ + /// + /// Every node type built with its parameterless constructor clones without throwing. + /// + [TestMethod] + public void EveryNodeType_ClonesFromItsParameterlessConstructor() + { + IEnumerable nodeTypes = typeof(AstNode).Assembly.GetTypes() + .Where(t => typeof(AstNode).IsAssignableFrom(t) && !t.IsAbstract && !t.ContainsGenericParameters) + .Where(t => t.GetConstructor(BindingFlags.Public | BindingFlags.Instance, Type.EmptyTypes) is not null); + + List failures = []; + foreach (Type nodeType in nodeTypes) + { + AstNode node = (AstNode)Activator.CreateInstance(nodeType)!; + try + { + _ = node.Clone(); + } + catch (ArgumentException ex) + { + failures.Add($"{nodeType.Name}: {ex.Message}"); + } + } + + Assert.IsEmpty(failures, string.Join(Environment.NewLine, failures)); + } + + /// + /// A variable reference with no name yet clones to another with no name. + /// + [TestMethod] + public void VariableReference_WithNoName_ClonesToNoName() + { + VariableReference original = new() { ExpectedType = "int" }; + + VariableReference clone = (VariableReference)original.Clone(); + + Assert.AreEqual(string.Empty, clone.Name); + Assert.AreEqual("int", clone.ExpectedType?.ToString()); + } + + /// + /// A class with a property and an assignment that has no target yet still generates in the + /// languages that clone the class before writing it. + /// + /// The language to generate. + [TestMethod] + [DataRow("c")] + [DataRow("cpp")] + [DataRow("rust")] + [DataRow("go")] + public void ClassWithAnUnfilledAssignment_Generates(string languageId) + { + ClassDeclaration box = new("Box"); + box.Members.Add(new PropertyDeclaration("Size", "int")); + FunctionDeclaration reset = new("reset"); + reset.Body.Add(new AssignmentStatement()); + box.Members.Add(reset); + + ILanguageGenerator generator = languageId switch + { + "c" => new CGenerator(), + "cpp" => new CppGenerator(), + "rust" => new RustGenerator(), + _ => new GoGenerator(), + }; + + string output = generator.Generate(box); + + Assert.Contains("Box", output); + } +} diff --git a/Coder/Ast/VariableReference.cs b/Coder/Ast/VariableReference.cs index 0503605..38d9b8f 100644 --- a/Coder/Ast/VariableReference.cs +++ b/Coder/Ast/VariableReference.cs @@ -37,8 +37,11 @@ public class VariableReference : Expression /// A new instance with the same property values. public override AstNode Clone() { - VariableReference clone = new(Name) + // The parameterless constructor and an assignment, because the validating one would refuse + // the empty name an unfilled node legitimately holds, and a clone must not judge what it copies. + VariableReference clone = new() { + Name = Name, ExpectedType = ExpectedType };