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