diff --git a/CodeBlocker.Test/DocCommentTests.cs b/CodeBlocker.Test/DocCommentTests.cs index f802433..c192424 100644 --- a/CodeBlocker.Test/DocCommentTests.cs +++ b/CodeBlocker.Test/DocCommentTests.cs @@ -287,6 +287,39 @@ public void ATemplateWritesItsDocumentationAboveItsAttributes() Render(method.WriteTo)); } + [TestMethod] + [DataRow("\n", DisplayName = "LF")] + [DataRow("\r\n", DisplayName = "CRLF")] + [DataRow("\r", DisplayName = "CR")] + [DataRow("\u0085", DisplayName = "NEL")] + [DataRow("\u2028", DisplayName = "LINE SEPARATOR")] + [DataRow("\u2029", DisplayName = "PARAGRAPH SEPARATOR")] + public void ALineBreakInAnAttributeValueIsRejectedBeforeWriting(string terminator) + { + string value = $"A{terminator}B"; + DocComment[] comments = + [ + new() { Summary = "s", SeeAlso = { value } }, + new() { Summary = "s", InheritDoc = true, InheritDocCref = value }, + new() { Summary = "s", Params = { new DocTag { Name = value, Text = "t" } } }, + new() { Summary = "s", TypeParams = { new DocTag { Name = value, Text = "t" } } }, + new() { Summary = "s", Exceptions = { new DocTag { Name = value, Text = "t" } } }, + ]; + + foreach (DocComment documentation in comments) + { + using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf); + Assert.ThrowsExactly(() => documentation.WriteTo(codeBlocker)); + Assert.AreEqual(string.Empty, codeBlocker.ToString(), "Nothing should be written before the check."); + } + } + + [TestMethod] + public void AnUnusedInheritDocCrefIsNotChecked() => + Assert.AreEqual( + "/// s\n", + Render(new DocComment { Summary = "s", InheritDocCref = "A\nB" })); + [TestMethod] public void NullCodeBlockerIsRejected() => Assert.ThrowsExactly(() => new DocComment().WriteTo(null!)); diff --git a/CodeBlocker/Templates/DocComment.cs b/CodeBlocker/Templates/DocComment.cs index d82f857..a583813 100644 --- a/CodeBlocker/Templates/DocComment.cs +++ b/CodeBlocker/Templates/DocComment.cs @@ -97,10 +97,19 @@ public class DocComment /// /// The to write to. /// is . + /// + /// , a entry, or the of + /// a , or entry contains a + /// line break. Nothing is written. + /// public void WriteTo(CodeBlocker codeBlocker) { Ensure.NotNull(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(); + if (InheritDoc) { codeBlocker.WriteLine(string.IsNullOrEmpty(InheritDocCref) @@ -181,6 +190,39 @@ void Check(Collection tags, HashSet declared, string tagName, st } } + private void EnsureAttributesFitOnOneLine() + { + if (InheritDoc) + { + EnsureSingleLine(InheritDocCref, "inheritdoc cref", nameof(InheritDocCref)); + } + + EnsureNamesSingleLine(TypeParams, "typeparam name", nameof(TypeParams)); + EnsureNamesSingleLine(Params, "param name", nameof(Params)); + EnsureNamesSingleLine(Exceptions, "exception cref", nameof(Exceptions)); + + foreach (string cref in SeeAlso) + { + EnsureSingleLine(cref, "seealso cref", nameof(SeeAlso)); + } + + static void EnsureNamesSingleLine(Collection tags, string what, string paramName) + { + foreach (DocTag tag in tags) + { + EnsureSingleLine(tag.Name, what, paramName); + } + } + + static void EnsureSingleLine(string? value, string what, string paramName) + { + if (value is not null && LineTerminators.Contains(value)) + { + throw new ArgumentException($"A doc comment {what} must fit on one /// line.", paramName); + } + } + } + private void WriteElement(CodeBlocker codeBlocker, string tagName, string? attributes, string? text) { if (string.IsNullOrEmpty(text))