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
56 changes: 56 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,44 @@ public void ARejectedPropertyWritesNothing()
Assert.AreEqual("int a;\n", codeBlocker.ToString());
}

[TestMethod]
public void APropertyWithAnAccessorOfUnknownKindWritesNothing()
{
PropertyTemplate property = new()
{
Type = "int",
Name = "P",
Keywords = { "public" },
Getter = AccessorTemplate.Auto(),
Setter = new AccessorTemplate { Kind = (AccessorKind)99 },
};

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

Assert.ThrowsExactly<InvalidOperationException>(() => property.WriteTo(codeBlocker));
Assert.AreEqual("int a;\n", codeBlocker.ToString());
}

[TestMethod]
public void AnExpressionAccessorWithNoBodyIsRejectedBeforeAnythingIsWritten()
{
// It used to render "get => ;", which does not compile, and raise nothing.
PropertyTemplate property = new()
{
Type = "int",
Name = "P",
Keywords = { "public" },
Getter = new AccessorTemplate { Kind = AccessorKind.Expression },
};

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

Assert.ThrowsExactly<InvalidOperationException>(() => property.WriteTo(codeBlocker));
Assert.AreEqual("int a;\n", codeBlocker.ToString());
}

#endregion

#region MethodTemplate
Expand Down Expand Up @@ -799,6 +837,24 @@ public void AnOperatorOfUnknownKindWritesNothing()
Assert.AreEqual("int a;\n", codeBlocker.ToString());
}

[TestMethod]
public void AClassOfUnknownKindWritesNothing()
{
ClassTemplate unknown = new()
{
Kind = (TypeKind)99,
Name = "C",
Keywords = { "public" },
Attributes = { "Obsolete" },
};

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

Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => unknown.WriteTo(codeBlocker));
Assert.AreEqual("int a;\n", codeBlocker.ToString());
}

[TestMethod]
public void AConversionIsNamedByItsResultType()
{
Expand Down
26 changes: 26 additions & 0 deletions CodeBlocker/Templates/AccessorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,14 @@ public static AccessorTemplate Block(Action<CodeBlocker> body) =>
/// <param name="codeBlocker">The <see cref="CodeBlocker"/> to write to.</param>
/// <param name="keyword">The accessor keyword: <c>get</c>, <c>set</c> or <c>init</c>.</param>
/// <exception cref="ArgumentNullException"><paramref name="codeBlocker"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">
/// <see cref="Kind"/> is not a known kind, or is <see cref="AccessorKind.Expression"/> with no
/// <see cref="BodyFactory"/>. Nothing is written.
/// </exception>
public void WriteTo(CodeBlocker codeBlocker, string keyword)
{
Ensure.NotNull(codeBlocker);
Validate();

string prefix = string.IsNullOrEmpty(Modifier) ? keyword : $"{Modifier} {keyword}";

Expand Down Expand Up @@ -108,6 +113,27 @@ public void WriteTo(CodeBlocker codeBlocker, string keyword)
}
}

/// <summary>
/// Throws when the accessor cannot be written, so a caller can check before writing anything.
/// </summary>
/// <exception cref="InvalidOperationException">
/// <see cref="Kind"/> is not a known kind, or is <see cref="AccessorKind.Expression"/> with no
/// <see cref="BodyFactory"/>, which would render as <c>get =&gt; ;</c>.
/// </exception>
internal void Validate()
{
if (Kind is not (AccessorKind.Auto or AccessorKind.Expression or AccessorKind.Block))
{
throw new InvalidOperationException($"Unknown accessor kind '{Kind}'.");
}

if (Kind == AccessorKind.Expression && BodyFactory is null)
{
throw new InvalidOperationException(
"An expression-bodied accessor needs a BodyFactory to write its expression.");
}
}

/// <summary>
/// Gets a value indicating whether this accessor can appear in the one-line
/// <c>{ get; set; }</c> shorthand.
Expand Down
8 changes: 7 additions & 1 deletion CodeBlocker/Templates/ClassTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,19 @@ public class ClassTemplate : TemplateBase
/// </summary>
/// <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.
/// </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.
string keyword = TypeKindKeywords.For(Kind);
base.WriteTo(codeBlocker);

codeBlocker.Write($"{TypeKindKeywords.For(Kind)} {Name}");
codeBlocker.Write($"{keyword} {Name}");
TemplateRendering.WriteTypeParameterList(codeBlocker, TypeParameters);

if (PositionalParameters.Count > 0)
Expand Down
16 changes: 12 additions & 4 deletions CodeBlocker/Templates/PropertyTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ public class PropertyTemplate : MemberTemplate
/// An expression body is written on the declaration line. Otherwise, accessors that are all
/// automatic and unqualified collapse to <c>{ get; set; }</c> on one line, with any
/// <see cref="TemplateBase.DefaultValue"/> as its initialiser; anything else gets a braced
/// accessor list. A property with no accessors and no expression body is rejected before
/// anything is written, so a caller that catches the exception can keep using
/// <paramref name="codeBlocker"/>.
/// accessor list. A property with no accessors and no expression body, or with an accessor that
/// cannot be written, is rejected before anything is written, so a caller that catches the
/// exception can keep using <paramref name="codeBlocker"/>.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="codeBlocker"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">
/// <see cref="Getter"/>, <see cref="Setter"/> and <see cref="ExpressionBodyFactory"/> are all
/// <see langword="null"/>.
/// <see langword="null"/>; or, with no expression body, an accessor's kind is unknown, or is
/// <see cref="AccessorKind.Expression"/> with no <see cref="AccessorTemplate.BodyFactory"/>.
/// </exception>
public override void WriteTo(CodeBlocker codeBlocker)
{
Expand All @@ -64,6 +65,13 @@ public override void WriteTo(CodeBlocker codeBlocker)
"Setter, or ExpressionBodyFactory.");
}

if (ExpressionBodyFactory is null)
{
// Likewise checked up front: the accessor list is only reached after "Type Name {".
Getter?.Validate();
Setter?.Validate();
}

base.WriteTo(codeBlocker);

if (ExpressionBodyFactory is not null)
Expand Down
Loading