Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => codeBlocker.AddClass(outer));
Assert.AreEqual(string.Empty, codeBlocker.ToString());
}

[TestMethod]
public void AddClassesWithAnInvalidClassWritesNothing()
{
Expand Down
1 change: 1 addition & 0 deletions CodeBlocker/Templates/ClassTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public override void WriteTo(CodeBlocker codeBlocker)
/// <inheritdoc/>
internal override void Validate()
{
base.Validate();
_ = TypeKindKeywords.For(Kind);

foreach (MemberTemplate member in Members)
Expand Down
14 changes: 12 additions & 2 deletions CodeBlocker/Templates/DocComment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -189,7 +189,17 @@ void Check(Collection<DocTag> tags, HashSet<string> declared, string tagName, st
}
}

private void EnsureAttributesFitOnOneLine()
/// <summary>
/// Throws if <see cref="WriteTo"/> would refuse this comment, without writing anything.
/// </summary>
/// <remarks>
/// <see cref="TemplateBase.Validate"/> calls this for a template's <see cref="TemplateBase.Documentation"/>,
/// so a container refuses a bad doc comment before writing any of itself, not partway through.
/// </remarks>
/// <exception cref="ArgumentException">
/// <see cref="InheritDocCref"/>, a <see cref="SeeAlso"/> entry, or a tag name contains a line break.
/// </exception>
internal void ValidateAttributes()
{
if (InheritDoc)
{
Expand Down
6 changes: 5 additions & 1 deletion CodeBlocker/Templates/OperatorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ public override void WriteTo(CodeBlocker codeBlocker)
}

/// <inheritdoc/>
internal override void Validate() => _ = Signature;
internal override void Validate()
{
base.Validate();
_ = Signature;
}

private string Signature => Kind switch
{
Expand Down
2 changes: 2 additions & 0 deletions CodeBlocker/Templates/PropertyTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public override void WriteTo(CodeBlocker codeBlocker)
/// <inheritdoc/>
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
Expand Down
7 changes: 3 additions & 4 deletions CodeBlocker/Templates/TemplateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,10 @@ public virtual void WriteTo(CodeBlocker codeBlocker)
/// </summary>
/// <remarks>
/// 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 <see cref="Documentation"/>; an override must call it.
/// </remarks>
internal virtual void Validate()
{
}
internal virtual void Validate() => Documentation?.ValidateAttributes();

/// <summary>
/// Writes <c> = value</c> when <see cref="HasDefaultValue"/> is set, as an escaped string literal
Expand Down
Loading