Skip to content
Merged
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
107 changes: 107 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,82 @@ public void AClassOfUnknownKindWritesNothing()
Assert.AreEqual("int a;\n", codeBlocker.ToString());
}

private static PropertyTemplate InvalidProperty() => new() { Name = "P", Type = "int" };

[TestMethod]
public void AClassWithAnInvalidMemberWritesNothing()
{
ClassTemplate type = new() { Name = "C" };
type.Members.Add(new FieldTemplate { Name = "a", Type = "int" });
type.Members.Add(InvalidProperty());
type.Members.Add(new FieldTemplate { Name = "b", Type = "int", Keywords = { "private" } });

using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);

InvalidOperationException exception =
Assert.ThrowsExactly<InvalidOperationException>(() => codeBlocker.AddClass(type));
Assert.Contains("'P'", exception.Message);
Assert.AreEqual(string.Empty, codeBlocker.ToString());
}

[TestMethod]
public void AnInvalidMemberOfANestedClassLeavesTheOuterClassUnwritten()
{
ClassTemplate inner = new() { Name = "Inner", Members = { InvalidProperty() } };
ClassTemplate outer = new()
{
Name = "Outer",
Members = { new FieldTemplate { Name = "a", Type = "int" } },
NestedClasses = { inner },
};

using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);
codeBlocker.WriteLine("int x;");

Assert.ThrowsExactly<InvalidOperationException>(() => codeBlocker.AddClass(outer));
Assert.AreEqual("int x;\n", codeBlocker.ToString());
}

[TestMethod]
public void ANestedClassOfUnknownKindLeavesTheOuterClassUnwritten()
{
ClassTemplate outer = new() { Name = "Outer", NestedClasses = { new ClassTemplate { Name = "Inner", Kind = (TypeKind)99 } } };

using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);

Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => codeBlocker.AddClass(outer));
Assert.AreEqual(string.Empty, codeBlocker.ToString());
}

[TestMethod]
public void AnOperatorOfUnknownKindLeavesItsClassUnwritten()
{
ClassTemplate type = new()
{
Name = "C",
Members = { new OperatorTemplate { Kind = (OperatorKind)99, Type = "C", Symbol = "+" } },
};

using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);

Assert.ThrowsExactly<InvalidOperationException>(() => codeBlocker.AddClass(type));
Assert.AreEqual(string.Empty, codeBlocker.ToString());
}

[TestMethod]
public void AClassWithValidMembersIsStillWritten() =>
Assert.AreEqual(
"class C\n{\n\tint a;\n\n\tint P { get; }\n}\n",
Render(new ClassTemplate
{
Name = "C",
Members =
{
new FieldTemplate { Name = "a", Type = "int" },
new PropertyTemplate { Name = "P", Type = "int", Getter = AccessorTemplate.Auto() },
},
}));

[TestMethod]
public void AConversionIsNamedByItsResultType()
{
Expand Down Expand Up @@ -1194,6 +1270,37 @@ private class Inner
public void AnEmptySourceFileWritesNothing() =>
Assert.AreEqual(string.Empty, Render(codeBlocker => codeBlocker.AddSourceFile(new SourceFileTemplate())));

[TestMethod]
public void ASourceFileWithAnInvalidClassWritesNothing()
{
SourceFileTemplate file = new()
{
Namespace = "Contoso",
Usings = { "System" },
Comments = { "// <auto-generated />" },
Classes =
{
new ClassTemplate { Name = "Good" },
new ClassTemplate { Name = "Bad", Members = { InvalidProperty() } },
},
};

using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);

Assert.ThrowsExactly<InvalidOperationException>(() => codeBlocker.AddSourceFile(file));
Assert.AreEqual(string.Empty, codeBlocker.ToString());
}

[TestMethod]
public void AddClassesWithAnInvalidClassWritesNothing()
{
using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);

Assert.ThrowsExactly<InvalidOperationException>(() => codeBlocker.AddClasses(
[new ClassTemplate { Name = "Good" }, new ClassTemplate { Name = "Bad", Members = { InvalidProperty() } }]));
Assert.AreEqual(string.Empty, codeBlocker.ToString());
}

[TestMethod]
public void ASourceFileWritesItsPreambleThenItsTypes()
{
Expand Down
27 changes: 24 additions & 3 deletions CodeBlocker/Templates/ClassTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,19 @@ public class ClassTemplate : TemplateBase
/// <param name="codeBlocker">The <see cref="CodeBlocker"/> to write to.</param>
/// <exception cref="ArgumentNullException"><paramref name="codeBlocker"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <see cref="Kind"/> is not a known kind. Nothing is written.
/// <see cref="Kind"/>, or the kind of a nested type, is not a known kind. Nothing is written.
/// </exception>
/// <exception cref="InvalidOperationException">
/// A member, or a member of a nested type, would be refused when written. Nothing is written.
/// </exception>
public override void WriteTo(CodeBlocker codeBlocker)
{
Ensure.NotNull(codeBlocker);

// Resolved before the doc comment, attributes and modifiers are written, so an unknown kind
// does not leave them behind as an unfinished declaration.
// Everything is checked before the doc comment, attributes and modifiers are written, so a
// failing check cannot leave an unfinished declaration behind, nor a closed body that is
// silently missing the failing member and everything after it.
Validate();
string keyword = TypeKindKeywords.For(Kind);
base.WriteTo(codeBlocker);

Expand Down Expand Up @@ -96,6 +101,22 @@ public override void WriteTo(CodeBlocker codeBlocker)
WriteBodyTo(codeBlocker);
}

/// <inheritdoc/>
internal override void Validate()
{
_ = TypeKindKeywords.For(Kind);

foreach (MemberTemplate member in Members)
{
member.Validate();
}

foreach (ClassTemplate nestedClass in NestedClasses)
{
nestedClass.Validate();
}
}

/// <summary>
/// Gets a value indicating whether this is a record whose positional parameters are its whole
/// definition, which is declared with a semicolon instead of an empty body.
Expand Down
19 changes: 12 additions & 7 deletions CodeBlocker/Templates/OperatorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,7 @@ public override void WriteTo(CodeBlocker codeBlocker)

// Worked out before anything is written, so an unknown kind cannot leave a half-written
// declaration behind.
string signature = Kind switch
{
OperatorKind.Normal => $"{Type} operator {Symbol}",
OperatorKind.Implicit => $"implicit operator {Type}",
OperatorKind.Explicit => $"explicit operator {Type}",
_ => throw new InvalidOperationException($"Unknown operator kind '{Kind}'."),
};
string signature = Signature;

// Not through the MemberTemplate implementation: an operator's signature is its symbol and
// result type, not a type followed by a name.
Expand All @@ -84,4 +78,15 @@ public override void WriteTo(CodeBlocker codeBlocker)
TemplateRendering.WriteParameterList(codeBlocker, Parameters);
TemplateRendering.WriteBody(codeBlocker, BodyFactory);
}

/// <inheritdoc/>
internal override void Validate() => _ = Signature;

private string Signature => Kind switch
{
OperatorKind.Normal => $"{Type} operator {Symbol}",
OperatorKind.Implicit => $"implicit operator {Type}",
OperatorKind.Explicit => $"explicit operator {Type}",
_ => throw new InvalidOperationException($"Unknown operator kind '{Kind}'."),
};
}
37 changes: 21 additions & 16 deletions CodeBlocker/Templates/PropertyTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ public override void WriteTo(CodeBlocker codeBlocker)
{
Ensure.NotNull(codeBlocker);

Validate();
base.WriteTo(codeBlocker);

if (ExpressionBodyFactory is not null)
{
TemplateRendering.WriteExpressionBody(codeBlocker, ExpressionBodyFactory);
return;
}

if (CanUseShorthand)
{
WriteShorthand(codeBlocker);
return;
}

WriteAccessorList(codeBlocker);
}

/// <inheritdoc/>
internal override void 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 All @@ -71,22 +92,6 @@ public override void WriteTo(CodeBlocker codeBlocker)
Getter?.Validate();
Setter?.Validate();
}

base.WriteTo(codeBlocker);

if (ExpressionBodyFactory is not null)
{
TemplateRendering.WriteExpressionBody(codeBlocker, ExpressionBodyFactory);
return;
}

if (CanUseShorthand)
{
WriteShorthand(codeBlocker);
return;
}

WriteAccessorList(codeBlocker);
}

/// <summary>The keyword the setter is written with.</summary>
Expand Down
27 changes: 25 additions & 2 deletions CodeBlocker/Templates/SourceFileTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,18 @@ public static class SourceFileTemplateExtensions
/// <exception cref="ArgumentNullException">
/// <paramref name="codeBlocker"/> or <paramref name="template"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="InvalidOperationException">
/// A type in the file would be refused when written. Nothing is written.
/// </exception>
public static CodeBlocker AddSourceFile(this CodeBlocker codeBlocker, SourceFileTemplate template)
{
Ensure.NotNull(codeBlocker);
Ensure.NotNull(template);

// Checked before the header is written, so a type that would be refused leaves nothing
// behind rather than a file holding only the types before it.
ValidateAll(template.Classes);

codeBlocker.AddDocumentation(template.Documentation);
codeBlocker.AddComments(template.Comments);
if (template.Attributes.Count > 0)
Expand Down Expand Up @@ -82,19 +89,35 @@ public static CodeBlocker AddSourceFile(this CodeBlocker codeBlocker, SourceFile
/// <param name="classes">The types to write.</param>
/// <returns>The same <see cref="CodeBlocker"/>, for chaining.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="codeBlocker"/> or <paramref name="classes"/> is <see langword="null"/>.
/// <paramref name="codeBlocker"/> or <paramref name="classes"/>, or one of the types, is
/// <see langword="null"/>. Nothing is written.
/// </exception>
/// <exception cref="InvalidOperationException">
/// One of the types would be refused when written. Nothing is written.
/// </exception>
public static CodeBlocker AddClasses(this CodeBlocker codeBlocker, IEnumerable<ClassTemplate> classes)
{
Ensure.NotNull(codeBlocker);
Ensure.NotNull(classes);

foreach (ClassTemplate classTemplate in classes)
List<ClassTemplate> classList = [.. classes];
ValidateAll(classList);

foreach (ClassTemplate classTemplate in classList)
{
codeBlocker.AddClass(classTemplate);
codeBlocker.NewLine();
}

return codeBlocker;
}

private static void ValidateAll(IEnumerable<ClassTemplate> classes)
{
foreach (ClassTemplate classTemplate in classes)
{
Ensure.NotNull(classTemplate);
classTemplate.Validate();
}
}
}
11 changes: 11 additions & 0 deletions CodeBlocker/Templates/TemplateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ public virtual void WriteTo(CodeBlocker codeBlocker)
codeBlocker.AddKeywords(Keywords);
}

/// <summary>
/// Throws if <see cref="WriteTo"/> would refuse this template, without writing anything.
/// </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.
/// </remarks>
internal virtual void Validate()
{
}

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