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
142 changes: 142 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,148 @@ public int Count
Render(property));
}

[TestMethod]
public void AMultiLineExpressionBodiedPropertyKeepsTheEnclosingIndent()
{
ClassTemplate type = new()
{
Name = "C",
Keywords = { "public" },
Members =
{
new PropertyTemplate
{
Type = "int",
Name = "X",
Keywords = { "public" },
ExpressionBodyFactory = codeBlocker =>
{
codeBlocker.WriteLine("k switch");
codeBlocker.WriteLine("{");
codeBlocker.Indent();
codeBlocker.WriteLine("_ => 0,");
codeBlocker.Outdent();
codeBlocker.Write("}");
},
},
},
};

Assert.AreEqual(
"""
public class C
{
public int X => k switch
{
_ => 0,
};
}

""".ReplaceLineEndings("\n"),
Render(type));
}

[TestMethod]
public void AMultiLineExpressionBodiedAccessorKeepsTheEnclosingIndent()
{
ClassTemplate type = new()
{
Name = "C",
Keywords = { "public" },
Members =
{
new PropertyTemplate
{
Type = "int[]",
Name = "Items",
Keywords = { "public" },
Getter = AccessorTemplate.Expression(codeBlocker =>
{
codeBlocker.WriteLine("[");
codeBlocker.Indent();
codeBlocker.WriteLine("1,");
codeBlocker.WriteLine("2,");
codeBlocker.Outdent();
codeBlocker.WriteLine("]");
}),
},
},
};

Assert.AreEqual(
"""
public class C
{
public int[] Items
{
get => [
1,
2,
];
}
}

""".ReplaceLineEndings("\n"),
Render(type));
}

[TestMethod]
public void AnExpressionBodyEndingInALineTerminatorKeepsItsSemicolonOnTheLastLine()
{
PropertyTemplate property = new()
{
Type = "int",
Name = "Doubled",
Keywords = { "public" },
ExpressionBodyFactory = codeBlocker => codeBlocker.WriteLine("count * 2"),
};

Assert.AreEqual("public int Doubled => count * 2;\n", Render(property));
}

[TestMethod]
public void AMultiLineExpressionBodyKeepsABlankLineWithoutIndentingIt()
{
ClassTemplate type = new()
{
Name = "C",
Keywords = { "public" },
Members =
{
new PropertyTemplate
{
Type = "int",
Name = "X",
Keywords = { "public" },
ExpressionBodyFactory = codeBlocker =>
{
codeBlocker.WriteLine("a");
codeBlocker.NewLine();
codeBlocker.Write("+ b");
},
},
},
};

Assert.AreEqual(
"public class C\n{\n\tpublic int X => a\n\n\t+ b;\n}\n",
Render(type));
}

[TestMethod]
public void AnEmptyExpressionBodyStillTerminatesTheDeclaration()
{
PropertyTemplate property = new()
{
Type = "int",
Name = "X",
Keywords = { "public" },
ExpressionBodyFactory = _ => { },
};

Assert.AreEqual("public int X => ;\n", Render(property));
}

[TestMethod]
public void ABlockBodiedAccessorIsBracedAndIndented()
{
Expand Down
4 changes: 1 addition & 3 deletions CodeBlocker/Templates/AccessorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ public void WriteTo(CodeBlocker codeBlocker, string keyword)

case AccessorKind.Expression:
codeBlocker.Write(prefix);
codeBlocker.Write(" => ");
codeBlocker.Write(TemplateRendering.RenderFragment(codeBlocker, BodyFactory));
codeBlocker.WriteLine(";");
TemplateRendering.WriteExpressionBody(codeBlocker, BodyFactory);
break;

case AccessorKind.Block:
Expand Down
4 changes: 1 addition & 3 deletions CodeBlocker/Templates/PropertyTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ public override void WriteTo(CodeBlocker codeBlocker)

if (ExpressionBodyFactory is not null)
{
codeBlocker.Write(" => ");
codeBlocker.Write(TemplateRendering.RenderFragment(codeBlocker, ExpressionBodyFactory));
codeBlocker.WriteLine(";");
TemplateRendering.WriteExpressionBody(codeBlocker, ExpressionBodyFactory);
return;
}

Expand Down
49 changes: 49 additions & 0 deletions CodeBlocker/Templates/TemplateRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,53 @@ internal static void WriteBody(CodeBlocker codeBlocker, Action<CodeBlocker>? bod
codeBlocker.WriteLine();
SpliceFragment(codeBlocker, body);
}

/// <summary>
/// Writes an expression body — the arrow, the expression and its terminating semicolon — and
/// ends the line.
/// </summary>
/// <param name="codeBlocker">The <see cref="CodeBlocker"/> to write to.</param>
/// <param name="expressionFactory">
/// The callback that writes the expression, without <c>=&gt;</c> or a semicolon.
/// </param>
/// <remarks>
/// The first line of the expression stays on the declaration line, after the arrow, and every
/// following line is spliced at the current indent, so a multi-line switch expression, collection
/// expression or initialiser keeps the nesting it was written with. The semicolon goes on the last
/// line that has content, so a factory that ends with a line terminator does not leave it on a
/// line of its own.
/// </remarks>
internal static void WriteExpressionBody(CodeBlocker codeBlocker, Action<CodeBlocker>? expressionFactory)
{
string[] lines = SplitLines(codeBlocker, RenderFragment(codeBlocker, expressionFactory));

int last = lines.Length - 1;
while (last >= 0 && lines[last].Length == 0)
{
last--;
}

codeBlocker.Write(" => ");
if (last < 0)
{
codeBlocker.WriteLine(";");
return;
}

for (int i = 0; i <= last; i++)
{
if (i == last)
{
codeBlocker.WriteLine(lines[i] + ";");
}
else if (lines[i].Length == 0)
{
codeBlocker.NewLine();
}
else
{
codeBlocker.WriteLine(lines[i]);
}
}
}
}
Loading