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