From 573e633f9afcc355c25d3b650c5cd56fbc19fc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Wed, 22 Jul 2026 14:38:34 +0200 Subject: [PATCH 1/4] fix(generator): emit LF explicitly in KeywordAnalysisHelper reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (#1345; revives the KeywordAnalysisHelper piece of the closed #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 --- .../parser/antlr/KeywordAnalysisHelper.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/com.avaloq.tools.ddk.xtext.generator/src/com/avaloq/tools/ddk/xtext/generator/parser/antlr/KeywordAnalysisHelper.java b/com.avaloq.tools.ddk.xtext.generator/src/com/avaloq/tools/ddk/xtext/generator/parser/antlr/KeywordAnalysisHelper.java index ebad409b0d..9e66c4a014 100644 --- a/com.avaloq.tools.ddk.xtext.generator/src/com/avaloq/tools/ddk/xtext/generator/parser/antlr/KeywordAnalysisHelper.java +++ b/com.avaloq.tools.ddk.xtext.generator/src/com/avaloq/tools/ddk/xtext/generator/parser/antlr/KeywordAnalysisHelper.java @@ -179,7 +179,7 @@ private boolean hasLetters(final String keyword) { */ public void printViolations(final String srcGenPath) { String fileName = getKeywordsDiagnosticReportFileName(srcGenPath); - try (PrintWriter writer = new PrintWriter(new File(fileName), StandardCharsets.UTF_8)) { + try (PrintWriter writer = new LfPrintWriter(new File(fileName))) { writer.println("Please check in this file, so a diff can be used to detect unexpected changes"); writer.println(); writer.println(" identifiers rejected - are not listed in MWE2 file as reserved words"); @@ -439,11 +439,11 @@ public List getAllGrammars() { public void printReport(final String srcGenPath) { try { String fileName = getReportFileName(srcGenPath); - try (PrintWriter writer = new PrintWriter(new File(fileName), StandardCharsets.UTF_8)) { + try (PrintWriter writer = new LfPrintWriter(new File(fileName))) { writer.print(report.build()); } String docuFileName = getDocFileName(srcGenPath); - try (PrintWriter docuWriter = new PrintWriter(new File(docuFileName), StandardCharsets.UTF_8)) { + try (PrintWriter docuWriter = new LfPrintWriter(new File(docuFileName))) { docuWriter.print(new CombinedGrammarReportBuilder(grammarExtensions).getDocumentation(grammar, parserRules, enumRules)); } LOGGER.info("report on keywords is written into {}", fileName); @@ -506,6 +506,24 @@ private String getDocFileSimpleName() { private String getDocFileRelativeName() { return getAntlrrFileName() + "CombinedGrammar.html"; } + + /** + * A {@link PrintWriter} whose line terminator is always LF ({@code \n}) instead of the + * platform separator, keeping the generated reports byte-identical on every OS (#1345). + * All {@code println(...)} overloads are specified to terminate via {@link #println()}, + * so overriding it alone covers every call site. + */ + private static final class LfPrintWriter extends PrintWriter { + + LfPrintWriter(final File file) throws IOException { + super(file, StandardCharsets.UTF_8); + } + + @Override + public void println() { + write('\n'); + } + } } /* Copyright (c) Avaloq Group AG */ From 2fb43ec9a6d45e9c402810a16dab5d0dfded4f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Wed, 22 Jul 2026 14:41:05 +0200 Subject: [PATCH 2/4] chore: emit LF from the EMF Ecore generation workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #1413's coverage; CustomClassAwareEcoreGenerator already forwards getLineDelimiter() into the EMF generator adapter. Assisted by Claude --- .../src/com/avaloq/tools/ddk/workflow/ModelInference.mwe2 | 1 + .../src/com/avaloq/tools/ddk/workflow/TypeModel.mwe2 | 2 ++ 2 files changed, 3 insertions(+) diff --git a/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/ModelInference.mwe2 b/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/ModelInference.mwe2 index cf4667f4cb..cb59e7479a 100644 --- a/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/ModelInference.mwe2 +++ b/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/ModelInference.mwe2 @@ -29,6 +29,7 @@ Workflow { component = com.avaloq.tools.ddk.xtext.generator.util.CustomClassAwareEcoreGenerator { genModel = "platform:/resource/${projectName}/model/ModelInference.genmodel" generateEdit = false + lineDelimiter = "\n" } } diff --git a/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/TypeModel.mwe2 b/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/TypeModel.mwe2 index 916c684db5..7c41561492 100644 --- a/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/TypeModel.mwe2 +++ b/com.avaloq.tools.ddk.workflow/src/com/avaloq/tools/ddk/workflow/TypeModel.mwe2 @@ -31,10 +31,12 @@ Workflow { component = EcoreGenerator auto-inject { genModel = "platform:/resource/${projectName}/model/TypeModel.genmodel" generateEdit=true + lineDelimiter = "\n" } component = EcoreGenerator auto-inject { genModel = "platform:/resource/${projectName}/model/BuiltInTypeModel.genmodel" generateEdit=false + lineDelimiter = "\n" } } From 5eedd734b3e12241adca1f2da14ea10fa53ec47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 23 Jul 2026 20:16:04 +0200 Subject: [PATCH 3/4] fix: deterministic LF line endings for generated files via ILineSeparatorInformation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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', #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 #1331/#1413 at the root; tracked by #1345. Assisted by Claude --- .../tools/ddk/check/CheckRuntimeModule.java | 12 ++++++ .../ddk/checkcfg/CheckCfgRuntimeModule.java | 12 ++++++ .../ddk/xtext/export/ExportRuntimeModule.java | 12 ++++++ .../expression/ExpressionRuntimeModule.java | 12 ++++++ .../ddk/xtext/format/FormatRuntimeModule.java | 12 ++++++ .../ddk/xtext/scope/ScopeRuntimeModule.java | 12 ++++++ .../ddk/xtext/valid/ValidRuntimeModule.java | 12 ++++++ .../META-INF/MANIFEST.MF | 1 + .../generator/LfLineSeparatorInformation.java | 37 +++++++++++++++++++ 9 files changed, 122 insertions(+) create mode 100644 com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/generator/LfLineSeparatorInformation.java diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/CheckRuntimeModule.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/CheckRuntimeModule.java index e48e5a19c8..14abe82698 100644 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/CheckRuntimeModule.java +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/CheckRuntimeModule.java @@ -11,6 +11,7 @@ package com.avaloq.tools.ddk.check; import org.eclipse.xtext.documentation.IEObjectDocumentationProvider; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.generator.IOutputConfigurationProvider; import org.eclipse.xtext.linking.ILinkingService; import org.eclipse.xtext.naming.IQualifiedNameProvider; @@ -40,6 +41,7 @@ import com.avaloq.tools.ddk.check.scoping.ExtensionPointAwareScopeProvider; import com.avaloq.tools.ddk.check.typing.CheckExpressionHelper; import com.avaloq.tools.ddk.check.typing.CheckTypeComputer; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; import com.google.inject.name.Names; @@ -48,6 +50,16 @@ */ @SuppressWarnings({"PMD.CouplingBetweenObjects", "restriction"}) public class CheckRuntimeModule extends com.avaloq.tools.ddk.check.AbstractCheckRuntimeModule { + + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } @Override public Class bindXtextResource() { return CheckBatchLinkableResource.class; diff --git a/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/CheckCfgRuntimeModule.java b/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/CheckCfgRuntimeModule.java index 5d0cd4126f..0a1520115d 100644 --- a/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/CheckCfgRuntimeModule.java +++ b/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/CheckCfgRuntimeModule.java @@ -11,6 +11,7 @@ package com.avaloq.tools.ddk.checkcfg; import org.eclipse.xtext.conversion.IValueConverterService; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.generator.IGenerator; import org.eclipse.xtext.naming.IQualifiedNameProvider; import org.eclipse.xtext.resource.ILocationInFileProvider; @@ -26,6 +27,7 @@ import com.avaloq.tools.ddk.checkcfg.resource.CheckCfgLocationInFileProvider; import com.avaloq.tools.ddk.checkcfg.scoping.CheckCfgBatchLinkingService; import com.avaloq.tools.ddk.checkcfg.scoping.CheckCfgScopeProvider; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; import com.google.inject.name.Names; @@ -34,6 +36,16 @@ */ public class CheckCfgRuntimeModule extends com.avaloq.tools.ddk.checkcfg.AbstractCheckCfgRuntimeModule { + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } + /** * Custom location in file provider used for revealing and highlighting a model element in the editor. *

diff --git a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java index 34463c042f..cb2953e09e 100644 --- a/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.export/src/com/avaloq/tools/ddk/xtext/export/ExportRuntimeModule.java @@ -10,12 +10,14 @@ *******************************************************************************/ package com.avaloq.tools.ddk.xtext.export; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.generator.IOutputConfigurationProvider; import org.eclipse.xtext.naming.IQualifiedNameConverter; import com.avaloq.tools.ddk.xtext.export.conversion.ExportValueConverterService; import com.avaloq.tools.ddk.xtext.export.generator.ExportOutputConfigurationProvider; import com.avaloq.tools.ddk.xtext.export.naming.ExportQualifiedNameConverter; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; /** @@ -23,6 +25,16 @@ */ public class ExportRuntimeModule extends com.avaloq.tools.ddk.xtext.export.AbstractExportRuntimeModule { + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } + /** * {@inheritDoc} */ diff --git a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/ExpressionRuntimeModule.java b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/ExpressionRuntimeModule.java index ebb83dabe0..c04599fc1b 100644 --- a/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/ExpressionRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.expression/src/com/avaloq/tools/ddk/xtext/expression/ExpressionRuntimeModule.java @@ -11,8 +11,10 @@ package com.avaloq.tools.ddk.xtext.expression; import org.eclipse.xtext.conversion.IValueConverterService; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import com.avaloq.tools.ddk.xtext.expression.conversion.ExpressionValueConverterService; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; /** @@ -20,6 +22,16 @@ */ public class ExpressionRuntimeModule extends AbstractExpressionRuntimeModule { + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } + @Override public Class bindIValueConverterService() { return ExpressionValueConverterService.class; diff --git a/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/FormatRuntimeModule.java b/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/FormatRuntimeModule.java index 98f0db796a..7dc025be4a 100644 --- a/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/FormatRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/FormatRuntimeModule.java @@ -11,6 +11,7 @@ package com.avaloq.tools.ddk.xtext.format; import org.eclipse.xtext.conversion.IValueConverterService; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.generator.IOutputConfigurationProvider; import org.eclipse.xtext.linking.ILinkingService; import org.eclipse.xtext.linking.LinkingScopeProviderBinding; @@ -34,6 +35,7 @@ import com.avaloq.tools.ddk.xtext.format.resource.FormatResourceDescriptionStrategy; import com.avaloq.tools.ddk.xtext.format.scoping.FormatLinkingService; import com.avaloq.tools.ddk.xtext.format.scoping.FormatScopeProvider; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; import com.google.inject.Binder; import com.google.inject.name.Names; @@ -42,6 +44,16 @@ */ public class FormatRuntimeModule extends AbstractFormatRuntimeModule { + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } + @Override public Class bindXtextResource() { return FormatResource.class; diff --git a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java index 09c5499475..2e4d93d9fa 100644 --- a/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.scope/src/com/avaloq/tools/ddk/xtext/scope/ScopeRuntimeModule.java @@ -11,11 +11,13 @@ package com.avaloq.tools.ddk.xtext.scope; import org.eclipse.xtext.conversion.IValueConverterService; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.linking.ILinkingService; import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.resource.IDefaultResourceDescriptionStrategy; import org.eclipse.xtext.resource.ILocationInFileProvider; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; import com.avaloq.tools.ddk.xtext.scope.conversion.ScopeValueConverterService; import com.avaloq.tools.ddk.xtext.scope.linking.ScopeLinkingService; import com.avaloq.tools.ddk.xtext.scope.naming.ScopeQualifiedNameConverter; @@ -28,6 +30,16 @@ */ public class ScopeRuntimeModule extends AbstractScopeRuntimeModule { + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } + @Override public Class bindIValueConverterService() { return ScopeValueConverterService.class; diff --git a/com.avaloq.tools.ddk.xtext.valid/src/com/avaloq/tools/ddk/xtext/valid/ValidRuntimeModule.java b/com.avaloq.tools.ddk.xtext.valid/src/com/avaloq/tools/ddk/xtext/valid/ValidRuntimeModule.java index e7722bb99a..32d3959ac2 100644 --- a/com.avaloq.tools.ddk.xtext.valid/src/com/avaloq/tools/ddk/xtext/valid/ValidRuntimeModule.java +++ b/com.avaloq.tools.ddk.xtext.valid/src/com/avaloq/tools/ddk/xtext/valid/ValidRuntimeModule.java @@ -11,9 +11,11 @@ package com.avaloq.tools.ddk.xtext.valid; import org.eclipse.xtext.conversion.IValueConverterService; +import org.eclipse.xtext.formatting.ILineSeparatorInformation; import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.scoping.IScopeProvider; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; import com.avaloq.tools.ddk.xtext.valid.conversion.ValidValueConverterService; import com.avaloq.tools.ddk.xtext.valid.naming.ValidQualifiedNameConverter; import com.avaloq.tools.ddk.xtext.valid.scoping.ValidScopeProvider; @@ -24,6 +26,16 @@ */ public class ValidRuntimeModule extends com.avaloq.tools.ddk.xtext.valid.AbstractValidRuntimeModule { + /** + * Binds the generated-file line separator to LF so code generation is deterministic + * across platforms; headless builds otherwise fall back to the platform separator. + * + * @return the LF {@link ILineSeparatorInformation} implementation, never {@code null} + */ + public Class bindILineSeparatorInformation() { + return LfLineSeparatorInformation.class; + } + @Override public Class bindIScopeProvider() { return ValidScopeProvider.class; diff --git a/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF index 460039bfdf..aff173e15a 100644 --- a/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF @@ -25,6 +25,7 @@ Export-Package: com.avaloq.tools.ddk.xtext.build, com.avaloq.tools.ddk.xtext.formatting, com.avaloq.tools.ddk.xtext.formatting.locators, com.avaloq.tools.ddk.xtext.formatting.locators.conditional, + com.avaloq.tools.ddk.xtext.generator, com.avaloq.tools.ddk.xtext.grammar, com.avaloq.tools.ddk.xtext.layered, com.avaloq.tools.ddk.xtext.linking, diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/generator/LfLineSeparatorInformation.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/generator/LfLineSeparatorInformation.java new file mode 100644 index 0000000000..138460ecb1 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/generator/LfLineSeparatorInformation.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ + +package com.avaloq.tools.ddk.xtext.generator; + +import org.eclipse.xtext.formatting.ILineSeparatorInformation; + + +/** + * Fixes the generated-file line separator to LF ({@code \n}). + *

+ * Generated files are machine-owned, so their line endings must be deterministic across + * platforms; git stores text blobs as LF, making LF output byte-stable against the + * repository in every checkout configuration. Binding this in a language's runtime + * module makes Xtext's {@code LineSeparatorHarmonizer} — the post-processor that owns the + * final bytes of every {@code IFileSystemAccess} write — target LF in headless builds, + * replacing the platform-dependent {@code System.lineSeparator()} default. In the IDE the + * UI module's preference/sensing-based provider still takes precedence, so workspaces keep + * converging to the checked-out form. + *

+ */ +public class LfLineSeparatorInformation implements ILineSeparatorInformation { + + @Override + public String getLineSeparator() { + return "\n"; //$NON-NLS-1$ + } + +} From de2642cfda389e2ce701cfa294a57c01e0a25d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 23 Jul 2026 20:21:06 +0200 Subject: [PATCH 4/4] test: guarantee LF line endings for generated files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../META-INF/MANIFEST.MF | 1 + .../test/AbstractCheckGenerationTestCase.java | 5 ++ .../test/CheckLineSeparatorBindingTest.java | 47 ++++++++++++ .../check/test/core/CheckCoreTestSuite.java | 2 + .../META-INF/MANIFEST.MF | 1 + .../test/generator/GeneratorTestSuite.java | 2 + .../test/generator/LfPrintWriterTest.java | 55 ++++++++++++++ .../generator/LineEndingDeterminismTest.java | 71 +++++++++++++++++++ 8 files changed, 184 insertions(+) create mode 100644 com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckLineSeparatorBindingTest.java create mode 100644 com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LfPrintWriterTest.java create mode 100644 com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LineEndingDeterminismTest.java diff --git a/com.avaloq.tools.ddk.check.core.test/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.check.core.test/META-INF/MANIFEST.MF index 5a2d5feef6..1ebe36f35b 100644 --- a/com.avaloq.tools.ddk.check.core.test/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.check.core.test/META-INF/MANIFEST.MF @@ -7,6 +7,7 @@ Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Require-Bundle: com.avaloq.tools.ddk.check.core, + com.avaloq.tools.ddk.xtext, com.avaloq.tools.ddk.xtext.test.core, com.avaloq.tools.ddk.check.ui, org.eclipse.xtext, diff --git a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/AbstractCheckGenerationTestCase.java b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/AbstractCheckGenerationTestCase.java index 34a39b048c..e0a30b2573 100644 --- a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/AbstractCheckGenerationTestCase.java +++ b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/AbstractCheckGenerationTestCase.java @@ -11,6 +11,7 @@ package com.avaloq.tools.ddk.check.core.test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -96,6 +97,10 @@ public List generateAndCompile(final InputStream sourceStream) { fsa.getOutputConfigurations().put(output.getName(), output); } generator.doGenerate(res, fsa); + // Generated content must be line-ending-deterministic (LF) on every platform. + for (java.util.Map.Entry file : fsa.getTextFiles().entrySet()) { + assertEquals(-1, file.getValue().toString().indexOf('\r'), "generated file must not contain CR: " + file.getKey()); + } // We now should have a number of files. String baseName = root.getPackageName() + '.' + root.getName(); String basePath = baseName.replace('.', '/'); diff --git a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckLineSeparatorBindingTest.java b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckLineSeparatorBindingTest.java new file mode 100644 index 0000000000..d4f2a0f983 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckLineSeparatorBindingTest.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.check.core.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import org.eclipse.xtext.formatting.ILineSeparatorInformation; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.avaloq.tools.ddk.check.CheckInjectorProvider; +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; +import com.google.inject.Inject; + + +/** + * Guarantees the Check runtime injector resolves {@link ILineSeparatorInformation} to the LF + * binding, so headless generation is line-ending-deterministic. The other DDK language + * runtime modules declare the identical binding method; this test pins the Guice + * module-convention wiring they all rely on. + */ +@InjectWith(CheckInjectorProvider.class) +@ExtendWith(InjectionExtension.class) +@SuppressWarnings("nls") +public class CheckLineSeparatorBindingTest { + + @Inject + private ILineSeparatorInformation lineSeparatorInformation; + + @Test + public void runtimeInjectorBindsLfLineSeparator() { + assertInstanceOf(LfLineSeparatorInformation.class, lineSeparatorInformation, "Check runtime injector must bind the LF separator information"); + assertEquals("\n", lineSeparatorInformation.getLineSeparator(), "bound separator must be LF"); + } + +} diff --git a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java index 42aa8b6013..54bb16c5f6 100644 --- a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java +++ b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java @@ -15,6 +15,7 @@ import com.avaloq.tools.ddk.check.core.generator.IssueCodeValueTest; import com.avaloq.tools.ddk.check.core.test.BasicModelTest; +import com.avaloq.tools.ddk.check.core.test.CheckLineSeparatorBindingTest; import com.avaloq.tools.ddk.check.core.test.BugAig1314; import com.avaloq.tools.ddk.check.core.test.BugAig830; import com.avaloq.tools.ddk.check.core.test.BugDsl27; @@ -35,6 +36,7 @@ // @Format-Off IssueCodeValueTest.class, BasicModelTest.class, + CheckLineSeparatorBindingTest.class, BugAig830.class, CheckScopingTest.class, CheckValidationTest.class, diff --git a/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF index d986bbc123..c3a231f0cd 100644 --- a/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.generator.test/META-INF/MANIFEST.MF @@ -8,6 +8,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Fragment-Host: com.avaloq.tools.ddk.xtext.generator Require-Bundle: com.avaloq.tools.ddk.test.core, + com.avaloq.tools.ddk.xtext, com.avaloq.tools.ddk.xtext.expression, com.avaloq.tools.ddk.xtext.test.core, org.eclipse.xtend, diff --git a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java index efbd202e46..e4f7850f44 100644 --- a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java +++ b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/GeneratorTestSuite.java @@ -28,6 +28,8 @@ @SelectClasses({ // @Format-Off CodeGenerationXTest.class, + LfPrintWriterTest.class, + LineEndingDeterminismTest.class, CompilationContextTest.class, ExpressionsExtentionsTest.class, EClassComparatorTest.class, diff --git a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LfPrintWriterTest.java b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LfPrintWriterTest.java new file mode 100644 index 0000000000..47cd547e2d --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LfPrintWriterTest.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.generator.test.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.avaloq.tools.ddk.xtext.generator.parser.antlr.KeywordAnalysisHelper; + + +/** + * Guarantees that {@code KeywordAnalysisHelper}'s report writer terminates lines with LF on + * every platform. The keyword reports are committed to git, so a platform-dependent + * {@link PrintWriter#println()} would rewrite them on every Windows build. All + * {@code println(...)} overloads are specified to terminate via {@code println()}, so + * asserting the no-argument terminator covers every call site. + */ +@SuppressWarnings("nls") +public class LfPrintWriterTest { + + @TempDir + private File tempDir; + + @Test + public void lfPrintWriterTerminatesWithLfOnly() throws Exception { + File file = new File(tempDir, "report.txt"); + Class lfPrintWriter = Class.forName(KeywordAnalysisHelper.class.getName() + "$LfPrintWriter", true, KeywordAnalysisHelper.class.getClassLoader()); + Constructor constructor = lfPrintWriter.getDeclaredConstructor(File.class); + constructor.setAccessible(true); + try (PrintWriter writer = (PrintWriter) constructor.newInstance(file)) { + writer.println("first"); + writer.println(); + writer.print("second"); + writer.println(42); + } + assertEquals("first\n\nsecond42\n", Files.readString(file.toPath(), StandardCharsets.UTF_8), "every println termination must be a bare LF"); + } + +} diff --git a/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LineEndingDeterminismTest.java b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LineEndingDeterminismTest.java new file mode 100644 index 0000000000..e784b549fd --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.generator.test/src/com/avaloq/tools/ddk/xtext/generator/test/generator/LineEndingDeterminismTest.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.generator.test.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.eclipse.xtext.formatting.ILineSeparatorInformation; +import org.eclipse.xtext.generator.JavaIoFileSystemAccess; +import org.eclipse.xtext.parser.IEncodingProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.avaloq.tools.ddk.xtext.generator.LfLineSeparatorInformation; +import com.google.inject.Guice; +import com.google.inject.Injector; + + +/** + * Guarantees that generated-file line endings are decided by the bound + * {@link ILineSeparatorInformation} — the pipeline's actual enforcement point — and that the + * {@link LfLineSeparatorInformation} binding normalizes every separator style to LF on the + * headless ({@link JavaIoFileSystemAccess}) write path, regardless of host platform. + */ +@SuppressWarnings("nls") +public class LineEndingDeterminismTest { + + private static final String MIXED_CONTENT = "a\r\nb\rc\nd"; + + @TempDir + private File tempDir; + + @Test + public void lfBindingNormalizesAllSeparatorStyles() throws IOException { + assertEquals("a\nb\nc\nd", generateAndRead(new LfLineSeparatorInformation()), "LF binding must normalize CRLF, CR and LF to LF"); + } + + @Test + public void harmonizerHonorsConfiguredSeparator() throws IOException { + assertEquals("a\r\nb\r\nc\r\nd", generateAndRead(() -> "\r\n"), "the post-processor must follow the bound separator; this is the mechanism that made headless output platform-dependent before the LF binding"); + } + + @Test + public void lfLineSeparatorInformationReturnsLf() { + assertEquals("\n", new LfLineSeparatorInformation().getLineSeparator(), "LfLineSeparatorInformation must return LF"); + } + + private String generateAndRead(final ILineSeparatorInformation separatorInformation) throws IOException { + Injector injector = Guice.createInjector(binder -> { + binder.bind(ILineSeparatorInformation.class).toInstance(separatorInformation); + binder.bind(IEncodingProvider.class).to(IEncodingProvider.Runtime.class); + }); + JavaIoFileSystemAccess fsa = injector.getInstance(JavaIoFileSystemAccess.class); + fsa.setOutputPath(tempDir.getAbsolutePath()); + fsa.generateFile("Sample.txt", MIXED_CONTENT); + return Files.readString(new File(tempDir, "Sample.txt").toPath(), StandardCharsets.UTF_8); + } + +}