diff --git a/CLAUDE.md b/CLAUDE.md
index 57061ee..6e624d3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -42,12 +42,12 @@ The opt-in lives in `.sonarlint/sonar-local.props` (the analyzer package) and
analyzer package ships disabled). Nothing imports these automatically, so normal builds, the CI
pipeline, and packaging are unaffected.
-On current `main` the run reports six findings, and they are the same six SonarCloud reports:
-five `S2699` (test methods that assert nothing) in `CodeBlocker.Test`, and one `S8969` on
-`CodeBlocker/Templates/DocComment.cs:191` — a null-forgiving `text!` that the
-`string.IsNullOrEmpty(text)` guard directly above already makes redundant. The setup also earlier
-found and named `S4144` on `ScopeTests.cs` — two test methods with identical bodies, one of which
-did not test what its name claimed — which is the kind of finding it exists for.
+On current `main` the run reports five findings, and they are the same five SonarCloud reports:
+five `S2699` (test methods that assert nothing) in `CodeBlocker.Test`. The setup also earlier found
+and named `S8969` on `CodeBlocker/Templates/DocComment.cs` — a null-forgiving `text!` that the
+`string.IsNullOrEmpty(text)` guard directly above already made redundant — and `S4144` on
+`ScopeTests.cs` — two test methods with identical bodies, one of which did not test what its name
+claimed — which is the kind of finding it exists for.
### Recalibrating
diff --git a/CodeBlocker.Test/DocCommentTests.cs b/CodeBlocker.Test/DocCommentTests.cs
index 94e704c..eabbbb2 100644
--- a/CodeBlocker.Test/DocCommentTests.cs
+++ b/CodeBlocker.Test/DocCommentTests.cs
@@ -187,6 +187,38 @@ public void ValidationReportsADuplicateTag()
Assert.Contains("more than once", issues[0]);
}
+ [TestMethod]
+ public void ANamedTagWithEmptyTextIsStillWritten()
+ {
+ DocComment documentation = new() { Summary = "S" };
+ documentation.TypeParams.Add(new DocTag { Name = "T", Text = "" });
+ documentation.Params.Add(new DocTag { Name = "a", Text = "A" });
+ documentation.Params.Add(new DocTag { Name = "b", Text = "" });
+ documentation.Exceptions.Add(new DocTag { Name = "ArgumentException", Text = "" });
+
+ Assert.IsEmpty(documentation.Validate(["a", "b"], ["T"]));
+ Assert.AreEqual(
+ """
+ /// S
+ ///
+ /// A
+ ///
+ ///
+
+ """.ReplaceLineEndings("\n"),
+ Render(documentation));
+ }
+
+ [TestMethod]
+ public void ACommentHoldingOnlyAnEmptyParamIsNotEmptyAndWritesIt()
+ {
+ DocComment documentation = new();
+ documentation.Params.Add(new DocTag { Name = "b", Text = "" });
+
+ Assert.IsFalse(documentation.IsEmpty);
+ Assert.AreEqual("/// \n", Render(documentation));
+ }
+
[TestMethod]
public void ValidationRejectsNullArguments()
{
diff --git a/CodeBlocker/Templates/DocComment.cs b/CodeBlocker/Templates/DocComment.cs
index f13eea7..6e52b13 100644
--- a/CodeBlocker/Templates/DocComment.cs
+++ b/CodeBlocker/Templates/DocComment.cs
@@ -112,12 +112,12 @@ public void WriteTo(CodeBlocker codeBlocker)
foreach (DocTag typeParam in TypeParams)
{
- WriteElement(codeBlocker, "typeparam", $" name=\"{EscapeAttribute(typeParam.Name)}\"", typeParam.Text);
+ WriteNamedElement(codeBlocker, "typeparam", $" name=\"{EscapeAttribute(typeParam.Name)}\"", typeParam.Text);
}
foreach (DocTag param in Params)
{
- WriteElement(codeBlocker, "param", $" name=\"{EscapeAttribute(param.Name)}\"", param.Text);
+ WriteNamedElement(codeBlocker, "param", $" name=\"{EscapeAttribute(param.Name)}\"", param.Text);
}
WriteElement(codeBlocker, "returns", null, Returns);
@@ -125,7 +125,7 @@ public void WriteTo(CodeBlocker codeBlocker)
foreach (DocTag exception in Exceptions)
{
- WriteElement(codeBlocker, "exception", $" cref=\"{EscapeAttribute(exception.Name)}\"", exception.Text);
+ WriteNamedElement(codeBlocker, "exception", $" cref=\"{EscapeAttribute(exception.Name)}\"", exception.Text);
}
WriteElement(codeBlocker, "remarks", null, Remarks);
@@ -188,7 +188,21 @@ private void WriteElement(CodeBlocker codeBlocker, string tagName, string? attri
return;
}
- string[] lines = SplitLines(text!);
+ WriteNamedElement(codeBlocker, tagName, attributes, text);
+ }
+
+ ///
+ /// Writes an element whose attribute names what it documents. Unlike the unnamed elements, it is
+ /// written even when its text is empty: counts it as documenting that
+ /// name, and leaving it out would raise the CS1573 the validation promised was not coming.
+ ///
+ /// The to write to.
+ /// The element name.
+ /// The element's attributes, already escaped.
+ /// The element's content.
+ private void WriteNamedElement(CodeBlocker codeBlocker, string tagName, string? attributes, string? text)
+ {
+ string[] lines = SplitLines(text ?? string.Empty);
if (lines.Length == 1)
{
codeBlocker.WriteLine($"/// <{tagName}{attributes}>{Escape(lines[0])}{tagName}>");