From 095fc3ae624fd46c4d6ce39cf2f7c51fc820df8f 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 Co-Authored-By: Claude Fable 5 --- .../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 5fcb29f0ab095b22cb33a9385e63d8431bbdf42a 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 Xtext and Ecore generation workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModelInference.mwe2 and TypeModel.mwe2 were 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 output against the LF policy. CustomClassAwareEcoreGenerator already forwards getLineDelimiter() into the EMF generator adapter. GenerateTestLanguage.mwe2 and GenerateHelloWorld.mwe2 went further in the wrong direction: they pinned lineDelimiter = "\r\n", emitting CRLF unconditionally on every host while their committed src-gen checks out as LF (.gitattributes: * text=auto eol=lf) — so any regeneration rewrote both trees wholesale. Pin them to "\n" like every other DDK workflow. Completes #1413's coverage. Assisted by Claude Co-Authored-By: Claude Fable 5 --- .../src/com/avaloq/tools/ddk/check/GenerateTestLanguage.mwe2 | 2 +- .../avaloq/tools/ddk/sample/helloworld/GenerateHelloWorld.mwe2 | 2 +- .../src/com/avaloq/tools/ddk/workflow/ModelInference.mwe2 | 1 + .../src/com/avaloq/tools/ddk/workflow/TypeModel.mwe2 | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/com.avaloq.tools.ddk.check.test.runtime/src/com/avaloq/tools/ddk/check/GenerateTestLanguage.mwe2 b/com.avaloq.tools.ddk.check.test.runtime/src/com/avaloq/tools/ddk/check/GenerateTestLanguage.mwe2 index 558b6bd1ec..0f06c7d682 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/src/com/avaloq/tools/ddk/check/GenerateTestLanguage.mwe2 +++ b/com.avaloq.tools.ddk.check.test.runtime/src/com/avaloq/tools/ddk/check/GenerateTestLanguage.mwe2 @@ -50,7 +50,7 @@ Workflow { } code = { encoding = "UTF-8" - lineDelimiter = "\r\n" + lineDelimiter = "\n" fileHeader = "/*\n * generated by Xtext\n */" } } diff --git a/com.avaloq.tools.ddk.sample.helloworld/src/com/avaloq/tools/ddk/sample/helloworld/GenerateHelloWorld.mwe2 b/com.avaloq.tools.ddk.sample.helloworld/src/com/avaloq/tools/ddk/sample/helloworld/GenerateHelloWorld.mwe2 index b3c2ca89a1..be44e31ce2 100644 --- a/com.avaloq.tools.ddk.sample.helloworld/src/com/avaloq/tools/ddk/sample/helloworld/GenerateHelloWorld.mwe2 +++ b/com.avaloq.tools.ddk.sample.helloworld/src/com/avaloq/tools/ddk/sample/helloworld/GenerateHelloWorld.mwe2 @@ -29,7 +29,7 @@ Workflow { } code = { encoding = "UTF-8" - lineDelimiter = "\r\n" + lineDelimiter = "\n" fileHeader = "/*\n * generated by Xtext\n */" preferXtendStubs = false } 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 6f1ea9ff824b042bc56367be05b70d9ff60d3849 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 IFilePostProcessor (LineSeparatorHarmonizer, or the trace-preserving TraceAwarePostProcessor for Xbase languages) post-processes text IFileSystemAccess writes, 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 IWhitespaceInformationProvider still takes precedence for file writes, so workspaces keep converging to the checked-out form. The class lives in the exported com.avaloq.tools.ddk.xtext.formatting package (alongside its interface's home in Xtext) — putting it in a package named "generator" would split the package already exported by the com.avaloq.tools.ddk.xtext.generator bundle. With the binding at the enforcement point, Check/CheckCfg's LfNormalizingFileSystemAccess wrapper is redundant and is removed: it never controlled the final bytes (the post-processor runs after it), and stringifying the JvmModelGenerator's TreeAppendable destroyed the ITraceRegionProvider identity that trace-aware post-processing relies on, silently dropping trace regions for CRLF content. Completes the direction of #1331/#1413 at the root; tracked by #1345. Assisted by Claude Co-Authored-By: Claude Fable 5 --- .../tools/ddk/check/CheckRuntimeModule.java | 13 ++ .../ddk/check/generator/CheckGenerator.xtend | 12 +- .../LfNormalizingFileSystemAccess.java | 126 ------------------ .../ddk/checkcfg/CheckCfgRuntimeModule.java | 12 ++ .../checkcfg/generator/CheckCfgGenerator.java | 5 +- .../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 ++ .../LfLineSeparatorInformation.java | 38 ++++++ 11 files changed, 129 insertions(+), 137 deletions(-) delete mode 100644 com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/LfNormalizingFileSystemAccess.java create mode 100644 com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/formatting/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 e5e5a6eb8f..18f6c1ba64 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.formatting.LfLineSeparatorInformation; import com.google.inject.name.Names; @@ -48,6 +50,17 @@ */ @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.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend index 083ddc9b25..be2ed319b4 100644 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.xtend @@ -16,7 +16,6 @@ import com.google.inject.Inject import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.generator.AbstractFileSystemAccess import org.eclipse.xtext.generator.IFileSystemAccess -import org.eclipse.xtext.generator.IFileSystemAccess2 import org.eclipse.xtext.xbase.compiler.JvmModelGenerator import static org.eclipse.xtext.xbase.lib.IteratorExtensions.* @@ -37,16 +36,15 @@ class CheckGenerator extends JvmModelGenerator { @Inject ICheckGeneratorConfigProvider generatorConfigProvider; override void doGenerate(Resource resource, IFileSystemAccess fsa) { - val lfFsa = new LfNormalizingFileSystemAccess(fsa as IFileSystemAccess2) - super.doGenerate(resource, lfFsa); // Generate validator, catalog, and preference initializer from inferred Jvm models. + super.doGenerate(resource, fsa); // Generate validator, catalog, and preference initializer from inferred Jvm models. val config = generatorConfigProvider.get(resource?.URI); for (catalog : toIterable(resource.allContents).filter(typeof(CheckCatalog))) { - lfFsa.generateFile(catalog.issueCodesFilePath, catalog.compileIssueCodes) - lfFsa.generateFile(catalog.standaloneSetupPath, catalog.compileStandaloneSetup) + fsa.generateFile(catalog.issueCodesFilePath, catalog.compileIssueCodes) + fsa.generateFile(catalog.standaloneSetupPath, catalog.compileStandaloneSetup) // change output path for service registry - lfFsa.generateFile( + fsa.generateFile( CheckUtil::serviceRegistryClassName, CheckGeneratorConstants::CHECK_REGISTRY_OUTPUT, catalog.generateServiceRegistry(CheckUtil::serviceRegistryClassName, fsa) @@ -54,7 +52,7 @@ class CheckGenerator extends JvmModelGenerator { // generate documentation for SCA-checks only if(config !== null && (config.doGenerateDocumentationForAllChecks || !config.generateLanguageInternalChecks)){ // change output path for html files to docs/ - lfFsa.generateFile(catalog.docFileName, CheckGeneratorConstants::CHECK_DOC_OUTPUT, catalog.compileDoc) + fsa.generateFile(catalog.docFileName, CheckGeneratorConstants::CHECK_DOC_OUTPUT, catalog.compileDoc) } } } diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/LfNormalizingFileSystemAccess.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/LfNormalizingFileSystemAccess.java deleted file mode 100644 index 53dbd1f2af..0000000000 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/LfNormalizingFileSystemAccess.java +++ /dev/null @@ -1,126 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 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.generator; - -import java.io.InputStream; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.generator.IFileSystemAccess2; - -import com.google.common.base.Preconditions; - - -/** - * A delegating {@link IFileSystemAccess2} that normalizes line endings to LF ({@code \n}) - * before writing content. This ensures generated files are platform-independent regardless - * of the OS on which the build runs. - * - *

Implements {@link IFileSystemAccess2} so that {@code instanceof} checks in the framework - * (e.g., in {@code JvmModelGenerator}) continue to work and no behavior is lost.

- */ -public class LfNormalizingFileSystemAccess implements IFileSystemAccess2 { - - private final IFileSystemAccess2 delegate; - - /** - * Wraps the given delegate. Callers that hold the weaker {@link org.eclipse.xtext.generator.IFileSystemAccess} - * (e.g. from Xtext's {@code Generator2#doGenerate(Resource, IFileSystemAccess)}) must cast at the - * call site — every default Xtext FSA implementation is also an {@link IFileSystemAccess2}. - * - * @param delegate the delegate to wrap, must not be {@code null} - */ - public LfNormalizingFileSystemAccess(final IFileSystemAccess2 delegate) { - this.delegate = Preconditions.checkNotNull(delegate); - } - - @Override - public void generateFile(final String fileName, final CharSequence contents) { - delegate.generateFile(fileName, normalizeLineEndings(contents)); - } - - @Override - public void generateFile(final String fileName, final String outputConfigName, final CharSequence contents) { - delegate.generateFile(fileName, outputConfigName, normalizeLineEndings(contents)); - } - - @Override - public void deleteFile(final String fileName) { - delegate.deleteFile(fileName); - } - - @Override - public void generateFile(final String fileName, final InputStream content) { - delegate.generateFile(fileName, content); - } - - @Override - public void generateFile(final String fileName, final String outputConfigName, final InputStream content) { - delegate.generateFile(fileName, outputConfigName, content); - } - - @Override - public URI getURI(final String fileName, final String outputConfigName) { - return delegate.getURI(fileName, outputConfigName); - } - - @Override - public URI getURI(final String fileName) { - return delegate.getURI(fileName); - } - - @Override - public void deleteFile(final String fileName, final String outputConfigName) { - delegate.deleteFile(fileName, outputConfigName); - } - - @Override - public InputStream readBinaryFile(final String fileName, final String outputConfigName) { - return delegate.readBinaryFile(fileName, outputConfigName); - } - - @Override - public InputStream readBinaryFile(final String fileName) { - return delegate.readBinaryFile(fileName); - } - - @Override - public CharSequence readTextFile(final String fileName, final String outputConfigName) { - return delegate.readTextFile(fileName, outputConfigName); - } - - @Override - public CharSequence readTextFile(final String fileName) { - return delegate.readTextFile(fileName); - } - - @Override - public boolean isFile(final String path, final String outputConfigurationName) { - return delegate.isFile(path, outputConfigurationName); - } - - @Override - public boolean isFile(final String path) { - return delegate.isFile(path); - } - - private static CharSequence normalizeLineEndings(final CharSequence content) { - if (content == null) { - return null; - } - String text = content.toString(); - if (text.indexOf('\r') < 0) { - return content; - } - return text.replace("\r\n", "\n").replace("\r", "\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ - } - -} 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..973eed76f9 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.formatting.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.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.java b/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.java index 4d9d0ee004..d61c434bf6 100644 --- a/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.java +++ b/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.java @@ -15,11 +15,9 @@ import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.generator.AbstractFileSystemAccess; import org.eclipse.xtext.generator.IFileSystemAccess; -import org.eclipse.xtext.generator.IFileSystemAccess2; import org.eclipse.xtext.generator.IGenerator; import org.eclipse.xtext.xbase.lib.IteratorExtensions; -import com.avaloq.tools.ddk.check.generator.LfNormalizingFileSystemAccess; import com.avaloq.tools.ddk.check.runtime.configuration.ICheckConfigurationStoreService; import com.avaloq.tools.ddk.checkcfg.checkcfg.CheckConfiguration; import com.google.common.collect.Iterables; @@ -56,9 +54,8 @@ public void doGenerate(final Resource resource, final IFileSystemAccess fsa) { if (fsa instanceof AbstractFileSystemAccess abstractFsa) { abstractFsa.setOutputPath(outputPath()); } - final LfNormalizingFileSystemAccess lfFsa = new LfNormalizingFileSystemAccess((IFileSystemAccess2) fsa); for (final CheckConfiguration configuration : Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), CheckConfiguration.class)) { - lfFsa.generateFile(fileName(configuration), compile(configuration)); + fsa.generateFile(fileName(configuration), compile(configuration)); } } 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..bce96473c5 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.formatting.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..685e701e8d 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.formatting.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..c20d17a4f6 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.formatting.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..cc64f1aa80 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.formatting.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..2a43aa8220 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.formatting.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/src/com/avaloq/tools/ddk/xtext/formatting/LfLineSeparatorInformation.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/formatting/LfLineSeparatorInformation.java new file mode 100644 index 0000000000..495450fc50 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/formatting/LfLineSeparatorInformation.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * 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.formatting; + +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 IFilePostProcessor} ({@code LineSeparatorHarmonizer}, or the + * trace-preserving {@code TraceAwarePostProcessor} for Xbase languages) target LF for + * {@code IFileSystemAccess} text writes in headless builds, replacing the + * platform-dependent {@code System.lineSeparator()} default. In the IDE the UI module's + * preference/sensing-based {@code IWhitespaceInformationProvider} takes precedence for + * file writes, so workspaces keep converging to the checked-out form. + *

+ */ +public class LfLineSeparatorInformation implements ILineSeparatorInformation { + + @Override + public String getLineSeparator() { + return "\n"; //$NON-NLS-1$ + } + +} From 84b3f31db90420caeeee05b538a2076631a1d2c1 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 Four 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 and FormatLineSeparatorBindingTest: pin the Guice module-convention wiring by resolving ILineSeparatorInformation from the Check and Format runtime injectors. Scope, Export, Valid, Expression and CheckCfg declare the identical binding method but have no runtime test harness to assert it in. - AbstractCheckGenerationTestCase now asserts no generated file contains CR — and injects members into its InMemoryFileSystemAccess so the IFilePostProcessor chain (and with it the LF binding) is actually on the exercised path, upgrading every existing Check generation test into an emission regression guard. - LfPrintWriterTest covers the raw-IO report writer's terminator. Assisted by Claude Co-Authored-By: Claude Fable 5 --- .../META-INF/MANIFEST.MF | 1 + .../test/AbstractCheckGenerationTestCase.java | 9 ++- .../test/CheckLineSeparatorBindingTest.java | 47 ++++++++++++ .../check/test/core/CheckCoreTestSuite.java | 2 + .../META-INF/MANIFEST.MF | 1 + .../FormatLineSeparatorBindingTest.java | 47 ++++++++++++ .../xtext/test/format/FormatTestSuite.java | 3 +- .../META-INF/MANIFEST.MF | 1 + .../test/generator/GeneratorTestSuite.java | 2 + .../test/generator/LfPrintWriterTest.java | 55 ++++++++++++++ .../generator/LineEndingDeterminismTest.java | 71 +++++++++++++++++++ 11 files changed, 237 insertions(+), 2 deletions(-) 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.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatLineSeparatorBindingTest.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..6d5d38ab4b 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; @@ -90,12 +91,18 @@ public List generateAndCompile(final InputStream sourceStream) { } } assertNotNull(type, "Should have an inferred Jvm model"); - // Run the generator using an in-memory file system access + // Run the generator using an in-memory file system access; member injection wires the + // IFilePostProcessor so the LF ILineSeparatorInformation binding is actually exercised InMemoryFileSystemAccess fsa = new InMemoryFileSystemAccess(); + getInjector().injectMembers(fsa); for (OutputConfiguration output : outputConfigurationProvider.getOutputConfigurations()) { 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..8b9ad06df4 --- /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.formatting.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..f36d61b1f6 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 @@ -18,6 +18,7 @@ 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; +import com.avaloq.tools.ddk.check.core.test.CheckLineSeparatorBindingTest; import com.avaloq.tools.ddk.check.core.test.CheckScopingTest; import com.avaloq.tools.ddk.check.core.test.IssueCodeToLabelMapGenerationTest; import com.avaloq.tools.ddk.check.core.test.ProjectBasedTests; @@ -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.format.test/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF index 9313b37ad8..ddf99127ae 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.format.test/META-INF/MANIFEST.MF @@ -8,6 +8,7 @@ Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Fragment-Host: com.avaloq.tools.ddk.xtext.format.ui Require-Bundle: com.avaloq.tools.ddk.xtext.format, + com.avaloq.tools.ddk.xtext, com.google.inject, com.avaloq.tools.ddk.xtext.test.core, org.mockito.mockito-core, diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatLineSeparatorBindingTest.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatLineSeparatorBindingTest.java new file mode 100644 index 0000000000..41a9d84652 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/FormatLineSeparatorBindingTest.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.xtext.format; + +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.xtext.formatting.LfLineSeparatorInformation; +import com.google.inject.Inject; + + +/** + * Guarantees the Format runtime injector resolves {@link ILineSeparatorInformation} to the LF + * binding, so headless generation is line-ending-deterministic. Together with the Check + * sibling this pins the Guice module-convention wiring; Scope, Export, Valid, Expression + * and CheckCfg declare the identical binding method but have no runtime test harness to + * assert it in. + */ +@ExtendWith(InjectionExtension.class) +@InjectWith(FormatInjectorProvider.class) +@SuppressWarnings("nls") +public class FormatLineSeparatorBindingTest { + + @Inject + private ILineSeparatorInformation lineSeparatorInformation; + + @Test + public void runtimeInjectorBindsLfLineSeparator() { + assertInstanceOf(LfLineSeparatorInformation.class, lineSeparatorInformation, "Format runtime injector must bind the LF separator information"); + assertEquals("\n", lineSeparatorInformation.getLineSeparator(), "bound separator must be LF"); + } + +} diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java index 8b12ea9435..db1def04f3 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java @@ -13,6 +13,7 @@ import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; +import com.avaloq.tools.ddk.xtext.format.FormatLineSeparatorBindingTest; import com.avaloq.tools.ddk.xtext.format.FormatParsingTest; import com.avaloq.tools.ddk.xtext.format.builder.FormatBuilderParticipantTest; import com.avaloq.tools.ddk.xtext.format.formatting.FormatFormattingTest; @@ -24,7 +25,7 @@ * Empty class serving only as holder for JUnit5 annotations. */ @Suite -@SelectClasses({FormatParsingTest.class, FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class}) +@SelectClasses({FormatParsingTest.class, FormatLineSeparatorBindingTest.class, FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class}) public class FormatTestSuite { } 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..2dbf9a7f89 --- /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.formatting.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); + } + +}