From 765822d2f6324e4de1b2f76996716904bdb8d1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Wed, 20 May 2026 00:53:59 +0200 Subject: [PATCH 1/2] chore: emit LF explicitly in KeywordAnalysisHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `KeywordAnalysisHelper` wrote its diagnostic reports via `PrintWriter`, whose `println()` uses `System.lineSeparator()` — platform-dependent. This produced CRLF on Windows, contradicting the project's `.gitattributes` LF policy. Replace `PrintWriter` with `Files.newBufferedWriter` + a tiny `writeLine(...)` helper that writes a literal `'\n'`. The reports now emit LF on every platform. This is the one raw-IO emission path that wasn't covered by the `LfNormalizingFileSystemAccess` decorator (which only wraps FSAs). Fixing it here is the prerequisite for removing the decorator in the following commit — the codebase no longer needs a post-hoc normaliser because every emission path now produces LF natively. Co-Authored-By: Claude Opus 4.7 --- .../parser/antlr/KeywordAnalysisHelper.java | 138 +++++++++--------- 1 file changed, 71 insertions(+), 67 deletions(-) diff --git a/com.avaloq.tools.ddk.xtext.generator/src/com/avaloq/tools/ddk/xtext/generator/parser/antlr/KeywordAnalysisHelper.java b/com.avaloq.tools.ddk.xtext.generator/src/com/avaloq/tools/ddk/xtext/generator/parser/antlr/KeywordAnalysisHelper.java index a92cb100e..1548b718a 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 @@ -11,11 +11,12 @@ package com.avaloq.tools.ddk.xtext.generator.parser.antlr; +import java.io.BufferedWriter; import java.io.File; import java.io.IOException; -import java.io.PrintWriter; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -178,99 +179,102 @@ private boolean hasLetters(final String keyword) { * if an I/O error occurs */ public void printViolations(final String srcGenPath) { - try { - String fileName = getKeywordsDiagnosticReportFileName(srcGenPath); - PrintWriter writer = new PrintWriter(new File(fileName), StandardCharsets.UTF_8); - 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"); - writer.println(" (or keywords), but are not accepted by the rule."); - writer.println(" reserved words accepted - listed in MWE2 as reserved, but accepted."); - writer.println(); - writer.println("Read more on https://ddk.tools.avaloq.com/keywords.html"); - writer.println(); - writer.println("Specification"); - writer.println(); - writer.println("RESERVED WORDS"); + String fileName = getKeywordsDiagnosticReportFileName(srcGenPath); + try (BufferedWriter writer = Files.newBufferedWriter(new File(fileName).toPath(), StandardCharsets.UTF_8)) { + writeLine(writer, "Please check in this file, so a diff can be used to detect unexpected changes"); + writeLine(writer, ""); + writeLine(writer, " identifiers rejected - are not listed in MWE2 file as reserved words"); + writeLine(writer, " (or keywords), but are not accepted by the rule."); + writeLine(writer, " reserved words accepted - listed in MWE2 as reserved, but accepted."); + writeLine(writer, ""); + writeLine(writer, "Read more on https://ddk.tools.avaloq.com/keywords.html"); + writeLine(writer, ""); + writeLine(writer, "Specification"); + writeLine(writer, ""); + writeLine(writer, "RESERVED WORDS"); int count = 0; String indent = " "; for (String word : reservedWords) { if (count++ % WORDS_PER_ROW == 0) { - writer.println(); - writer.print(indent); + writeLine(writer, ""); + writer.write(indent); } - writer.print(Strings.padEnd(word, WORDS_PADDING, ' ')); + writer.write(Strings.padEnd(word, WORDS_PADDING, ' ')); } - writer.println(); - writer.println(); - writer.println("KEYWORDS"); + writeLine(writer, ""); + writeLine(writer, ""); + writeLine(writer, "KEYWORDS"); count = 0; for (String word : keywordsSpec) { if (count++ % WORDS_PER_ROW == 0) { - writer.println(); - writer.print(indent); + writeLine(writer, ""); + writer.write(indent); } - writer.print(Strings.padEnd(word, WORDS_PADDING, ' ')); + writer.write(Strings.padEnd(word, WORDS_PADDING, ' ')); } - writer.println(); + writeLine(writer, ""); boolean problemsReported = false; for (String ruleName : keywordsViolatingSpec.keySet()) { - writer.println(); - writer.print("RULE: "); - writer.println(ruleName); + writeLine(writer, ""); + writer.write("RULE: "); + writeLine(writer, ruleName); Set kwSet = keywordsViolatingSpec.get(ruleName); - writer.println(" Identifiers rejected (PROBLEM if non empty): "); + writeLine(writer, " Identifiers rejected (PROBLEM if non empty): "); for (String kw : Sets.difference(kwSet, keywordsSpec)) { problemsReported = true; // only rejected are real problems - writer.print(indent); - writer.println(kw); + writer.write(indent); + writeLine(writer, kw); } - writer.println(); + writeLine(writer, ""); Set rwSet = keywordsSofterThanSpec.get(ruleName); - writer.println(" Reserved words rejected: "); + writeLine(writer, " Reserved words rejected: "); for (String kw : Sets.difference(reservedWords, rwSet)) { - writer.print(indent); - writer.println(kw); + writer.write(indent); + writeLine(writer, kw); } - writer.println(); - writer.println(" Reserved words accepted: "); + writeLine(writer, ""); + writeLine(writer, " Reserved words accepted: "); for (String kw : rwSet) { - writer.print(indent); - writer.println(kw); + writer.write(indent); + writeLine(writer, kw); } - writer.println(); - writer.println(" Keywords rejected: "); + writeLine(writer, ""); + writeLine(writer, " Keywords rejected: "); for (String kw : Sets.intersection(keywordsSpec, kwSet)) { - writer.print(indent); - writer.println(kw); + writer.write(indent); + writeLine(writer, kw); } - writer.println(); - writer.println(" Keywords accepted: "); + writeLine(writer, ""); + writeLine(writer, " Keywords accepted: "); for (String kw : Sets.difference(keywordsSpec, kwSet)) { - writer.print(indent); - writer.println(kw); + writer.write(indent); + writeLine(writer, kw); } - writer.println(); + writeLine(writer, ""); } if (problemsReported) { LOGGER.error("REJECTED KEYWORDS BY ID RULES DETECTED."); LOGGER.error("Read {} !", fileName); - writer.println(); - writer.println("(!) Problems were detected: neither reserved words nor keywords, but rejected by identifier rules"); - writer.println("Use " + Path.fromPortableString(getReportFileName(srcGenPath)).lastSegment() + " to find out why these words are keywords."); + writeLine(writer, ""); + writeLine(writer, "(!) Problems were detected: neither reserved words nor keywords, but rejected by identifier rules"); + writeLine(writer, "Use " + Path.fromPortableString(getReportFileName(srcGenPath)).lastSegment() + " to find out why these words are keywords."); } - writer.println(); - writer.println("The following rules were not checked, but might also be relevant"); + writeLine(writer, ""); + writeLine(writer, "The following rules were not checked, but might also be relevant"); for (String ruleName : uncheckedRules) { - writer.print(indent); - writer.println(ruleName); + writer.write(indent); + writeLine(writer, ruleName); } - writer.println("if any of them is used to parse identifiers, add them to identifierRules in MWE2 file"); - writer.close(); + writeLine(writer, "if any of them is used to parse identifiers, add them to identifierRules in MWE2 file"); } catch (IOException e) { throw new UncheckedIOException(e); } + } + /** Write {@code line} followed by an LF newline, regardless of platform. */ + private static void writeLine(final BufferedWriter writer, final String line) throws IOException { + writer.write(line); + writer.write('\n'); } /** @@ -439,19 +443,19 @@ public List getAllGrammars() { * if an I/O error occurs */ public void printReport(final String srcGenPath) { - try { - String fileName = getReportFileName(srcGenPath); - PrintWriter writer = new PrintWriter(new File(fileName), StandardCharsets.UTF_8); - writer.print(report.build()); - writer.close(); - String docuFileName = getDocFileName(srcGenPath); - PrintWriter docuWriter = new PrintWriter(new File(docuFileName), StandardCharsets.UTF_8); - docuWriter.print(new CombinedGrammarReportBuilder(grammarExtensions).getDocumentation(grammar, parserRules, enumRules)); - docuWriter.close(); - LOGGER.info("report on keywords is written into {}", fileName); + String fileName = getReportFileName(srcGenPath); + String docuFileName = getDocFileName(srcGenPath); + try (BufferedWriter writer = Files.newBufferedWriter(new File(fileName).toPath(), StandardCharsets.UTF_8)) { + writer.write(report.build()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + try (BufferedWriter docuWriter = Files.newBufferedWriter(new File(docuFileName).toPath(), StandardCharsets.UTF_8)) { + docuWriter.write(new CombinedGrammarReportBuilder(grammarExtensions).getDocumentation(grammar, parserRules, enumRules)); } catch (IOException e) { throw new UncheckedIOException(e); } + LOGGER.info("report on keywords is written into {}", fileName); } /** From 293bf38a60303bd29f43b9d65458001bd7b71213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Wed, 20 May 2026 00:55:55 +0200 Subject: [PATCH 2/2] chore: remove LfNormalizingFileSystemAccess (now superseded by config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete `LfNormalizingFileSystemAccess.java` and remove its wrap calls from `CheckGenerator.xtend` and `CheckCfgGenerator.xtend`. The decorator was introduced in PR #1331 as a post-hoc normaliser against CRLF leaks from various emission paths. After: - step 1 of this stack (#1352) — `.mwe2` `lineDelimiter` and `ddk-configuration` runtime prefs both flipped to `\n` - step 2 (#1353) — `line.separator=\n` propagated to every source-bearing bundle's `.settings/org.eclipse.core.runtime.prefs` - the preceding commit on this branch — `KeywordAnalysisHelper` no longer uses `PrintWriter.println()` …every emission path now produces LF natively. The wrapper is no-op work and the conditional `instanceof IFileSystemAccess2` cast it introduced is no longer needed. Closes #1345 — the planned extension of the same wrapper to `Scope` / `Format` / `Export` generators is no longer needed for the same reason. Those generators are already clean of `Strings.newLine()`, `System.lineSeparator()`, and hardcoded `"\r\n"` emission (audited). Co-Authored-By: Claude Opus 4.7 --- .../ddk/check/generator/CheckGenerator.xtend | 12 +- .../LfNormalizingFileSystemAccess.java | 126 ------------------ .../generator/CheckCfgGenerator.xtend | 5 +- 3 files changed, 6 insertions(+), 137 deletions(-) delete mode 100644 com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/LfNormalizingFileSystemAccess.java 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 083ddc9b2..be2ed319b 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 53dbd1f2a..000000000 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/LfNormalizingFileSystemAccess.java +++ /dev/null @@ -1,126 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Avaloq Group AG and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Avaloq Group AG - initial API and implementation - *******************************************************************************/ - -package com.avaloq.tools.ddk.check.generator; - -import java.io.InputStream; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.generator.IFileSystemAccess2; - -import com.google.common.base.Preconditions; - - -/** - * A delegating {@link IFileSystemAccess2} that normalizes line endings to LF ({@code \n}) - * before writing content. This ensures generated files are platform-independent regardless - * of the OS on which the build runs. - * - *

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

- */ -public class LfNormalizingFileSystemAccess implements IFileSystemAccess2 { - - private final IFileSystemAccess2 delegate; - - /** - * Wraps the given delegate. Callers that hold the weaker {@link org.eclipse.xtext.generator.IFileSystemAccess} - * (e.g. from Xtext's {@code Generator2#doGenerate(Resource, IFileSystemAccess)}) must cast at the - * call site — every default Xtext FSA implementation is also an {@link IFileSystemAccess2}. - * - * @param delegate the delegate to wrap, must not be {@code null} - */ - public LfNormalizingFileSystemAccess(final IFileSystemAccess2 delegate) { - this.delegate = Preconditions.checkNotNull(delegate); - } - - @Override - public void generateFile(final String fileName, final CharSequence contents) { - delegate.generateFile(fileName, normalizeLineEndings(contents)); - } - - @Override - public void generateFile(final String fileName, final String outputConfigName, final CharSequence contents) { - delegate.generateFile(fileName, outputConfigName, normalizeLineEndings(contents)); - } - - @Override - public void deleteFile(final String fileName) { - delegate.deleteFile(fileName); - } - - @Override - public void generateFile(final String fileName, final InputStream content) { - delegate.generateFile(fileName, content); - } - - @Override - public void generateFile(final String fileName, final String outputConfigName, final InputStream content) { - delegate.generateFile(fileName, outputConfigName, content); - } - - @Override - public URI getURI(final String fileName, final String outputConfigName) { - return delegate.getURI(fileName, outputConfigName); - } - - @Override - public URI getURI(final String fileName) { - return delegate.getURI(fileName); - } - - @Override - public void deleteFile(final String fileName, final String outputConfigName) { - delegate.deleteFile(fileName, outputConfigName); - } - - @Override - public InputStream readBinaryFile(final String fileName, final String outputConfigName) { - return delegate.readBinaryFile(fileName, outputConfigName); - } - - @Override - public InputStream readBinaryFile(final String fileName) { - return delegate.readBinaryFile(fileName); - } - - @Override - public CharSequence readTextFile(final String fileName, final String outputConfigName) { - return delegate.readTextFile(fileName, outputConfigName); - } - - @Override - public CharSequence readTextFile(final String fileName) { - return delegate.readTextFile(fileName); - } - - @Override - public boolean isFile(final String path, final String outputConfigurationName) { - return delegate.isFile(path, outputConfigurationName); - } - - @Override - public boolean isFile(final String path) { - return delegate.isFile(path); - } - - private static CharSequence normalizeLineEndings(final CharSequence content) { - if (content == null) { - return null; - } - String text = content.toString(); - if (text.indexOf('\r') < 0) { - return content; - } - return text.replace("\r\n", "\n").replace("\r", "\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ - } - -} diff --git a/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.xtend b/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.xtend index e7a49aa53..9b305ef40 100644 --- a/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.xtend +++ b/com.avaloq.tools.ddk.checkcfg.core/src/com/avaloq/tools/ddk/checkcfg/generator/CheckCfgGenerator.xtend @@ -10,14 +10,12 @@ *******************************************************************************/ package com.avaloq.tools.ddk.checkcfg.generator -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.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.generator.IGenerator import static org.eclipse.xtext.xbase.lib.IteratorExtensions.* @@ -39,9 +37,8 @@ class CheckCfgGenerator implements IGenerator { if (fsa instanceof AbstractFileSystemAccess) { fsa.setOutputPath(outputPath) } - val lfFsa = new LfNormalizingFileSystemAccess(fsa as IFileSystemAccess2) for (configuration:toIterable(resource.allContents).filter(typeof(CheckConfiguration))) { - lfFsa.generateFile(configuration.fileName, configuration.compile) + fsa.generateFile(configuration.fileName, configuration.compile) } }