What's wrong
TemplateRendering.SpliceFragment (CodeBlocker/Templates/TemplateRendering.cs:73-86) splits a rendered body on NewLineString and writes each piece back with WriteLine, so every line picks up the enclosing indent. This path handles method, constructor and operator bodies (WriteBody, line 244) and block accessors (AccessorTemplate.cs:411). Since #112 it also handles expression bodies (WriteExpressionBody, lines 287-301).
That is only safe if a line break never falls inside a token. A multi-line verbatim string literal (@"...", $@"...", @$"...") keeps its whitespace literally. Adding tabs to its continuation lines therefore changes the string's runtime value, and nothing reports it because the code still compiles.
Writing the same text directly through CodeBlocker preserves it: IndentedTextWriter.WriteLine(string) does not indent after an embedded newline. The problem is specific to the template layer.
Repro (HEAD d04adc9)
var cls = new ClassTemplate { Name = "C" };
cls.Members.Add(new MethodTemplate {
Name = "M", Type = "string",
BodyFactory = b => { b.WriteLine("{"); b.Indent(); b.WriteLine("return @\"line1\nline2\";"); b.Outdent(); b.WriteLine("}"); },
});
using var cb = CodeBlocker.Create();
cb.WriteLine("namespace N");
using (new Scope(cb)) { cb.AddClass(cls); }
Output (→ = tab):
→→string M()
→→{
→→→return @"line1
→→line2";
→→}
M() now returns "line1\n\t\tline2" instead of "line1\nline2".
For comparison, cb.WriteLine("string s = @\"a\nb\";") inside a Scope, written directly, puts b"; at column 0, which is correct.
Raw string literals (""") are not affected, because the closing delimiter is indented by the same amount and the compiler strips it. The problem is limited to verbatim strings, interpolated verbatim strings included.
Why it matters
Generators often embed multi-line text (SQL, templates, help text, resource contents), and verbatim strings are the usual way to do that on older language versions. The corruption is silent: the output compiles, and golden files simply record the wrong value. The point of the library is that callers don't have to think about indentation, and the template model is the recommended path (CLAUDE.md: "route it through TemplateRendering.SpliceFragment"), so it shouldn't change what the generated program does.
Suggested fix
Make SplitLines/SpliceFragment aware of line breaks inside verbatim strings. A small scanner over the fragment would track:
// and /* */ comments
- char literals
- regular strings with
\ escapes
- verbatim strings with
"" escapes
- raw strings, by counting quotes
A terminator that falls inside a verbatim literal then stays part of the current line instead of becoming a split point. Continuation lines are written unchanged, using WriteLineNoTabs and then re-arming the pending tabs the way NewLine() now does.
An alternative that needs no scanner is to document the limitation on BodyFactory/ExpressionBodyFactory and recommend raw string literals. The scanner is the better option, because template output would then match direct CodeBlocker output.
Acceptance criteria
- A
MethodTemplate, AccessorTemplate.Block or ExpressionBodyFactory whose body contains @"a\nb" renders the continuation lines byte-for-byte as the factory wrote them.
- Ordinary multi-line bodies and multi-line raw string literals are still re-indented as they are today, and the existing golden tests are unchanged.
- A regression test covers the literal's continuation line, by compiling it or at least by string comparison.
What's wrong
TemplateRendering.SpliceFragment(CodeBlocker/Templates/TemplateRendering.cs:73-86) splits a rendered body onNewLineStringand writes each piece back withWriteLine, so every line picks up the enclosing indent. This path handles method, constructor and operator bodies (WriteBody, line 244) and block accessors (AccessorTemplate.cs:411). Since #112 it also handles expression bodies (WriteExpressionBody, lines 287-301).That is only safe if a line break never falls inside a token. A multi-line verbatim string literal (
@"...",$@"...",@$"...") keeps its whitespace literally. Adding tabs to its continuation lines therefore changes the string's runtime value, and nothing reports it because the code still compiles.Writing the same text directly through
CodeBlockerpreserves it:IndentedTextWriter.WriteLine(string)does not indent after an embedded newline. The problem is specific to the template layer.Repro (HEAD d04adc9)
Output (
→= tab):M()now returns"line1\n\t\tline2"instead of"line1\nline2".For comparison,
cb.WriteLine("string s = @\"a\nb\";")inside aScope, written directly, putsb";at column 0, which is correct.Raw string literals (
""") are not affected, because the closing delimiter is indented by the same amount and the compiler strips it. The problem is limited to verbatim strings, interpolated verbatim strings included.Why it matters
Generators often embed multi-line text (SQL, templates, help text, resource contents), and verbatim strings are the usual way to do that on older language versions. The corruption is silent: the output compiles, and golden files simply record the wrong value. The point of the library is that callers don't have to think about indentation, and the template model is the recommended path (CLAUDE.md: "route it through
TemplateRendering.SpliceFragment"), so it shouldn't change what the generated program does.Suggested fix
Make
SplitLines/SpliceFragmentaware of line breaks inside verbatim strings. A small scanner over the fragment would track://and/* */comments\escapes""escapesA terminator that falls inside a verbatim literal then stays part of the current line instead of becoming a split point. Continuation lines are written unchanged, using
WriteLineNoTabsand then re-arming the pending tabs the wayNewLine()now does.An alternative that needs no scanner is to document the limitation on
BodyFactory/ExpressionBodyFactoryand recommend raw string literals. The scanner is the better option, because template output would then match directCodeBlockeroutput.Acceptance criteria
MethodTemplate,AccessorTemplate.BlockorExpressionBodyFactorywhose body contains@"a\nb"renders the continuation lines byte-for-byte as the factory wrote them.