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])}");