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.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.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" } } 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.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); + } + +} 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 */ 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$ + } + +}