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