From 76c54efa3c9452149783a718ee9834d472cdc9a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 19:28:04 +0000 Subject: [PATCH] Check a template's doc comment before writing the class that holds it [patch] The doc-comment attribute checks ran only inside DocComment.WriteTo, so a member, nested class or later class with a line break in a param name, seealso cref or inheritdoc cref threw after the class had been partly written. TemplateBase.Validate now runs those checks for the template's own Documentation, and every Validate override calls it, so ClassTemplate and SourceFileTemplate refuse it before writing anything. Fixes ktsu-dev/CodeBlocker#188 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5kkYGVswnWUL9YgPE --- CodeBlocker.Test/TemplateTests.cs | 54 +++++++++++++++++++++++ CodeBlocker/Templates/ClassTemplate.cs | 1 + CodeBlocker/Templates/DocComment.cs | 14 +++++- CodeBlocker/Templates/OperatorTemplate.cs | 6 ++- CodeBlocker/Templates/PropertyTemplate.cs | 2 + CodeBlocker/Templates/TemplateBase.cs | 7 ++- 6 files changed, 77 insertions(+), 7 deletions(-) diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index 3fd8237..78c5f4a 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -1557,6 +1557,60 @@ public void ASourceFileWithAnInvalidClassWritesNothing() Assert.AreEqual(string.Empty, codeBlocker.ToString()); } + [TestMethod] + public void AClassWithAMemberWhoseDocCommentCannotBeWrittenWritesNothing() + { + // It used to write "public class C { void A() { } }" up to B, then throw. + MethodTemplate b = new() { Type = "void", Name = "B", BodyFactory = _ => { }, Documentation = new DocComment() }; + b.Documentation.Params.Add(new DocTag { Name = "a\nb", Text = "t" }); + ClassTemplate type = new() + { + Name = "C", + Keywords = { "public" }, + Members = + { + new MethodTemplate { Type = "void", Name = "A", BodyFactory = _ => { } }, + b, + }, + }; + + using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf); + codeBlocker.WriteLine("int x;"); + + Assert.ThrowsExactly(() => codeBlocker.AddClass(type)); + Assert.AreEqual("int x;\n", codeBlocker.ToString()); + } + + [TestMethod] + public void ASourceFileWhoseLaterClassHasADocCommentThatCannotBeWrittenWritesNothing() + { + ClassTemplate second = new() { Name = "Second", Documentation = new DocComment { Summary = "s" } }; + second.Documentation.SeeAlso.Add("A\nB"); + SourceFileTemplate file = new() + { + Namespace = "Contoso", + Usings = { "System" }, + Classes = { new ClassTemplate { Name = "First" }, second }, + }; + + using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf); + + Assert.ThrowsExactly(() => codeBlocker.AddSourceFile(file)); + Assert.AreEqual(string.Empty, codeBlocker.ToString()); + } + + [TestMethod] + public void ANestedClassWhoseDocCommentCannotBeWrittenLeavesTheOuterClassUnwritten() + { + ClassTemplate inner = new() { Name = "Inner", Documentation = new DocComment { InheritDoc = true, InheritDocCref = "X\r\nY" } }; + ClassTemplate outer = new() { Name = "Outer", NestedClasses = { inner } }; + + using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf); + + Assert.ThrowsExactly(() => codeBlocker.AddClass(outer)); + Assert.AreEqual(string.Empty, codeBlocker.ToString()); + } + [TestMethod] public void AddClassesWithAnInvalidClassWritesNothing() { diff --git a/CodeBlocker/Templates/ClassTemplate.cs b/CodeBlocker/Templates/ClassTemplate.cs index 9304811..b39db7f 100644 --- a/CodeBlocker/Templates/ClassTemplate.cs +++ b/CodeBlocker/Templates/ClassTemplate.cs @@ -104,6 +104,7 @@ public override void WriteTo(CodeBlocker codeBlocker) /// internal override void Validate() { + base.Validate(); _ = TypeKindKeywords.For(Kind); foreach (MemberTemplate member in Members) diff --git a/CodeBlocker/Templates/DocComment.cs b/CodeBlocker/Templates/DocComment.cs index 93261a9..3a19a4a 100644 --- a/CodeBlocker/Templates/DocComment.cs +++ b/CodeBlocker/Templates/DocComment.cs @@ -108,7 +108,7 @@ public void WriteTo(CodeBlocker codeBlocker) // Checked before the first line is written: a cref or name cannot be split across /// lines // the way element text is, and left as it is the rest of it would be compiled as code. - EnsureAttributesFitOnOneLine(); + ValidateAttributes(); if (InheritDoc) { @@ -189,7 +189,17 @@ void Check(Collection tags, HashSet declared, string tagName, st } } - private void EnsureAttributesFitOnOneLine() + /// + /// Throws if would refuse this comment, without writing anything. + /// + /// + /// calls this for a template's , + /// so a container refuses a bad doc comment before writing any of itself, not partway through. + /// + /// + /// , a entry, or a tag name contains a line break. + /// + internal void ValidateAttributes() { if (InheritDoc) { diff --git a/CodeBlocker/Templates/OperatorTemplate.cs b/CodeBlocker/Templates/OperatorTemplate.cs index 4c237b3..a766aff 100644 --- a/CodeBlocker/Templates/OperatorTemplate.cs +++ b/CodeBlocker/Templates/OperatorTemplate.cs @@ -80,7 +80,11 @@ public override void WriteTo(CodeBlocker codeBlocker) } /// - internal override void Validate() => _ = Signature; + internal override void Validate() + { + base.Validate(); + _ = Signature; + } private string Signature => Kind switch { diff --git a/CodeBlocker/Templates/PropertyTemplate.cs b/CodeBlocker/Templates/PropertyTemplate.cs index 471ec0a..cd48b0e 100644 --- a/CodeBlocker/Templates/PropertyTemplate.cs +++ b/CodeBlocker/Templates/PropertyTemplate.cs @@ -87,6 +87,8 @@ public override void WriteTo(CodeBlocker codeBlocker) /// internal override void Validate() { + base.Validate(); + if (ExpressionBodyFactory is null && Getter is null && Setter is null) { // Writing "Type Name;" here would emit a field, not a property — which is what an diff --git a/CodeBlocker/Templates/TemplateBase.cs b/CodeBlocker/Templates/TemplateBase.cs index 1abb91b..2dbdd37 100644 --- a/CodeBlocker/Templates/TemplateBase.cs +++ b/CodeBlocker/Templates/TemplateBase.cs @@ -90,11 +90,10 @@ public virtual void WriteTo(CodeBlocker codeBlocker) /// /// /// A container calls this on everything it holds before writing any of it, so a template that - /// fails its check cannot leave a container written without it, and the ones after it. + /// fails its check cannot leave a container written without it, and the ones after it. The base + /// check covers ; an override must call it. /// - internal virtual void Validate() - { - } + internal virtual void Validate() => Documentation?.ValidateAttributes(); /// /// Writes = value when is set, as an escaped string literal