What's wrong
CodeBlocker.NewLine() (CodeBlocker/CodeBlocker.cs:206) is:
public void NewLine() => IndentedTextWriter.WriteLineNoTabs(string.Empty);
IndentedTextWriter.WriteLineNoTabs does not re-arm the writer's pending-tabs flag. If the writer is part-way through a line when NewLine() is called, that line is ended, but whatever is written next starts at column 0.
The repository already knows about this hazard. TemplateRendering.WriteConstraints (lines 186–189) avoids NewLine() for exactly this reason. The public API has no such guard.
Reproduction
This was run against main at 88289f7.
Core API:
cb.WriteLine("class C");
using (new Scope(cb))
{
cb.Write("int x = 1;");
cb.NewLine();
cb.WriteLine("int y = 2;");
}
- Observed:
class C\n{\n\tint x = 1;\nint y = 2;\n}\n. int y is at column 0.
- Expected:
\tint y = 2;.
Templates: WriteExpressionBody (Templates/TemplateRendering.cs:274, :287-289) hits the same bug when the expression body's first rendered line is blank:
new PropertyTemplate
{
Type = "int", Name = "X", Keywords = { "public" },
ExpressionBodyFactory = e => { e.NewLine(); e.WriteLine("1 +"); e.Write("2"); }
}
- Observed inside a class:
\tpublic int X => \n1 +\n\t2;. There is a trailing space after =>, and 1 + is at column 0.
- The existing test
AMultiLineExpressionBodyKeepsABlankLineWithoutIndentingIt only covers a blank line in the middle of the body, where the writer is already at the start of a line.
Why it matters
Generated code comes out mis-indented. For source generators and formatters built on CodeBlocker, the output no longer matches .editorconfig expectations and makes noisy diffs. Callers cannot see that it will happen, because it depends on whether the previous call was Write or WriteLine.
Suggested fix
- Have
CodeBlocker track whether it is part-way through a line: set the flag after a non-empty Write, and clear it after WriteLine and NewLine.
- In
NewLine(), when part-way through a line, call IndentedTextWriter.WriteLine(). That writes no tabs in that state and re-arms the indent. Otherwise keep WriteLineNoTabs, so blank lines stay free of whitespace.
- The existing tests
NewLineShouldAddEmptyLine and BlankLineUsesTheConfiguredTerminator still pass under this change.
WriteExpressionBody should also skip leading blank lines, as it already skips trailing ones, so " => " isn't left dangling at the end of the declaration line.
- Add tests for both reproductions above.
What's wrong
CodeBlocker.NewLine()(CodeBlocker/CodeBlocker.cs:206) is:IndentedTextWriter.WriteLineNoTabsdoes not re-arm the writer's pending-tabs flag. If the writer is part-way through a line whenNewLine()is called, that line is ended, but whatever is written next starts at column 0.The repository already knows about this hazard.
TemplateRendering.WriteConstraints(lines 186–189) avoidsNewLine()for exactly this reason. The public API has no such guard.Reproduction
This was run against
mainat 88289f7.Core API:
class C\n{\n\tint x = 1;\nint y = 2;\n}\n.int yis at column 0.\tint y = 2;.Templates:
WriteExpressionBody(Templates/TemplateRendering.cs:274,:287-289) hits the same bug when the expression body's first rendered line is blank:\tpublic int X => \n1 +\n\t2;. There is a trailing space after=>, and1 +is at column 0.AMultiLineExpressionBodyKeepsABlankLineWithoutIndentingItonly covers a blank line in the middle of the body, where the writer is already at the start of a line.Why it matters
Generated code comes out mis-indented. For source generators and formatters built on CodeBlocker, the output no longer matches
.editorconfigexpectations and makes noisy diffs. Callers cannot see that it will happen, because it depends on whether the previous call wasWriteorWriteLine.Suggested fix
CodeBlockertrack whether it is part-way through a line: set the flag after a non-emptyWrite, and clear it afterWriteLineandNewLine.NewLine(), when part-way through a line, callIndentedTextWriter.WriteLine(). That writes no tabs in that state and re-arms the indent. Otherwise keepWriteLineNoTabs, so blank lines stay free of whitespace.NewLineShouldAddEmptyLineandBlankLineUsesTheConfiguredTerminatorstill pass under this change.WriteExpressionBodyshould also skip leading blank lines, as it already skips trailing ones, so" => "isn't left dangling at the end of the declaration line.