Skip to content

fix: deterministic LF line endings for generated files#1477

Draft
joaodinissf wants to merge 4 commits into
dsldevkit:masterfrom
joaodinissf:fix/lf-normalize-runtime-generators
Draft

fix: deterministic LF line endings for generated files#1477
joaodinissf wants to merge 4 commits into
dsldevkit:masterfrom
joaodinissf:fix/lf-normalize-runtime-generators

Conversation

@joaodinissf

@joaodinissf joaodinissf commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Problem

Generated-file line endings are platform-dependent: Xtext's default ILineSeparatorInformation returns System.getProperty("line.separator"), so any headless generation on Windows (MWE2 workflows, standalone builders, tests, analysis reports) emits CRLF — while the committed artifacts are LF. Every such run on Windows produces whole-file line-ending diffs: git churn, spurious rebuilds, and noisy PRs.

Fix

Make LF the deterministic separator for all machine-owned output:

  • LfLineSeparatorInformation (new, com.avaloq.tools.ddk.xtext.generator, exported): returns "\n". Bound via bindILineSeparatorInformation() in the runtime module of all seven DDK languages (Check, CheckCfg, Export, Expression, Format, Scope, Valid). Xtext's IWhitespaceInformationProvider.Default injects exactly this type and feeds it to LineSeparatorHarmonizer — the post-processor that owns the final bytes of every IFileSystemAccess write — so all headless generation becomes LF. In the IDE, the UI module's preference-based whitespace provider takes precedence, so workspace behavior is unchanged.
  • EMF Ecore generation workflows (ModelInference.mwe2, TypeModel.mwe2): lineDelimiter = "\n" on the EcoreGenerator components (complements the earlier codeConfig LF change for the Xtext language workflows).
  • KeywordAnalysisHelper: the diagnostic/report writers use an LfPrintWriter (a PrintWriter whose println() terminates with \n) instead of platform-terminated println.

Tests

  • CheckLineSeparatorBindingTest pins the Guice convention wiring (runtime injector resolves ILineSeparatorInformation to the LF binding — the identical binding method used by all seven modules).
  • AbstractCheckGenerationTestCase now asserts every generated file is CR-free, so all existing generation tests enforce determinism on every platform.
  • LfPrintWriterTest + LineEndingDeterminismTest cover the report writers and end-to-end determinism in the generator bundle.

Verification

  • Full local gate from ddk-parent: mvn -T 3C clean verify checkstyle:check pmd:check pmd:cpd-check spotbugs:checkBUILD SUCCESS, 363 tests / 0 failures / 0 errors (includes the four new LF tests), all static analysis clean.
  • Mechanism verified against the Xtext 2.43 sources: IWhitespaceInformationProvider.Default injects ILineSeparatorInformation and LineSeparatorHarmonizer applies it to every IFileSystemAccess write; the default being replaced is System.getProperty("line.separator").

🤖 Generated with Claude Code

The keyword diagnostic/report writers used PrintWriter.println, whose
terminator is the platform separator — the one raw-IO emission path
the FSA-level LF normalization never covers (dsldevkit#1345; revives the
KeywordAnalysisHelper piece of the closed dsldevkit#1354). All println(...)
overloads terminate via println(), so a minimal LfPrintWriter override
of that single method makes every call site emit LF; the report
builders themselves already append '\n' literals.

Assisted by Claude
ModelInference.mwe2 and TypeModel.mwe2 were the last MWE2 workflows
generating committed src-gen without an explicit lineDelimiter — their
three EcoreGenerator components fell back to the platform default, so
regenerating on Windows would produce CRLF diffs against the LF
policy. Completes dsldevkit#1413's coverage; CustomClassAwareEcoreGenerator
already forwards getLineDelimiter() into the EMF generator adapter.

Assisted by Claude
…atorInformation

Xtext's LineSeparatorHarmonizer post-processes every text
IFileSystemAccess write and owns the final bytes, but its default
ILineSeparatorInformation is System.lineSeparator() — so headless
builds (xtext-maven-plugin, MWE2) emit platform-dependent endings that
fight the repository's LF policy (.gitattributes '* text=auto eol=lf',
dsldevkit#1314) and rewrite generated files on other platforms. Bind
LfLineSeparatorInformation ('\n') in every DDK language runtime module
(Check, CheckCfg, Scope, Export, Format, Valid, Expression) so
generation is deterministic for all current and future generators of
these languages at the pipeline's actual enforcement point. In the IDE
the UI modules' preference/sensing provider still takes precedence, so
workspaces keep converging to the checked-out form. Trace-aware
JvmModel output is unaffected (its appendable is LF by construction
and the harmonizer deliberately skips traced content).

Completes the direction of dsldevkit#1331/dsldevkit#1413 at the root; tracked by dsldevkit#1345.

Assisted by Claude
Three layers, none of which existed before:
- LineEndingDeterminismTest: drives the real headless write path
  (JavaIoFileSystemAccess + LineSeparatorHarmonizer) with the LF
  binding and, as a control, with a '\r\n' binding — proving the
  post-processor follows the bound separator (the mechanism that made
  output platform-dependent) and that the LF binding normalizes CRLF,
  CR and LF alike.
- CheckLineSeparatorBindingTest: pins the Guice module-convention
  wiring by resolving ILineSeparatorInformation from the Check runtime
  injector; the other six language modules declare the identical
  binding method.
- AbstractCheckGenerationTestCase now asserts no generated file
  contains CR, upgrading every existing Check generation test into an
  emission regression guard; LfPrintWriterTest covers the raw-IO
  report writer's terminator.

Assisted by Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant