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 f5c6ba4f93..b7072d2827 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.IssueExpressionGenerationTest; @@ -36,6 +37,7 @@ // @Format-Off IssueCodeValueTest.class, BasicModelTest.class, + CheckLineSeparatorBindingTest.class, BugAig830.class, CheckScopingTest.class, CheckValidationTest.class, diff --git a/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF index 037d521288..89b61b03ac 100644 --- a/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.check.core Bundle-SymbolicName: com.avaloq.tools.ddk.check.core;singleton:=true -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.3.3.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy diff --git a/com.avaloq.tools.ddk.check.core/pom.xml b/com.avaloq.tools.ddk.check.core/pom.xml index e74d8a7860..955fdb5090 100644 --- a/com.avaloq.tools.ddk.check.core/pom.xml +++ b/com.avaloq.tools.ddk.check.core/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.3.3-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.check.core eclipse-plugin - \ No newline at end of file + 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.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java index 863c815c66..252f47a9cc 100644 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java @@ -21,7 +21,6 @@ import org.eclipse.xtext.common.types.JvmField; import org.eclipse.xtext.generator.AbstractFileSystemAccess; import org.eclipse.xtext.generator.IFileSystemAccess; -import org.eclipse.xtext.generator.IFileSystemAccess2; import org.eclipse.xtext.generator.OutputConfiguration; import org.eclipse.xtext.xbase.compiler.GeneratorConfig; import org.eclipse.xtext.xbase.compiler.JvmModelGenerator; @@ -59,8 +58,7 @@ public class CheckGenerator extends JvmModelGenerator { @Override public void doGenerate(final Resource resource, final IFileSystemAccess fsa) { - final LfNormalizingFileSystemAccess lfFsa = new LfNormalizingFileSystemAccess((IFileSystemAccess2) fsa); - 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. URI uri = null; if (resource != null) { uri = resource.getURI(); @@ -68,18 +66,18 @@ public void doGenerate(final Resource resource, final IFileSystemAccess fsa) { final CheckGeneratorConfig config = generatorConfigProvider.get(uri); final Iterable catalogs = Iterables.filter(IteratorExtensions.toIterable(resource.getAllContents()), CheckCatalog.class); for (final CheckCatalog catalog : catalogs) { - lfFsa.generateFile(checkGeneratorNaming.issueCodesFilePath(catalog), compileIssueCodes(catalog)); - lfFsa.generateFile(checkGeneratorNaming.standaloneSetupPath(catalog), compileStandaloneSetup(catalog)); + fsa.generateFile(checkGeneratorNaming.issueCodesFilePath(catalog), compileIssueCodes(catalog)); + fsa.generateFile(checkGeneratorNaming.standaloneSetupPath(catalog), compileStandaloneSetup(catalog)); // change output path for service registry - lfFsa.generateFile( + fsa.generateFile( CheckUtil.serviceRegistryClassName(), CheckGeneratorConstants.CHECK_REGISTRY_OUTPUT, generateServiceRegistry(catalog, CheckUtil.serviceRegistryClassName(), fsa)); // generate documentation for SCA-checks only if (config != null && (config.doGenerateDocumentationForAllChecks() || !config.isGenerateLanguageInternalChecks())) { // change output path for html files to docs/ - lfFsa.generateFile(checkGeneratorNaming.docFileName(catalog), CheckGeneratorConstants.CHECK_DOC_OUTPUT, compileDoc(catalog)); + fsa.generateFile(checkGeneratorNaming.docFileName(catalog), CheckGeneratorConstants.CHECK_DOC_OUTPUT, compileDoc(catalog)); } } } 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/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.checkcfg.core/META-INF/MANIFEST.MF index 002965fee7..134bcf217a 100644 --- a/com.avaloq.tools.ddk.checkcfg.core/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.checkcfg.core/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.checkcfg.core Bundle-Vendor: Avaloq Group AG -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.3.3.qualifier Bundle-SymbolicName: com.avaloq.tools.ddk.checkcfg.core; singleton:=true Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/com.avaloq.tools.ddk.checkcfg.core/pom.xml b/com.avaloq.tools.ddk.checkcfg.core/pom.xml index 5b8d10443b..8580af50df 100644 --- a/com.avaloq.tools.ddk.checkcfg.core/pom.xml +++ b/com.avaloq.tools.ddk.checkcfg.core/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.3.3-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.checkcfg.core eclipse-plugin - \ No newline at end of file + 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.feature/feature.xml b/com.avaloq.tools.ddk.feature/feature.xml index e92053cf6c..3485399f8a 100644 --- a/com.avaloq.tools.ddk.feature/feature.xml +++ b/com.avaloq.tools.ddk.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/com.avaloq.tools.ddk.feature/pom.xml b/com.avaloq.tools.ddk.feature/pom.xml index 7394c149db..02bc6b724b 100644 --- a/com.avaloq.tools.ddk.feature/pom.xml +++ b/com.avaloq.tools.ddk.feature/pom.xml @@ -7,7 +7,7 @@ 18.0.1-SNAPSHOT ../ddk-parent - 19.1.1-SNAPSHOT + 19.1.2-SNAPSHOT com.avaloq.tools.ddk.feature eclipse-feature diff --git a/com.avaloq.tools.ddk.runtime.feature/feature.xml b/com.avaloq.tools.ddk.runtime.feature/feature.xml index 92396d6693..753f53002a 100644 --- a/com.avaloq.tools.ddk.runtime.feature/feature.xml +++ b/com.avaloq.tools.ddk.runtime.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/com.avaloq.tools.ddk.runtime.feature/pom.xml b/com.avaloq.tools.ddk.runtime.feature/pom.xml index 6caa847071..02f14dcf67 100644 --- a/com.avaloq.tools.ddk.runtime.feature/pom.xml +++ b/com.avaloq.tools.ddk.runtime.feature/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 19.1.0-SNAPSHOT + 19.1.1-SNAPSHOT com.avaloq.tools.ddk.runtime.feature eclipse-feature - \ No newline at end of file + 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/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF index 4fcb354f03..19d1027bee 100644 --- a/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.export/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext.export Bundle-SymbolicName: com.avaloq.tools.ddk.xtext.export;singleton:=true -Bundle-Version: 17.3.3.qualifier +Bundle-Version: 17.3.4.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy diff --git a/com.avaloq.tools.ddk.xtext.export/pom.xml b/com.avaloq.tools.ddk.xtext.export/pom.xml index 65b107c1ae..5baf93d448 100644 --- a/com.avaloq.tools.ddk.xtext.export/pom.xml +++ b/com.avaloq.tools.ddk.xtext.export/pom.xml @@ -6,7 +6,7 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.3-SNAPSHOT + 17.3.4-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.xtext.export eclipse-plugin 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 8dee346250..3cb93e319e 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,6 +10,7 @@ *******************************************************************************/ 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 org.eclipse.xtext.xbase.compiler.JvmModelGenerator; @@ -18,6 +19,7 @@ import com.avaloq.tools.ddk.xtext.export.generator.ExportJvmModelGenerator; 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; /** @@ -25,6 +27,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; + } + @Override public Class bindIValueConverterService() { return ExportValueConverterService.class; diff --git a/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF index 366e4f8545..a63a30d469 100644 --- a/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.expression/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext.expression Bundle-SymbolicName: com.avaloq.tools.ddk.xtext.expression;singleton:=true -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.3.3.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Require-Bundle: org.eclipse.xtext, diff --git a/com.avaloq.tools.ddk.xtext.expression/pom.xml b/com.avaloq.tools.ddk.xtext.expression/pom.xml index 2050902799..ce074bc3c9 100644 --- a/com.avaloq.tools.ddk.xtext.expression/pom.xml +++ b/com.avaloq.tools.ddk.xtext.expression/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.3.3-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.xtext.expression eclipse-plugin - \ No newline at end of file + 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..5158b8cb56 --- /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, 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 4a4ef63c53..b15b42d688 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; @@ -25,7 +26,7 @@ * Empty class serving only as holder for JUnit5 annotations. */ @Suite -@SelectClasses({FormatParsingTest.class, FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class, FormatJvmModelInferrerTest.class}) +@SelectClasses({FormatParsingTest.class, FormatLineSeparatorBindingTest.class, FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class, FormatJvmModelInferrerTest.class}) public class FormatTestSuite { } diff --git a/com.avaloq.tools.ddk.xtext.format/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.format/META-INF/MANIFEST.MF index ca0bf0d252..7246ff1206 100644 --- a/com.avaloq.tools.ddk.xtext.format/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.format/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext.format Bundle-SymbolicName: com.avaloq.tools.ddk.xtext.format;singleton:=true -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.3.3.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy diff --git a/com.avaloq.tools.ddk.xtext.format/pom.xml b/com.avaloq.tools.ddk.xtext.format/pom.xml index dbbfaa8004..3b5667f9e7 100644 --- a/com.avaloq.tools.ddk.xtext.format/pom.xml +++ b/com.avaloq.tools.ddk.xtext.format/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.3.3-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.xtext.format eclipse-plugin - \ No newline at end of file + 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 6a1bd1d8f8..9c2afb4d97 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 @@ -9,6 +9,7 @@ Bundle-ActivationPolicy: lazy Fragment-Host: com.avaloq.tools.ddk.xtext.generator Require-Bundle: com.avaloq.tools.ddk.test.core, com.avaloq.tools.ddk.xtext.export, + com.avaloq.tools.ddk.xtext, com.avaloq.tools.ddk.xtext.expression, com.avaloq.tools.ddk.xtext.scope, com.avaloq.tools.ddk.xtext.test.core, 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 faf395b6ec..64074a5e47 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 ExportExpressionCodeGenerationTest.class, + LfPrintWriterTest.class, + LineEndingDeterminismTest.class, ExpressionsExtentionsTest.class, ScopeExpressionCodeGenerationTest.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/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.generator/META-INF/MANIFEST.MF index 58f677040d..4fa27a2228 100644 --- a/com.avaloq.tools.ddk.xtext.generator/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.generator/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext.generator Bundle-SymbolicName: com.avaloq.tools.ddk.xtext.generator;singleton:=true -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.3.3.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Require-Bundle: org.eclipse.jface, diff --git a/com.avaloq.tools.ddk.xtext.generator/pom.xml b/com.avaloq.tools.ddk.xtext.generator/pom.xml index 4189d7b4d0..dd5ab4f8bc 100644 --- a/com.avaloq.tools.ddk.xtext.generator/pom.xml +++ b/com.avaloq.tools.ddk.xtext.generator/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.3.3-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.xtext.generator eclipse-plugin - \ No newline at end of file + 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/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext.scope/META-INF/MANIFEST.MF index eb79e677ad..8aab1de2fd 100644 --- a/com.avaloq.tools.ddk.xtext.scope/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext.scope/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext.scope Bundle-SymbolicName: com.avaloq.tools.ddk.xtext.scope;singleton:=true -Bundle-Version: 17.3.3.qualifier +Bundle-Version: 17.3.4.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Require-Bundle: org.eclipse.xtext, diff --git a/com.avaloq.tools.ddk.xtext.scope/pom.xml b/com.avaloq.tools.ddk.xtext.scope/pom.xml index c5745c9e60..ca807724bd 100644 --- a/com.avaloq.tools.ddk.xtext.scope/pom.xml +++ b/com.avaloq.tools.ddk.xtext.scope/pom.xml @@ -6,7 +6,7 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.3-SNAPSHOT + 17.3.4-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.xtext.scope eclipse-plugin 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 103bee2a13..56b0ddd137 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/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF index 80617eb2ac..10e4c2811d 100644 --- a/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.xtext/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.xtext Bundle-SymbolicName: com.avaloq.tools.ddk.xtext;singleton:=true -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.4.0.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy @@ -48,4 +48,3 @@ Export-Package: com.avaloq.tools.ddk.xtext.build, Import-Package: org.apache.logging.log4j, org.apache.logging.log4j.util Automatic-Module-Name: com.avaloq.tools.ddk.xtext - diff --git a/com.avaloq.tools.ddk.xtext/pom.xml b/com.avaloq.tools.ddk.xtext/pom.xml index c7e8c5d3c7..4c4a2e4dee 100644 --- a/com.avaloq.tools.ddk.xtext/pom.xml +++ b/com.avaloq.tools.ddk.xtext/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.4.0-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.xtext eclipse-plugin - \ No newline at end of file + 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..259e3e4208 --- /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}). + *

+ * Binding this in a language's runtime module makes Xtext's + * {@code IFilePostProcessor} ({@code LineSeparatorHarmonizer}, or the trace-preserving + * {@code TraceAwarePostProcessor} for Xbase languages) use LF for + * {@code IFileSystemAccess} text writes in headless builds, independently of the host's + * default line separator. + * In the IDE, the preference-based {@code IWhitespaceInformationProvider} controls writes + * with a resource URI; its fallback for a missing resource URI uses this binding. + *

+ * + * @since 17.4 + */ +public class LfLineSeparatorInformation implements ILineSeparatorInformation { + + @Override + public String getLineSeparator() { + return "\n"; //$NON-NLS-1$ + } + +} diff --git a/ddk-repository/category.xml b/ddk-repository/category.xml index 5a1067f01f..74c09ed1d6 100644 --- a/ddk-repository/category.xml +++ b/ddk-repository/category.xml @@ -1,9 +1,9 @@ - + - +