Skip to content

Make output line endings deterministic and configurable #81

Description

@matt-edmondson

Problem

CodeBlocker writes through System.CodeDom.Compiler.IndentedTextWriter, which terminates lines with Environment.NewLine. The same input therefore produces CRLF on Windows and LF on Linux.

That is fine for throwaway output, but it breaks the main use case for a code generator: committed generated source. ktsu.Semantics commits its generator output and verifies it in CI, so it had to add a post-processing pass purely to undo this (GeneratedSource.cs):

internal static string NormalizeLineEndings(string source) =>
    source.Replace("\r\n", "\n").Replace("\n", "\r\n");

Every consumer that cares about reproducible output has to rediscover and re-implement that workaround.

The dependency also leaks into consumers in a second, less obvious way: Semantics' template model splits rendered bodies on Environment.NewLine to decide whether a method body is single- or multi-line, so a body assembled on one OS is laid out differently on another.

Proposal

Give CodeBlocker an explicit newline, defaulting to something stable rather than to the host's:

  • Add a NewLineString option to the constructors and Create factories, alongside the existing indentString.
  • Route all line termination through it (set TextWriter.CoreNewLine, or wrap the writer) instead of leaving IndentedTextWriter on Environment.NewLine.
  • Expose it as a read-only property (mirroring IndentString) so nested/child CodeBlocker instances can inherit it.
  • Decide and document the default. "\n" is the reproducible choice; Environment.NewLine is the back-compatible one. If the default changes, call it out in CHANGELOG.md as a behavioural break.

Acceptance criteria

  • The same sequence of Write/WriteLine/Scope calls produces byte-identical output on Windows and Linux.
  • A custom newline can be supplied at construction and is used for every line ending, including those emitted by Scope and NewLine().
  • Tests assert exact bytes (not Environment.NewLine-relative comparisons) for at least one nested-scope fixture.
  • README.md documents the option and the default.

Context

Part of ktsu-dev/Semantics#181 — extracting the code-generation stack out of Semantics.SourceGenerators so it can be shared. Once this lands, Semantics can delete GeneratedSource.NormalizeLineEndings.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions