From a08982375345e3f5ec3a8f47663c63b3860ad70a Mon Sep 17 00:00:00 2001 From: Paul Nelissen Date: Thu, 16 Jul 2026 09:39:54 +0200 Subject: [PATCH 1/6] Add status reporting This change introduces a new reporting infrastructure for the XPlus project, including a StatusReport record for serializable validation messages (mimicking EMF's Diagnostic), support classes for reporting and conversion utilities, and integration into the CLI layer to replace direct validation API usage with structured error handling and JSON export capabilities. --- .../META-INF/MANIFEST.MF | 3 +- .../reporting/StatusReportHelperTest.xtend | 295 +++++++++++++++ .../META-INF/MANIFEST.MF | 4 +- .../xtext/common/lang/reporting/Severity.java | 79 ++++ .../common/lang/reporting/StatusReport.java | 87 +++++ .../lang/reporting/StatusReportHelper.java | 345 ++++++++++++++++++ .../lang/reporting/StatusReporting.java | 23 ++ .../reporting/StatusReportingSupport.java | 35 ++ .../types/generator/CmdLineContext.xtend | 3 +- .../types/generator/SpecDiffContext.xtend | 3 +- .../types/generator/StepsParserContext.xtend | 5 +- .../types/generator/TestSpecContext.xtend | 5 +- .../esi/xtext/types/generator/XPlusMain.xtend | 138 +++---- 13 files changed, 956 insertions(+), 69 deletions(-) create mode 100644 bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Severity.java create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java diff --git a/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF b/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF index 5aa1ab8..9a6af35 100644 --- a/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF +++ b/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF @@ -13,4 +13,5 @@ Require-Bundle: nl.esi.xtext.common.lang, Import-Package: org.junit.jupiter.api;version="[5.1.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.1.0,6.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-21 -Export-Package: nl.esi.xtext.common.lang.tests;x-internal=true +Export-Package: nl.esi.xtext.common.lang.tests;x-internal=true, + nl.esi.xtext.common.lang.tests.reporting;x-internal:=true diff --git a/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend b/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend new file mode 100644 index 0000000..17f9184 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend @@ -0,0 +1,295 @@ +/** + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.tests.reporting + +import com.google.inject.Inject +import java.util.List +import nl.esi.xtext.common.lang.base.ModelContainer +import nl.esi.xtext.common.lang.tests.BaseInjectorProvider +import nl.esi.xtext.common.lang.reporting.Severity +import nl.esi.xtext.common.lang.reporting.StatusReportHelper +import nl.esi.xtext.common.lang.reporting.StatusReport +import org.eclipse.xtext.testing.InjectWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +import org.eclipse.xtext.testing.util.ParseHelper +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.^extension.ExtendWith + +/** + * Test cases for StatusReportHelper utility class. + * Tests validation of EObjects and Resources using EMF Diagnostician. + */ +@ExtendWith(InjectionExtension) +@InjectWith(BaseInjectorProvider) +class StatusReportHelperTest { + @Inject + ParseHelper parseHelper + + /** + * Test validation of a valid model (no imports, just a simple import statement). + * Expected: validation should succeed with OK severity. + */ + @Test + def void testValidateValidModel() { + val result = parseHelper.parse(''' + import "dummy.base_lang" + ''') + Assertions.assertNotNull(result) + + val StatusReport = StatusReportHelper.validate(result) + Assertions.assertNotNull(StatusReport) + Assertions.assertEquals(Severity.OK, StatusReport.severity, "Validation should succeed with OK severity") + Assertions.assertNotNull(StatusReport.message) + } + + /** + * Test validation of a valid model with named elements. + * Expected: validation should succeed without errors. + */ + @Test + def void testValidateModelWithNamedElements() { + val result = parseHelper.parse(''' + import "dummy.base_lang" + TestElement + AnotherElement + ''') + Assertions.assertNotNull(result) + + val StatusReport = StatusReportHelper.validate(result) + Assertions.assertNotNull(StatusReport) + Assertions.assertFalse(StatusReport.isError(), "Validation should not report errors") + Assertions.assertTrue(StatusReport.severity == Severity.OK, "Validation should be OK") + } + + /** + * Test validation of a model resource (via eResource). + * Expected: validation should complete successfully. + */ + @Test + def void testValidateResource() { + val result = parseHelper.parse(''' + import "dummy.base_lang" + Same + Same + ''') + Assertions.assertNotNull(result) + val resource = result.eResource + Assertions.assertNotNull(resource) + + val StatusReport = StatusReportHelper.validate(resource) + Assertions.assertNotNull(StatusReport) + Assertions.assertNotNull(StatusReport.message) + Assertions.assertTrue(StatusReport.message.length > 0, "Validation message should not be empty") + Assertions.assertTrue(StatusReport.children.size > 1, "There should be validation errors") + Assertions.assertTrue(StatusReport.children.get(0).message().contains("Duplicate"), + "Expected duplicate validation error") + } + + /** + * Test validation of a model resource (via eResource). + * Expected: validation should complete successfully. + */ + @Test + def void testGson() { + val result = parseHelper.parse(''' + import "dummy.base_lang" + Same + Same + ''') + Assertions.assertNotNull(result) + val resource = result.eResource + Assertions.assertNotNull(resource) + + val statusReport = StatusReportHelper.validate(resource) + + // serialize with gson + val json = StatusReportHelper.toJson(statusReport) + val statusReport2 = StatusReportHelper.fromJson(json); + Assertions.assertEquals(statusReport, statusReport2); + + } + + /** + * Test that Severity enum contains proper severity constants. + * Expected: all severity constants should be accessible via Severity enum. + */ + @Test + def void testStatusReportSeverityConstants() { + Assertions.assertEquals(0x0, Severity.OK.value) + Assertions.assertEquals(0x1, Severity.INFO.value) + Assertions.assertEquals(0x2, Severity.WARNING.value) + Assertions.assertEquals(0x4, Severity.ERROR.value) + Assertions.assertEquals(0x8, Severity.CANCEL.value) + } + + /** + * Test StatusReport.isError() method. + * Expected: should return true for ERROR and CANCEL, false for others. + */ + @Test + def void testStatusReportIsError() { + val okMessage = createStatusReport(Severity.OK, "Test", "source", 0) + Assertions.assertFalse(okMessage.isError(), "OK should not be an error") + + val errorMessage = createStatusReport(Severity.ERROR, "Test", "source", 0) + Assertions.assertTrue(errorMessage.isError(), "ERROR should be an error") + + val cancelMessage = createStatusReport(Severity.CANCEL, "Test", "source", 0) + Assertions.assertTrue(cancelMessage.isError(), "CANCEL should be an error") + } + + /** + * Test that StatusReport is a record with serializable fields. + * Expected: all fields should be accessible and non-null where applicable. + */ + @Test + def void testStatusReportFields() { + val message = createStatusReport( + Severity.INFO, + "Test message", + "test.source", + 42, + "Some details about the validation", + 1, + 2, + 3, + 4, + "5" + ) + + Assertions.assertEquals(Severity.INFO, message.severity()) + Assertions.assertEquals("Test message", message.message()) + Assertions.assertEquals("test.source", message.source()) + Assertions.assertEquals(1, message.startLine()) + Assertions.assertEquals(2, message.endLine()) + Assertions.assertEquals(3, message.offset()) + Assertions.assertEquals(4, message.length()) + Assertions.assertEquals("5", message.text()) + Assertions.assertEquals(42, message.code()) + Assertions.assertEquals("Some details about the validation", message.details()) + Assertions.assertNull(message.children()) + } + + /** + * Test that severity is elevated to the highest severity found in children. + * Expected: parent severity should be elevated from OK to ERROR when children contain ERROR. + */ + @Test + def void testSeverityElevationFromChildren() { + // Create child messages with different severities + val child1 = createStatusReport(Severity.WARNING, "Warning message", "source1", 1) + val child2 = createStatusReport(Severity.ERROR, "Error message", "source2", 2) + val child3 = createStatusReport(Severity.INFO, "Info message", "source3", 3) + + // Create parent with OK severity but ERROR children + val parent = createStatusReport(Severity.OK, #[child1, child2, child3]) + + // Verify severity was elevated to ERROR (the highest in children) + Assertions.assertEquals(Severity.ERROR, parent.severity(), "Parent severity should be elevated to ERROR") + Assertions.assertTrue(parent.isError(), "Parent should be an error") + } + + /** + * Test that severity is elevated to the highest severity when it's the minimum initially. + * Expected: parent severity should be elevated from INFO to CANCEL when children contain CANCEL. + */ + @Test + def void testSeverityElevationToCancel() { + // Create child with CANCEL severity + val child = createStatusReport(Severity.CANCEL, "Cancelled", "source", 1) + + // Create parent with INFO severity + val parent = createStatusReport(Severity.INFO, #[child]) + + // Verify severity was elevated to CANCEL (the highest possible) + Assertions.assertEquals(Severity.CANCEL, parent.severity(), "Parent severity should be elevated to CANCEL") + Assertions.assertTrue(parent.isError(), "Parent should be an error (CANCEL is error)") + } + + /** + * Test that severity remains unchanged when parent has higher severity than children. + * Expected: parent severity should remain ERROR when children are only WARNING. + */ + @Test + def void testSeverityRemainsWhenHigherThanChildren() { + // Create children with WARNING severity + val child1 = createStatusReport(Severity.WARNING, "Warning 1", "source1", 1) + val child2 = createStatusReport(Severity.WARNING, "Warning 2", "source2", 2) + + // Create parent with ERROR severity (higher than children) + val parent = createStatusReport(Severity.ERROR, #[child1, child2]) + + // Verify severity remains ERROR + Assertions.assertEquals(Severity.ERROR, parent.severity(), "Parent severity should remain ERROR") + } + + /** + * Test that severity is not modified when children is null or empty. + * Expected: severity should remain unchanged. + */ + @Test + def void testSeverityWithNullOrEmptyChildren() { + // Test with null children + val parentWithNull = createStatusReport(Severity.INFO, null) + Assertions.assertEquals(Severity.INFO, parentWithNull.severity(), + "Severity should remain INFO with null children") + + // Test with empty children + val parentWithEmpty = createStatusReport(Severity.WARNING, #[]) + Assertions.assertEquals(Severity.WARNING, parentWithEmpty.severity(), + "Severity should remain WARNING with empty children") + } + + /** + * Factory method to create a StatusReport with only the needed arguments. + * Defaults for location fields: startLine=0, endLine=0, offset=0, length=0, text="" + */ + private def static StatusReport createStatusReport( + Severity severity, + String message, + String source, + int code + ) { + return new StatusReport(severity, message, source, code, null, null, null, null, null, null, null) + } + + /** + * Factory method with location parameters + */ + private def static StatusReport createStatusReport( + Severity severity, + String message, + String source, + int code, + String details, + int startLine, + int endLine, + int offset, + int length, + String text + ) { + return new StatusReport(severity, message, source, code, details, startLine, endLine, offset, length, + text, null) + } + + /** + * Factory method with only severity and children. + * All other fields use defaults: message="", source="", code=0, details=null, + * startLine=0, endLine=0, offset=0, length=0, text="" + */ + private def static StatusReport createStatusReport( + Severity severity, + List children + ) { + return new StatusReport(severity, "", "", 0, null, 0, 0, 0, 0, "", children) + } + +} diff --git a/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF b/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF index 7b58165..2b55ebb 100644 --- a/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF +++ b/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF @@ -14,7 +14,8 @@ Require-Bundle: org.eclipse.xtext, org.eclipse.xtend.lib;bundle-version="2.36.0", org.eclipse.emf.common, org.antlr.runtime;bundle-version="[3.2.0,3.2.1)", - org.eclipse.core.resources + org.eclipse.core.resources, + com.google.gson;bundle-version="[2.8.0,3.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-21 Export-Package: nl.esi.xtext.common.lang, nl.esi.xtext.common.lang.base, @@ -25,6 +26,7 @@ Export-Package: nl.esi.xtext.common.lang, nl.esi.xtext.common.lang.generator, nl.esi.xtext.common.lang.parser.antlr, nl.esi.xtext.common.lang.parser.antlr.internal, + nl.esi.xtext.common.lang.reporting, nl.esi.xtext.common.lang.scoping, nl.esi.xtext.common.lang.serializer, nl.esi.xtext.common.lang.services, diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Severity.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Severity.java new file mode 100644 index 0000000..f6f9967 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Severity.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +/** + * Enum representing the severity level of a validation message. + */ +public enum Severity { + /** + * Severity indicating everything is okay. + */ + OK(0x0), + + /** + * Severity indicating there is an informational message. + */ + INFO(0x1), + + /** + * Severity indicating there is a warning message. + */ + WARNING(0x2), + + /** + * Severity indicating there is an error message. + */ + ERROR(0x4), + + /** + * Severity indicating that the diagnosis was canceled. + */ + CANCEL(0x8); + + /** + * The numeric value representing this severity level. + */ + public final int value; + + /** + * Constructs a Severity with the given numeric value. + * + * @param value the numeric severity value + */ + private Severity(int value) { + this.value = value; + } + + /** + * Converts a numeric severity value to the corresponding Severity enum constant. + * + * @param value the numeric severity value + * @return the corresponding Severity enum constant, defaults to OK if not found + */ + public static Severity fromValue(int value) { + for (Severity severity : Severity.values()) { + if (severity.value == value) { + return severity; + } + } + return OK; + } + + /** + * Checks if this severity indicates an error condition. + * + * @return true if this is ERROR or CANCEL, false otherwise + */ + public boolean isError() { + return (this.value & (ERROR.value | CANCEL.value)) != 0; + } + +} diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java new file mode 100644 index 0000000..4f9f9f3 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +import java.util.List; + +import org.eclipse.emf.common.util.Diagnostic; + +/** + * A record that represents the outcome of some processing activity, + * mimicking the {@link Diagnostic} interface and serializable with Gson. + */ +public record StatusReport( + Severity severity, + String message, + String source, + Integer code, + String details, + Integer startLine, + Integer endLine, + Integer offset, + Integer length, + String text, + List children +) { + /** + * Creates a new ValidationMessage with all fields. + * + * The severity field will be automatically elevated to the highest severity found in the children list, + * ensuring that the parent always reflects the most severe condition among its children. + * + * @param severity the initial severity level (will be elevated if children have higher severity) + * @param message the message describing the situation + * @param source the source identifier + * @param code the source-specific identity code + * @param details the details string (representing low-level exception information) + * @param startLine the starting line number of the location + * @param endLine the ending line number of the location + * @param offset the character offset in the source + * @param length the length of the located text + * @param text the located text content + * @param children the list of child validation messages (severity will be elevated based on these) + */ + public StatusReport { + // Calculate the highest severity from children + if (children != null && children.isEmpty()) { + // Set children to null if empty + children = null; + } + if (children != null) { + Severity maxSeverity = severity; + for (StatusReport child : children) { + if (child != null && child.severity.value > maxSeverity.value) { + maxSeverity = child.severity; + } + } + // Re-assign severity to the highest found + severity = maxSeverity; + } + } + + /** + * Returns a string representation of the severity level. + * + * @return a string describing the severity + */ + public String getSeverityString() { + return severity.name(); + } + + /** + * Checks if this report indicates an error condition. + * + * @return true if severity is ERROR or CANCEL, false otherwise + */ + public boolean isError() { + return severity.isError(); + } + +} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java new file mode 100644 index 0000000..59abd1a --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.emf.common.util.BasicDiagnostic; +import org.eclipse.emf.common.util.Diagnostic; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.Diagnostician; +import org.eclipse.emf.ecore.util.EObjectValidator; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import nl.esi.xtext.common.lang.utilities.EcoreUtil3; + + +public class StatusReportHelper { + + /** + * Gson instance for JSON serialization/deserialization. + */ + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .create(); + + /** + * validates the given EObject. + * @see {@link EcoreUtil3#validate(EObject)} + */ + public static StatusReport validate(EObject eObject) { + Diagnostician diagnostician = new Diagnostician(); + BasicDiagnostic diagnostics = diagnostician.createDefaultDiagnostic(eObject); + Map context = diagnostician.createDefaultContext(); + + diagnostician.validate(eObject, diagnostics, context); + return fromDiagnostic(diagnostics); + } + + /** + * validate the given EObject and add it to the reports if not OK. + */ + public static void validate(List reports, EObject object) { + var report = validate(object); + if(report.severity() != Severity.OK) { + reports.add(report); + } + } + + /** + * validate the given resource + * @see {@link EcoreUtil3#validate(EObject)} + */ + public static StatusReport validate(Resource resource) { + Diagnostician diagnostician = new Diagnostician(); + BasicDiagnostic diagnostics = new BasicDiagnostic(EObjectValidator.DIAGNOSTIC_SOURCE, 0, + "Diagnosis of " + resource.getURI(), new Object[] { resource }); + Map context = diagnostician.createDefaultContext(); + + for (EObject eObject : resource.getContents()) { + diagnostician.validate(eObject, diagnostics, context); + } + + return fromDiagnostic(diagnostics); + + } + + /** + * validate the given resource and add it to the reports if not OK. + * returns true if the resource an error exists in reports after validation. + */ + public static boolean validate(List reports, Resource resource) { + var report = validate(resource); + if(report.severity() != Severity.OK) { + reports.add(report); + } + return reports.stream().anyMatch(r->r.severity() == Severity.ERROR); + } + + /** + * Serializes this ValidationMessage to a JSON string using Gson. + * + * @return a JSON string representation of this ValidationMessage + */ + public static String toJson(StatusReport validationMessage) { + return GSON.toJson(validationMessage); + } + + + + public static StatusReport fromJson(String jsonString) { + return GSON.fromJson(jsonString, StatusReport.class); + } + + /** + * Generates a ValidationMessage from a Diagnostic instance, including Children. + * + * Mapping: + * - severity -> severity + * - message -> message + * - source -> source + * - code -> code + * - exception -> details (or null if none) + * - children -> children (recursively converted) + * - location -> startLine, endLine, offset, length, text (extracted from diagnostic data if available) + * + * @param diagnostic the diagnostic to convert + * @return a new ValidationMessage based on the diagnostic + */ + public static StatusReport fromDiagnostic(Diagnostic diagnostic) { + if (diagnostic == null) { + return null; + } + + String details = null; + Throwable exception = diagnostic.getException(); + if (exception != null) { + StringBuilder sb = new StringBuilder(); + sb.append(exception.getClass().getName()); + if (exception.getMessage() != null) { + sb.append(": ").append(exception.getMessage()); + } + sb.append("\n"); + + // Append stack trace (limited to 15 elements) + StackTraceElement[] stackTrace = exception.getStackTrace(); + int limit = Math.min(stackTrace.length, 15); + for (int i = 0; i < limit; i++) { + sb.append("\tat ").append(stackTrace[i]).append("\n"); + } + if (stackTrace.length > 15) { + sb.append("\t... ").append(stackTrace.length - 15).append(" more\n"); + } + + details = sb.toString(); + } + + List children = null; + List diagnosticChildren = diagnostic.getChildren(); + if (diagnosticChildren != null && !diagnosticChildren.isEmpty()) { + children = diagnosticChildren.stream() + .map(StatusReportHelper::fromDiagnostic) + .toList(); + } + + var source = extractSource(diagnostic); + var node = extractLocationData(diagnostic); + + return new StatusReport( + Severity.fromValue(diagnostic.getSeverity()), + diagnostic.getMessage(), + source, + diagnostic.getCode(), + details, + node!= null ? node.getStartLine(): null, + node!= null ? node.getEndLine(): null, + node!= null ? node.getOffset(): null, + node!= null ? node.getLength(): null, + node!= null ? node.getText(): null , + children + ); + } + + /** + * Converts an exception to a StatusReport, including cause chain as children. + * Detects cycles in the cause chain to prevent infinite loops. + * + * @param exception the exception to convert + * @param linkedHashSet + * @return a new StatusReport with the exception details and cause chain + */ + public static StatusReport fromException(Throwable exception, String userMessage, List children) { + return fromException(exception, userMessage, children, new LinkedHashSet<>()); + } + + public static StatusReport errorReport(String errorMessage, List children) { + return fromMessage(Severity.ERROR, errorMessage, children); + } + + public static StatusReport warningReport( String errorMessage, List children) { + return fromMessage(Severity.WARNING, errorMessage, children); + } + + public static StatusReport infoReport(String errorMessage, List children) { + return fromMessage(Severity.INFO, errorMessage, children); + } + + private static StatusReport fromMessage(Severity severity, String errorMessage, List children) { + + return new StatusReport( + severity, + errorMessage, + null, // source - unknown + null, // code - default + null, // details - none + null, // startLine - unknown + null, // endLine - unknown + null, // offset - unknown + null, // length - unknown + null, // text - unknown + children + ); + } + + /** + * Extracts location information from a Diagnostic instance. + * The location is derived from the diagnostic's data array, typically containing + * EObject or Resource information. + * + * @param diagnostic the diagnostic to extract location from + * @return a location string if available, null otherwise + */ + private static String extractSource(Diagnostic diagnostic) { + if (diagnostic == null) { + return null; + } + + List data = diagnostic.getData(); + if (data != null && !data.isEmpty()) { + Object firstData = data.get(0); + if (firstData instanceof EObject eObject) { + Resource resource = eObject.eResource(); + if (resource != null) { + return resource.getURI().toString(); + } + } else if (firstData instanceof Resource resource) { + return resource.getURI().toString(); + } + } + + return null; + } + + /** + * Gets detailed location information including line, column, offset + * + * @param diagnostic The diagnostic to extract location from + * @return Object array [startLine, endLine, offset, length, text] with default + * values if not available + */ + private static ICompositeNode extractLocationData(Diagnostic diagnostic) { + if (diagnostic == null || diagnostic.getData() == null || diagnostic.getData().isEmpty()) { + return null; + } + + Object firstData = diagnostic.getData().get(0); + + if (firstData instanceof EObject eObject) { + ICompositeNode node = NodeModelUtils.getNode(eObject); + + if (node != null) { + return node; + } + } + + return null; + } + + /** + * Internal method to convert exception with cycle detection. + * + * @param exception the exception to convert + * @param seenExceptions set of already processed exceptions to detect cycles + * @return a new StatusReport with the exception details and cause chain + */ + private static StatusReport fromException(Throwable exception, String userMessage, List children, Set seenExceptions) { + if (exception == null) { + return null; + } + + // Check for cycle + if (seenExceptions.contains(exception)) { + return null; + } + + seenExceptions.add(exception); + + // Build details string with class name and message + StringBuilder details = new StringBuilder(); + details.append(exception.getClass().getName()); + if (exception.getMessage() != null) { + details.append(": ").append(exception.getMessage()); + } + details.append("\n"); + + // Append stack trace (limited to 15 elements) + StackTraceElement[] stackTrace = exception.getStackTrace(); + int limit = Math.min(stackTrace.length, 15); + for (int i = 0; i < limit; i++) { + details.append("\tat ").append(stackTrace[i]).append("\n"); + } + if (stackTrace.length > 15) { + details.append("\t... ").append(stackTrace.length - 15).append(" more\n"); + } + + // Process cause as child (if exists) + List allChildren = new ArrayList(); + if (children != null) { + allChildren.addAll(children); + } + Throwable cause = exception.getCause(); + if (cause != null) { + StatusReport childReport = fromException(cause, null, null, seenExceptions); + if (childReport != null) { + allChildren.add(childReport); + } + } + + // Create StatusReport with defaults for unknown fields + StringBuffer message = new StringBuffer(); + if (userMessage != null && !userMessage.isEmpty()) { + message.append(userMessage).append("\n"); + } + message.append(exception.getLocalizedMessage() != null ? exception.getLocalizedMessage() : exception.getClass().getSimpleName()); + return new StatusReport( + Severity.ERROR, + message.toString(), + null, // source - unknown + null, // code - default + details.toString(), + null, // startLine - unknown + null, // endLine - unknown + null, // offset - unknown + null, // length - unknown + null, // text - unknown + allChildren + ); + } + +} diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java new file mode 100644 index 0000000..8d120a5 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +/** + * An interface for reporting status messages, allowing implementations to + * handle the reporting of validation messages. + */ +public interface StatusReporting { + /** + * Reports a status message. + * + * @param report the status report to be reported + */ + void addReport(StatusReport report); +} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java new file mode 100644 index 0000000..c6e4aa1 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * An interface for reporting status messages, allowing implementations to + * handle the reporting of validation messages. + */ +public class StatusReportingSupport implements StatusReporting { + List reports = Collections.synchronizedList(new ArrayList<>()); + /** + * Reports a status message. + * + * @param report the status report to be reported + */ + @Override + public void addReport(StatusReport report) { + reports.add(report); + } + + public List getReports() { + return Collections.unmodifiableList(reports); + } +} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend index e1aa22a..7da1bf3 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend @@ -11,8 +11,9 @@ package nl.esi.xtext.types.generator import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator +import nl.esi.xtext.common.lang.reporting.StatusReportingSupport -class CmdLineContext implements IGeneratorContext { +class CmdLineContext extends StatusReportingSupport implements IGeneratorContext { final static String context = "CMD_LINE" diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend index b560321..fa220b7 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend @@ -9,10 +9,11 @@ */ package nl.esi.xtext.types.generator +import nl.esi.xtext.common.lang.reporting.StatusReportingSupport import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator -class SpecDiffContext implements IGeneratorContext { +class SpecDiffContext extends StatusReportingSupport implements IGeneratorContext { public String oriFeaturePath public String updFeaturePath diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend index 80c5b38..11e722b 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend @@ -9,11 +9,12 @@ */ package nl.esi.xtext.types.generator +import java.util.List +import nl.esi.xtext.common.lang.reporting.StatusReportingSupport import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator -import java.util.List -class StepsParserContext implements IGeneratorContext { +class StepsParserContext extends StatusReportingSupport implements IGeneratorContext { public String stepsFilePath public List testContext diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend index 4089cdf..b86e8ed 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend @@ -9,10 +9,11 @@ */ package nl.esi.xtext.types.generator -import org.eclipse.xtext.generator.IGeneratorContext import java.nio.file.Path +import nl.esi.xtext.common.lang.reporting.StatusReportingSupport +import org.eclipse.xtext.generator.IGeneratorContext -class TestSpecContext implements IGeneratorContext { +class TestSpecContext extends StatusReportingSupport implements IGeneratorContext { public Path tspecPath diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend index c0178d2..de7fb1b 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend @@ -18,20 +18,21 @@ import java.nio.file.LinkOption import java.nio.file.Path import java.nio.file.Paths import java.text.MessageFormat +import java.util.ArrayList import java.util.Comparator +import java.util.List +import nl.esi.xtext.common.lang.reporting.Severity +import nl.esi.xtext.common.lang.reporting.StatusReport +import nl.esi.xtext.common.lang.reporting.StatusReportHelper import org.apache.commons.cli.CommandLine import org.apache.commons.cli.DefaultParser import org.apache.commons.cli.HelpFormatter import org.apache.commons.cli.Options import org.eclipse.emf.common.util.URI import org.eclipse.emf.ecore.resource.ResourceSet -import org.eclipse.xtext.diagnostics.Severity import org.eclipse.xtext.generator.GeneratorDelegate import org.eclipse.xtext.generator.JavaIoFileSystemAccess -import org.eclipse.xtext.util.CancelIndicator import org.eclipse.xtext.util.StringInputStream -import org.eclipse.xtext.validation.CheckMode -import org.eclipse.xtext.validation.IResourceValidator class XPlusMain { /* @@ -135,8 +136,6 @@ class XPlusMain { @Inject Provider resourceSetProvider - @Inject IResourceValidator validator - @Inject GeneratorDelegate generator @Inject JavaIoFileSystemAccess fileAccess @@ -157,9 +156,7 @@ class XPlusMain { val options = createOptions if (args.empty) { - System::err.println(ERR_LOCATION_MISSING) - showInfo(description, options) - System.exit(1); + abort(ERR_LOCATION_MISSING, options) return } @@ -198,9 +195,7 @@ class XPlusMain { cleanOutput(cmdLine, outputdir) if (tspecPath === null) { - System::err.println(ERR_LOCATION_MISSING) - showInfo(description, options) - System.exit(1) + abort(ERR_LOCATION_MISSING, options) return } @@ -223,7 +218,7 @@ class XPlusMain { generator.generate(resource, fileAccess, cliContext) } - + def specDiffOPT(CommandLine cmdLine, Options options) { val oriFeaturePath = getLocation(cmdLine, OPT_ORIGINALFEATURES) val updFeaturePath = getLocation(cmdLine, OPT_UPDATEDFEATURES) @@ -239,19 +234,15 @@ class XPlusMain { if (oriFeaturePath === null || updFeaturePath === null ){ - System::err.println(ERR_LOCATION_MISSING) - showInfo(description, options) - System.exit(1) + abort(ERR_LOCATION_MISSING, options) return } if (!Files.exists(oriFeaturePath, LinkOption.NOFOLLOW_LINKS)) { - System.out.println(ERR_LOCATION_WRONG + oriFeaturePath.toString) - System.exit(1) + abort(ERR_LOCATION_WRONG + oriFeaturePath.toString) return } if (!Files.exists(updFeaturePath, LinkOption.NOFOLLOW_LINKS)) { - System.out.println(ERR_LOCATION_WRONG + updFeaturePath.toString) - System.exit(1) + abort(ERR_LOCATION_WRONG + updFeaturePath.toString) return } @@ -267,20 +258,16 @@ class XPlusMain { def stepsParserOPT(CommandLine cmdLine, Options options) { val stepPath = getLocation(cmdLine, OPT_STEPSFILES) if (stepPath === null) { - System::err.println(ERR_LOCATION_MISSING) - showInfo(description, options) - System.exit(1) + abort(ERR_LOCATION_MISSING, options) return } if (!Files.exists(stepPath, LinkOption.NOFOLLOW_LINKS)) { - System.out.println(ERR_LOCATION_WRONG + stepPath.toString) - System.exit(1) + abort(ERR_LOCATION_WRONG + stepPath.toString) return } val context = cmdLine.getOptionValue(OPT_CONTEXT) if (context === null) { - System::err.println(ERR_CONTEXT_MISSING) - System.exit(1) + abort(ERR_CONTEXT_MISSING) return } val outputdir = Paths.get(cmdLine.getOptionValue(OPT_TESTCONFIGOUTPUT)) @@ -303,14 +290,11 @@ class XPlusMain { def monitoringOPT(CommandLine cmdLine, Options options) { val locationPath = getLocation(cmdLine, OPT_LOCATION) if (locationPath === null) { - System::err.println(ERR_LOCATION_MISSING) - showInfo(description, options) - System.exit(1) + abort(ERR_LOCATION_MISSING, options) return } if (!Files.exists(locationPath, LinkOption.NOFOLLOW_LINKS)) { - System.out.println(ERR_LOCATION_WRONG + locationPath.toString) - System.exit(1) + abort(ERR_LOCATION_WRONG + locationPath.toString) return } @@ -318,24 +302,34 @@ class XPlusMain { val outputdir = getOutputdir(cmdLine, locationPath, OPT_OUTPUT) System.out.println(INFO_OUTPUT + outputdir) cleanOutput(cmdLine, outputdir) - + var statusReports = new ArrayList() as List + if (Files.isDirectory(locationPath, LinkOption.NOFOLLOW_LINKS)) { println(MessageFormat.format(INFO_SEARCHING, language, ext, locationPath.toString)) val dir = new File(locationPath.toString) val prjFiles = dir.listFiles(createFileFilter(ext)); for (file : prjFiles) { - runGeneration(file.absolutePath, outputdir.toString, validation) + val reports = runGeneration(file.absolutePath, outputdir.toString, validation) + statusReports.addAll(reports) + if( reports.exists[severity == Severity.ERROR]) { + exit(StatusReportHelper.errorReport(INFO_STOP, statusReports), null) + } } if (prjFiles.empty) { System.out.println(INFO_EMPTY_LOCATION + locationPath.toString) } } else { - runGeneration(locationPath.toString, outputdir.toString, validation) + val reports = runGeneration(locationPath.toString, outputdir.toString, validation) + statusReports.addAll(reports) + if( reports.exists[severity == Severity.ERROR]) { + exit(StatusReportHelper.errorReport(INFO_STOP, statusReports), null) + } } System.out.println(INFO_GENERATION_FINISHED) System.out.println("") - System.out.println(INFO_XPLUS_FINISHED) + System.out.println(INFO_XPLUS_FINISHED) + exit(StatusReportHelper.infoReport(INFO_GENERATION_FINISHED, statusReports), null) } def Options createOptions() { @@ -493,46 +487,42 @@ class XPlusMain { generator.generate(resource, fileAccess, cliContext) } - def runGeneration(String string, String outputdir, Boolean validation) { + def List runGeneration(String string, String outputdir, Boolean validation) { // Load the resource System.out.println(INFO_READING + string) val set = resourceSetProvider.get val resource = set.getResource(URI.createFileURI(string), true) - + val context = new CmdLineContext // Validate the resource if (validation) { - val issues = validator.validate(resource, CheckMode.ALL, CancelIndicator.NullImpl) - if (!issues.empty) { - issues.forEach[System.err.println(it)] - } - - val errors = issues.filter[it.severity == Severity.ERROR] - if (!errors.empty) { - System.out.println(INFO_STOP) - System.exit(1) - return; - } + val report = StatusReportHelper.validate(resource) + context.addReport(report) + if (report.error) { + // stop when error else continue + return context.reports + } } // Configure and start the generator fileAccess.outputPath = outputdir setXPlusGen(fileAccess, outputdir) try { - generator.generate(resource, fileAccess, new CmdLineContext) - } catch (Exception e) { - System.err.println(e.localizedMessage) - System.err.println(INFO_STOP) - System.exit(1) - return; - } + generator.generate(resource, fileAccess, context) + } catch (Exception e) { + //create a status report + val report = StatusReportHelper.fromException(e, INFO_STOP, null); + context.addReport(report) + + } + return context.reports } def cleanOutput(CommandLine cmdLine, Path outputdir) { if (cmdLine.hasOption(OPT_CLEAN)) { println("Start cleaning output directory.") - val xplusgendir = outputdir.parent.resolve(nl.esi.xtext.types.generator.XPlusFileSystemAccess.XPLUS_OUTPUT_CONF.outputDirectory).normalize + val xplusgendir = outputdir.parent.resolve(XPlusFileSystemAccess.XPLUS_OUTPUT_CONF.outputDirectory).normalize //clean src-gen if(new File(outputdir.toString).exists){ Files.walk(outputdir) @@ -550,13 +540,13 @@ class XPlusMain { } def setXPlusGen(JavaIoFileSystemAccess fileAccess, String outputdir) { - val xplusConf = nl.esi.xtext.types.generator.XPlusFileSystemAccess.XPLUS_OUTPUT_CONF + val xplusConf = XPlusFileSystemAccess.XPLUS_OUTPUT_CONF xplusConf.outputDirectory = Paths.get(outputdir).parent.toString + xplusConf.outputDirectory - fileAccess.outputConfigurations.put(nl.esi.xtext.types.generator.XPlusFileSystemAccess.XPLUS_OUTPUT_ID, xplusConf) + fileAccess.outputConfigurations.put(XPlusFileSystemAccess.XPLUS_OUTPUT_ID, xplusConf) } def getXPlusGen() { - val xplusGen = fileAccess.outputConfigurations.get(nl.esi.xtext.types.generator.XPlusFileSystemAccess.XPLUS_OUTPUT_ID) + val xplusGen = fileAccess.outputConfigurations.get(XPlusFileSystemAccess.XPLUS_OUTPUT_ID) xplusGen.outputDirectory } @@ -566,5 +556,31 @@ class XPlusMain { println("Current directory: " + dir.toPath.toString) return dir } - + + private def void abort(String message) { + abort(message, null) + } + + private def void abort(String message, Options options) { + exit(StatusReportHelper.errorReport(message,null), options) + } + + private def void exit(StatusReport statusReport, Options options) { + //be backwards compatible + System.err.println(statusReport.message()) + saveReport(statusReport) + if(options !== null) { + showInfo(description, options) + } + System.out.println(String.format("Exiting with status %s(%d)", statusReport.severity().name, statusReport.severity().value)) + System.err.println(String.format("Exiting with status %s(%d)", statusReport.severity().name, statusReport.severity().value)) + System.exit(statusReport.severity().value) + } + + private def void saveReport(StatusReport report) { + //store the report with name `StatusReport.json` in the root of file_access + val resource = "report/StatusReport.json" + val charSequence = StatusReportHelper.toJson(report) + fileAccess.generateFile(resource, charSequence); + } } \ No newline at end of file From da149810518b5d0efa988a6265274f9fd3eb4e31 Mon Sep 17 00:00:00 2001 From: Paul Nelissen Date: Thu, 16 Jul 2026 09:40:27 +0200 Subject: [PATCH 2/6] Prepare 0.4.0 in launch file --- bundles/nl.esi.xtext.launch/Maven Update Version.launch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/nl.esi.xtext.launch/Maven Update Version.launch b/bundles/nl.esi.xtext.launch/Maven Update Version.launch index e038a6f..89c3ad7 100644 --- a/bundles/nl.esi.xtext.launch/Maven Update Version.launch +++ b/bundles/nl.esi.xtext.launch/Maven Update Version.launch @@ -7,7 +7,7 @@ - + From 315b37874ac9ee435c3514c6247d8ff2c5bf1cfe Mon Sep 17 00:00:00 2001 From: Paul Nelissen Date: Thu, 16 Jul 2026 14:59:29 +0200 Subject: [PATCH 3/6] use IStatusReporting --- ...portingSupport.java => IStatusReporting.java} | 16 ++-------------- .../lang/reporting/StatusReportHelper.java | 6 +++++- .../common/lang/reporting/StatusReporting.java | 16 ++++++++++++++-- .../xtext/types/generator/CmdLineContext.xtend | 4 ++-- .../xtext/types/generator/SpecDiffContext.xtend | 4 ++-- .../types/generator/StepsParserContext.xtend | 4 ++-- .../xtext/types/generator/TestSpecContext.xtend | 4 ++-- .../nl/esi/xtext/types/generator/XPlusMain.xtend | 2 +- 8 files changed, 30 insertions(+), 26 deletions(-) rename bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/{StatusReportingSupport.java => IStatusReporting.java} (57%) diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java similarity index 57% rename from bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java rename to bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java index c6e4aa1..f6c1b64 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingSupport.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java @@ -9,27 +9,15 @@ */ package nl.esi.xtext.common.lang.reporting; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - /** * An interface for reporting status messages, allowing implementations to * handle the reporting of validation messages. */ -public class StatusReportingSupport implements StatusReporting { - List reports = Collections.synchronizedList(new ArrayList<>()); +public interface IStatusReporting { /** * Reports a status message. * * @param report the status report to be reported */ - @Override - public void addReport(StatusReport report) { - reports.add(report); - } - - public List getReports() { - return Collections.unmodifiableList(reports); - } + void addReport(StatusReport report); } \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java index 59abd1a..1f85318 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java @@ -165,7 +165,7 @@ public static StatusReport fromDiagnostic(Diagnostic diagnostic) { Severity.fromValue(diagnostic.getSeverity()), diagnostic.getMessage(), source, - diagnostic.getCode(), + diagnostic.getCode() == 0 ? null : diagnostic.getCode(), details, node!= null ? node.getStartLine(): null, node!= null ? node.getEndLine(): null, @@ -200,6 +200,10 @@ public static StatusReport infoReport(String errorMessage, List ch return fromMessage(Severity.INFO, errorMessage, children); } + public static StatusReport okReport(String errorMessage, List children) { + return fromMessage(Severity.OK, errorMessage, children); + } + private static StatusReport fromMessage(Severity severity, String errorMessage, List children) { return new StatusReport( diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java index 8d120a5..5079199 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java @@ -9,15 +9,27 @@ */ package nl.esi.xtext.common.lang.reporting; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * An interface for reporting status messages, allowing implementations to * handle the reporting of validation messages. */ -public interface StatusReporting { +public class StatusReporting implements IStatusReporting { + List reports = Collections.synchronizedList(new ArrayList<>()); /** * Reports a status message. * * @param report the status report to be reported */ - void addReport(StatusReport report); + @Override + public void addReport(StatusReport report) { + reports.add(report); + } + + public List getReports() { + return Collections.unmodifiableList(reports); + } } \ No newline at end of file diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend index 7da1bf3..07c5402 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend @@ -11,9 +11,9 @@ package nl.esi.xtext.types.generator import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator -import nl.esi.xtext.common.lang.reporting.StatusReportingSupport +import nl.esi.xtext.common.lang.reporting.StatusReporting -class CmdLineContext extends StatusReportingSupport implements IGeneratorContext { +class CmdLineContext extends StatusReporting implements IGeneratorContext { final static String context = "CMD_LINE" diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend index fa220b7..eaf28ce 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend @@ -9,11 +9,11 @@ */ package nl.esi.xtext.types.generator -import nl.esi.xtext.common.lang.reporting.StatusReportingSupport import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator +import nl.esi.xtext.common.lang.reporting.StatusReporting -class SpecDiffContext extends StatusReportingSupport implements IGeneratorContext { +class SpecDiffContext extends StatusReporting implements IGeneratorContext { public String oriFeaturePath public String updFeaturePath diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend index 11e722b..68d2d3c 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend @@ -10,11 +10,11 @@ package nl.esi.xtext.types.generator import java.util.List -import nl.esi.xtext.common.lang.reporting.StatusReportingSupport import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator +import nl.esi.xtext.common.lang.reporting.StatusReporting -class StepsParserContext extends StatusReportingSupport implements IGeneratorContext { +class StepsParserContext extends StatusReporting implements IGeneratorContext { public String stepsFilePath public List testContext diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend index b86e8ed..cc73d4f 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend @@ -10,10 +10,10 @@ package nl.esi.xtext.types.generator import java.nio.file.Path -import nl.esi.xtext.common.lang.reporting.StatusReportingSupport import org.eclipse.xtext.generator.IGeneratorContext +import nl.esi.xtext.common.lang.reporting.StatusReporting -class TestSpecContext extends StatusReportingSupport implements IGeneratorContext { +class TestSpecContext extends StatusReporting implements IGeneratorContext { public Path tspecPath diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend index de7fb1b..caf67b1 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend @@ -329,7 +329,7 @@ class XPlusMain { System.out.println(INFO_GENERATION_FINISHED) System.out.println("") System.out.println(INFO_XPLUS_FINISHED) - exit(StatusReportHelper.infoReport(INFO_GENERATION_FINISHED, statusReports), null) + exit(StatusReportHelper.okReport(INFO_GENERATION_FINISHED, statusReports), null) } def Options createOptions() { From 265c49c3607dd9581dfbf24f4aec76ad66df4be4 Mon Sep 17 00:00:00 2001 From: Paul Nelissen Date: Fri, 24 Jul 2026 11:07:49 +0200 Subject: [PATCH 4/6] Introduce IStatusReporting --- .../META-INF/MANIFEST.MF | 3 +- .../reporting/StatusReportHelperTest.xtend | 149 +++------ .../META-INF/MANIFEST.MF | 1 + .../lang/reporting/IStatusReporting.java | 11 +- .../xtext/common/lang/reporting/Location.java | 25 ++ .../common/lang/reporting/StatusReport.java | 315 ++++++++++++++---- .../lang/reporting/StatusReportCollector.java | 53 +++ .../lang/reporting/StatusReportHelper.java | 184 +++++----- .../lang/reporting/StatusReporting.java | 35 -- .../StatusReportingEclipseRuntime.java | 67 ++++ .../types/generator/CmdLineContext.xtend | 3 +- .../types/generator/SpecDiffContext.xtend | 3 +- .../types/generator/StepsParserContext.xtend | 5 +- .../types/generator/TestSpecContext.xtend | 5 +- .../esi/xtext/types/generator/XPlusMain.xtend | 47 +-- 15 files changed, 588 insertions(+), 318 deletions(-) create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportCollector.java delete mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java create mode 100644 bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingEclipseRuntime.java diff --git a/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF b/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF index 9a6af35..d733a60 100644 --- a/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF +++ b/bundles/nl.esi.xtext.common.lang.tests/META-INF/MANIFEST.MF @@ -9,7 +9,8 @@ Bundle-ActivationPolicy: lazy Require-Bundle: nl.esi.xtext.common.lang, org.eclipse.xtext.testing, org.eclipse.xtext.xbase.testing, - org.eclipse.xtext.xbase.lib;bundle-version="2.36.0" + org.eclipse.xtext.xbase.lib;bundle-version="2.36.0", + org.eclipse.core.runtime Import-Package: org.junit.jupiter.api;version="[5.1.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.1.0,6.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend b/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend index 17f9184..bcb69e9 100644 --- a/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend +++ b/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend @@ -13,6 +13,7 @@ import com.google.inject.Inject import java.util.List import nl.esi.xtext.common.lang.base.ModelContainer import nl.esi.xtext.common.lang.tests.BaseInjectorProvider +import nl.esi.xtext.common.lang.reporting.Location import nl.esi.xtext.common.lang.reporting.Severity import nl.esi.xtext.common.lang.reporting.StatusReportHelper import nl.esi.xtext.common.lang.reporting.StatusReport @@ -44,10 +45,9 @@ class StatusReportHelperTest { ''') Assertions.assertNotNull(result) - val StatusReport = StatusReportHelper.validate(result) - Assertions.assertNotNull(StatusReport) - Assertions.assertEquals(Severity.OK, StatusReport.severity, "Validation should succeed with OK severity") - Assertions.assertNotNull(StatusReport.message) + val statusReport = StatusReportHelper.validate(result) + Assertions.assertNotNull(statusReport) + Assertions.assertEquals(Severity.OK, statusReport.getSeverityLevel(), "Validation should succeed with OK severity") } /** @@ -63,10 +63,10 @@ class StatusReportHelperTest { ''') Assertions.assertNotNull(result) - val StatusReport = StatusReportHelper.validate(result) - Assertions.assertNotNull(StatusReport) - Assertions.assertFalse(StatusReport.isError(), "Validation should not report errors") - Assertions.assertTrue(StatusReport.severity == Severity.OK, "Validation should be OK") + val statusReport = StatusReportHelper.validate(result) + Assertions.assertNotNull(statusReport) + Assertions.assertFalse(statusReport.isError(), "Validation should not report errors") + Assertions.assertTrue(statusReport.getSeverityLevel() == Severity.OK, "Validation should be OK") } /** @@ -84,18 +84,14 @@ class StatusReportHelperTest { val resource = result.eResource Assertions.assertNotNull(resource) - val StatusReport = StatusReportHelper.validate(resource) - Assertions.assertNotNull(StatusReport) - Assertions.assertNotNull(StatusReport.message) - Assertions.assertTrue(StatusReport.message.length > 0, "Validation message should not be empty") - Assertions.assertTrue(StatusReport.children.size > 1, "There should be validation errors") - Assertions.assertTrue(StatusReport.children.get(0).message().contains("Duplicate"), - "Expected duplicate validation error") + val statusReport = StatusReportHelper.validate(resource) + Assertions.assertNotNull(statusReport) + Assertions.assertTrue(statusReport.getChildReports().isPresent(), "There should be validation errors") + Assertions.assertTrue(statusReport.getChildReports().get().size > 0, "There should be child reports") } /** - * Test validation of a model resource (via eResource). - * Expected: validation should complete successfully. + * Test GSON serialization/deserialization. */ @Test def void testGson() { @@ -112,9 +108,8 @@ class StatusReportHelperTest { // serialize with gson val json = StatusReportHelper.toJson(statusReport) - val statusReport2 = StatusReportHelper.fromJson(json); - Assertions.assertEquals(statusReport, statusReport2); - + val statusReport2 = StatusReportHelper.fromJson(json) + Assertions.assertEquals(statusReport.getSeverityLevel(), statusReport2.getSeverityLevel()) } /** @@ -136,46 +131,43 @@ class StatusReportHelperTest { */ @Test def void testStatusReportIsError() { - val okMessage = createStatusReport(Severity.OK, "Test", "source", 0) + val okMessage = createStatusReport(Severity.OK, "Test", null) Assertions.assertFalse(okMessage.isError(), "OK should not be an error") - val errorMessage = createStatusReport(Severity.ERROR, "Test", "source", 0) + val errorMessage = createStatusReport(Severity.ERROR, "Test", null) Assertions.assertTrue(errorMessage.isError(), "ERROR should be an error") - val cancelMessage = createStatusReport(Severity.CANCEL, "Test", "source", 0) + val cancelMessage = createStatusReport(Severity.CANCEL, "Test", null) Assertions.assertTrue(cancelMessage.isError(), "CANCEL should be an error") } /** - * Test that StatusReport is a record with serializable fields. - * Expected: all fields should be accessible and non-null where applicable. + * Test that StatusReport fields are accessible and correct. */ @Test def void testStatusReportFields() { - val message = createStatusReport( + var locations = newArrayList(new Location("test.source", 1, 2, 3, 4, "5")) + var statusReport = new StatusReport( + null, Severity.INFO, "Test message", - "test.source", 42, "Some details about the validation", - 1, - 2, - 3, - 4, - "5" + locations, + null, + null ) - Assertions.assertEquals(Severity.INFO, message.severity()) - Assertions.assertEquals("Test message", message.message()) - Assertions.assertEquals("test.source", message.source()) - Assertions.assertEquals(1, message.startLine()) - Assertions.assertEquals(2, message.endLine()) - Assertions.assertEquals(3, message.offset()) - Assertions.assertEquals(4, message.length()) - Assertions.assertEquals("5", message.text()) - Assertions.assertEquals(42, message.code()) - Assertions.assertEquals("Some details about the validation", message.details()) - Assertions.assertNull(message.children()) + Assertions.assertEquals(Severity.INFO, statusReport.getSeverityLevel()) + Assertions.assertTrue(statusReport.getLocations().isPresent()) + var loc = statusReport.getLocations().get().get(0) + Assertions.assertEquals(1, loc.startLine().intValue()) + Assertions.assertEquals(2, loc.endLine().intValue()) + Assertions.assertEquals(3, loc.offset().intValue()) + Assertions.assertEquals(4, loc.length().intValue()) + Assertions.assertEquals("5", loc.text()) + Assertions.assertTrue(statusReport.getDetails().isPresent()) + Assertions.assertEquals("Some details about the validation", statusReport.getDetails().get()) } /** @@ -185,15 +177,15 @@ class StatusReportHelperTest { @Test def void testSeverityElevationFromChildren() { // Create child messages with different severities - val child1 = createStatusReport(Severity.WARNING, "Warning message", "source1", 1) - val child2 = createStatusReport(Severity.ERROR, "Error message", "source2", 2) - val child3 = createStatusReport(Severity.INFO, "Info message", "source3", 3) + val child1 = createStatusReport(Severity.WARNING, "Warning message", null) + val child2 = createStatusReport(Severity.ERROR, "Error message", null) + val child3 = createStatusReport(Severity.INFO, "Info message", null) // Create parent with OK severity but ERROR children - val parent = createStatusReport(Severity.OK, #[child1, child2, child3]) + val parent = new StatusReport(null, Severity.OK, "test", null, null, null, #[child1, child2, child3], null) // Verify severity was elevated to ERROR (the highest in children) - Assertions.assertEquals(Severity.ERROR, parent.severity(), "Parent severity should be elevated to ERROR") + Assertions.assertEquals(Severity.ERROR, parent.getSeverityLevel(), "Parent severity should be elevated to ERROR") Assertions.assertTrue(parent.isError(), "Parent should be an error") } @@ -204,13 +196,13 @@ class StatusReportHelperTest { @Test def void testSeverityElevationToCancel() { // Create child with CANCEL severity - val child = createStatusReport(Severity.CANCEL, "Cancelled", "source", 1) + val child = createStatusReport(Severity.CANCEL, "Cancelled", null) // Create parent with INFO severity - val parent = createStatusReport(Severity.INFO, #[child]) + val parent = new StatusReport(null, Severity.INFO, "test", null, null, null, #[child], null) // Verify severity was elevated to CANCEL (the highest possible) - Assertions.assertEquals(Severity.CANCEL, parent.severity(), "Parent severity should be elevated to CANCEL") + Assertions.assertEquals(Severity.CANCEL, parent.getSeverityLevel(), "Parent severity should be elevated to CANCEL") Assertions.assertTrue(parent.isError(), "Parent should be an error (CANCEL is error)") } @@ -221,14 +213,14 @@ class StatusReportHelperTest { @Test def void testSeverityRemainsWhenHigherThanChildren() { // Create children with WARNING severity - val child1 = createStatusReport(Severity.WARNING, "Warning 1", "source1", 1) - val child2 = createStatusReport(Severity.WARNING, "Warning 2", "source2", 2) + val child1 = createStatusReport(Severity.WARNING, "Warning 1", null) + val child2 = createStatusReport(Severity.WARNING, "Warning 2", null) // Create parent with ERROR severity (higher than children) - val parent = createStatusReport(Severity.ERROR, #[child1, child2]) + val parent = new StatusReport(null, Severity.ERROR, "test", null, null, null, #[child1, child2], null) // Verify severity remains ERROR - Assertions.assertEquals(Severity.ERROR, parent.severity(), "Parent severity should remain ERROR") + Assertions.assertEquals(Severity.ERROR, parent.getSeverityLevel(), "Parent severity should remain ERROR") } /** @@ -238,58 +230,25 @@ class StatusReportHelperTest { @Test def void testSeverityWithNullOrEmptyChildren() { // Test with null children - val parentWithNull = createStatusReport(Severity.INFO, null) - Assertions.assertEquals(Severity.INFO, parentWithNull.severity(), + val parentWithNull = new StatusReport(null, Severity.INFO, "test", null, null, null, null, null) + Assertions.assertEquals(Severity.INFO, parentWithNull.getSeverityLevel(), "Severity should remain INFO with null children") // Test with empty children - val parentWithEmpty = createStatusReport(Severity.WARNING, #[]) - Assertions.assertEquals(Severity.WARNING, parentWithEmpty.severity(), + val parentWithEmpty = new StatusReport(null, Severity.WARNING, "test", null, null, null, #[], null) + Assertions.assertEquals(Severity.WARNING, parentWithEmpty.getSeverityLevel(), "Severity should remain WARNING with empty children") } /** - * Factory method to create a StatusReport with only the needed arguments. - * Defaults for location fields: startLine=0, endLine=0, offset=0, length=0, text="" + * Factory method to create a StatusReport with simple parameters. */ private def static StatusReport createStatusReport( Severity severity, String message, - String source, - int code - ) { - return new StatusReport(severity, message, source, code, null, null, null, null, null, null, null) - } - - /** - * Factory method with location parameters - */ - private def static StatusReport createStatusReport( - Severity severity, - String message, - String source, - int code, - String details, - int startLine, - int endLine, - int offset, - int length, - String text - ) { - return new StatusReport(severity, message, source, code, details, startLine, endLine, offset, length, - text, null) - } - - /** - * Factory method with only severity and children. - * All other fields use defaults: message="", source="", code=0, details=null, - * startLine=0, endLine=0, offset=0, length=0, text="" - */ - private def static StatusReport createStatusReport( - Severity severity, List children ) { - return new StatusReport(severity, "", "", 0, null, 0, 0, 0, 0, "", children) + return new StatusReport(null, severity, message, null, null, null, children, null) } -} +} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF b/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF index 2b55ebb..ec46ae1 100644 --- a/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF +++ b/bundles/nl.esi.xtext.common.lang/META-INF/MANIFEST.MF @@ -15,6 +15,7 @@ Require-Bundle: org.eclipse.xtext, org.eclipse.emf.common, org.antlr.runtime;bundle-version="[3.2.0,3.2.1)", org.eclipse.core.resources, + org.eclipse.core.runtime;visibility:=reexport, com.google.gson;bundle-version="[2.8.0,3.0.0)" Bundle-RequiredExecutionEnvironment: JavaSE-21 Export-Package: nl.esi.xtext.common.lang, diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java index f6c1b64..f257e21 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/IStatusReporting.java @@ -9,6 +9,8 @@ */ package nl.esi.xtext.common.lang.reporting; +import org.eclipse.core.runtime.IStatus; + /** * An interface for reporting status messages, allowing implementations to * handle the reporting of validation messages. @@ -19,5 +21,12 @@ public interface IStatusReporting { * * @param report the status report to be reported */ - void addReport(StatusReport report); + void addReport(IStatus report); + /** + * Reports an exception message. + * + * @param report the status report to be reported + */ + void addReport(Exception report); + } \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java new file mode 100644 index 0000000..582f9c3 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +/** + * A record representing a location in source code or text. + * Captures details such as line numbers, character offset, length, and text content. + */ +public record Location( + String source, + Integer startLine, + Integer endLine, + Integer offset, + Integer length, + String text +) { + // Record with no additional implementation needed +} diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java index 4f9f9f3..e17dc18 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java @@ -10,78 +10,255 @@ package nl.esi.xtext.common.lang.reporting; import java.util.List; +import java.util.Optional; -import org.eclipse.emf.common.util.Diagnostic; +import org.eclipse.core.runtime.IStatus; /** - * A record that represents the outcome of some processing activity, - * mimicking the {@link Diagnostic} interface and serializable with Gson. + * A class for status reporting that represents the outcome of some processing + * activity, implementing {@link IStatus} and serializable with Gson. */ -public record StatusReport( - Severity severity, - String message, - String source, - Integer code, - String details, - Integer startLine, - Integer endLine, - Integer offset, - Integer length, - String text, - List children -) { - /** - * Creates a new ValidationMessage with all fields. - * - * The severity field will be automatically elevated to the highest severity found in the children list, - * ensuring that the parent always reflects the most severe condition among its children. - * - * @param severity the initial severity level (will be elevated if children have higher severity) - * @param message the message describing the situation - * @param source the source identifier - * @param code the source-specific identity code - * @param details the details string (representing low-level exception information) - * @param startLine the starting line number of the location - * @param endLine the ending line number of the location - * @param offset the character offset in the source - * @param length the length of the located text - * @param text the located text content - * @param children the list of child validation messages (severity will be elevated based on these) - */ - public StatusReport { - // Calculate the highest severity from children - if (children != null && children.isEmpty()) { - // Set children to null if empty - children = null; - } - if (children != null) { - Severity maxSeverity = severity; - for (StatusReport child : children) { - if (child != null && child.severity.value > maxSeverity.value) { - maxSeverity = child.severity; - } - } - // Re-assign severity to the highest found - severity = maxSeverity; - } - } - - /** - * Returns a string representation of the severity level. - * - * @return a string describing the severity - */ - public String getSeverityString() { - return severity.name(); - } - - /** - * Checks if this report indicates an error condition. - * - * @return true if severity is ERROR or CANCEL, false otherwise - */ - public boolean isError() { - return severity.isError(); - } +public final class StatusReport implements IStatus { + + private String pluginId; + private final int code; + private final String message; + private final Severity severity; + private final String details; + private final List locations; // Supports multiple sources for one status message + private final List children; + private transient final Exception exception; // covered by details, not serialized + + /** + * Creates a new StatusReport with all fields including exception. + * + * The severity field will be automatically elevated to the highest severity + * found in the children list, ensuring that the parent always reflects the most + * severe condition among its children. + * + * @param pluginId the plugin identifier for this status + * @param severity the initial severity level (will be elevated if children + * have higher severity) + * @param message the message describing the situation + * @param code the source-specific identity code + * @param details the details string (representing low-level exception + * information) + * @param locations the list of location information (source, line numbers, + * offsets, text). Explicit multiplicity because one status can + * visible of multiple sources + * @param children the list of child validation messages (severity will be + * elevated based on these) + * @param exception the exception associated with this status (transient, not + * serialized) + */ + public StatusReport(String pluginId, Severity severity, String message, Integer code, String details, + List locations, List children, Exception exception) { + this.pluginId = pluginId; + this.code = code != null ? code : 0; + this.message = message; + this.exception = exception; + + // If details is null but exception is not null, generate stack trace string + var effectiveDetails = details == null && exception != null ? getStackTraceAsString(exception) : details; + + // Calculate the highest severity from children using streams + var effectiveSeverity = children != null + ? children.stream().filter(child -> child != null).map(child -> child.severity) + .filter(s -> s.value > severity.value).max((s1, s2) -> Integer.compare(s1.value, s2.value)) + .orElse(severity) + : severity; + + this.severity = effectiveSeverity; + this.details = effectiveDetails; + this.locations = (locations != null && !locations.isEmpty()) ? locations : null; + this.children = (children != null && !children.isEmpty()) ? children : null; + } + + // IStatus implementation methods + + @Override + public IStatus[] getChildren() { + return children != null ? children.toArray(new IStatus[0]) : new IStatus[0]; + } + + @Override + public int getCode() { + return code; + } + + @Override + public Throwable getException() { + return exception; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public String getPlugin() { + return pluginId; + } + + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + @Override + public int getSeverity() { + return mapSeverityToEclipse(severity); + } + + @Override + public boolean isMultiStatus() { + return children != null && !children.isEmpty(); + } + + @Override + public boolean isOK() { + return severity == Severity.OK; + } + + @Override + public boolean matches(int severityMask) { + return (getSeverity() & severityMask) != 0; + } + + // Custom getter methods + + /** + * Gets the severity level of this report. + * + * @return the severity level + */ + public Severity getSeverityLevel() { + return severity; + } + + /** + * Gets the list of location information for this status report. + * + * A single status message can refer to multiple sources, which is why this + * returns a list of locations. Each location contains source information such as + * file paths, line numbers, and offsets. + * + * @return an Optional containing the list of locations, or empty if no + * locations + */ + public Optional> getLocations() { + return Optional.ofNullable(locations); + } + + /** + * Gets the details string (low-level exception information). + * + * @return an Optional containing the details, or empty if no details + */ + public Optional getDetails() { + return Optional.ofNullable(details); + } + + /** + * Gets the list of child status reports. + * + * @return an Optional containing the children list, or empty if no children + */ + public Optional> getChildReports() { + return Optional.ofNullable(children); + } + + /** + * Gets the plugin identifier for this status. + * + * @return an Optional containing the plugin ID, or empty if not set + */ + public Optional getPluginId() { + return Optional.ofNullable(pluginId); + } + + /** + * Gets the exception associated with this status. This field is transient and + * will not be serialized. + * + * @return an Optional containing the exception, or empty if none + */ + public Optional getExceptionOptional() { + return Optional.ofNullable(exception); + } + + /** + * Returns a string representation of the severity level. + * + * @return a string describing the severity + */ + public String getSeverityString() { + return severity.name(); + } + + /** + * Checks if this report indicates an error condition. + * + * @return true if severity is ERROR or CANCEL, false otherwise + */ + public boolean isError() { + return severity.isError(); + } + + /** + * Converts an exception's stack trace to a string representation. Limits the + * stack trace to 15 elements using Java 21+ features. + * + * @param exception the exception to convert + * @return a string representation of the stack trace + */ + private static String getStackTraceAsString(Exception exception) { + if (exception == null) { + return null; + } + + var sb = new StringBuilder(); + sb.append(exception.getClass().getName()); + + // Use Optional for null-safe message appending + Optional.ofNullable(exception.getMessage()).ifPresentOrElse(msg -> sb.append(": ").append(msg), () -> { + }); + + sb.append("\n"); + + // Use array stream for stack trace processing + var stackTrace = exception.getStackTrace(); + var limit = Math.min(stackTrace.length, 15); + + for (int i = 0; i < limit; i++) { + sb.append("\tat ").append(stackTrace[i]).append("\n"); + } + + if (stackTrace.length > 15) { + sb.append("\t... ").append(stackTrace.length - 15).append(" more\n"); + } + + return sb.toString(); + } + + /** + * Maps a Severity enum value to Eclipse Status severity constants using pattern + * matching. + */ + private static int mapSeverityToEclipse(Severity severity) { + return severity == null ? IStatus.OK : switch (severity) { + case OK -> IStatus.OK; + case INFO -> IStatus.INFO; + case WARNING -> IStatus.WARNING; + case ERROR -> IStatus.ERROR; + case CANCEL -> IStatus.CANCEL; + }; + } + + @Override + public String toString() { + return "StatusReport [pluginId=" + pluginId + ", code=" + code + ", message=" + message + ", severity=" + + severity + ", details=" + details + ", locations=" + locations + ", children=" + children + "]"; + } } \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportCollector.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportCollector.java new file mode 100644 index 0000000..ec07b74 --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportCollector.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.core.runtime.IStatus; + +import com.google.inject.Singleton; + +/** + * An general class for reporting status messages, allowing implementations to + * handle the reporting of status messages. + */ +@Singleton +public class StatusReportCollector implements IStatusReporting { + private final List reports = Collections.synchronizedList(new ArrayList<>()); + /** + * Reports a status message. + * Supports reporting of StatusReport, IStatus, and Exception objects. Other object types will be logged as unsupported. + * + * @param object the status report to be reported + */ + @Override + public void addReport(IStatus report) { + if (report == null) { + return; + } + if( report instanceof StatusReport statusReport) { + reports.add(statusReport); + } else if( report instanceof IStatus) { + reports.add(StatusReportHelper.fromIStatus(report)); + } + } + + @Override + public void addReport(Exception report) { + addReport(StatusReportHelper.fromException(report, null, null)); + } + + public List getReports() { + return Collections.unmodifiableList(reports); + } +} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java index 1f85318..0119c0f 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java @@ -12,9 +12,9 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; +import org.eclipse.core.runtime.IStatus; import org.eclipse.emf.common.util.BasicDiagnostic; import org.eclipse.emf.common.util.Diagnostic; import org.eclipse.emf.ecore.EObject; @@ -44,9 +44,9 @@ public class StatusReportHelper { * @see {@link EcoreUtil3#validate(EObject)} */ public static StatusReport validate(EObject eObject) { - Diagnostician diagnostician = new Diagnostician(); - BasicDiagnostic diagnostics = diagnostician.createDefaultDiagnostic(eObject); - Map context = diagnostician.createDefaultContext(); + var diagnostician = new Diagnostician(); + var diagnostics = diagnostician.createDefaultDiagnostic(eObject); + var context = diagnostician.createDefaultContext(); diagnostician.validate(eObject, diagnostics, context); return fromDiagnostic(diagnostics); @@ -57,9 +57,7 @@ public static StatusReport validate(EObject eObject) { */ public static void validate(List reports, EObject object) { var report = validate(object); - if(report.severity() != Severity.OK) { - reports.add(report); - } + reports.add(report); } /** @@ -67,10 +65,10 @@ public static void validate(List reports, EObject object) { * @see {@link EcoreUtil3#validate(EObject)} */ public static StatusReport validate(Resource resource) { - Diagnostician diagnostician = new Diagnostician(); - BasicDiagnostic diagnostics = new BasicDiagnostic(EObjectValidator.DIAGNOSTIC_SOURCE, 0, + var diagnostician = new Diagnostician(); + var diagnostics = new BasicDiagnostic(EObjectValidator.DIAGNOSTIC_SOURCE, 0, "Diagnosis of " + resource.getURI(), new Object[] { resource }); - Map context = diagnostician.createDefaultContext(); + var context = diagnostician.createDefaultContext(); for (EObject eObject : resource.getContents()) { diagnostician.validate(eObject, diagnostics, context); @@ -86,10 +84,10 @@ public static StatusReport validate(Resource resource) { */ public static boolean validate(List reports, Resource resource) { var report = validate(resource); - if(report.severity() != Severity.OK) { + if(report.getSeverityLevel() != Severity.OK) { reports.add(report); } - return reports.stream().anyMatch(r->r.severity() == Severity.ERROR); + return reports.stream().anyMatch(r->r.getSeverityLevel() == Severity.ERROR); } /** @@ -113,45 +111,23 @@ public static StatusReport fromJson(String jsonString) { * Mapping: * - severity -> severity * - message -> message - * - source -> source * - code -> code * - exception -> details (or null if none) * - children -> children (recursively converted) - * - location -> startLine, endLine, offset, length, text (extracted from diagnostic data if available) + * - location -> locations list (extracted from diagnostic data if available) * * @param diagnostic the diagnostic to convert - * @return a new ValidationMessage based on the diagnostic + * @return a new StatusReport based on the diagnostic */ public static StatusReport fromDiagnostic(Diagnostic diagnostic) { if (diagnostic == null) { return null; } - String details = null; - Throwable exception = diagnostic.getException(); - if (exception != null) { - StringBuilder sb = new StringBuilder(); - sb.append(exception.getClass().getName()); - if (exception.getMessage() != null) { - sb.append(": ").append(exception.getMessage()); - } - sb.append("\n"); - - // Append stack trace (limited to 15 elements) - StackTraceElement[] stackTrace = exception.getStackTrace(); - int limit = Math.min(stackTrace.length, 15); - for (int i = 0; i < limit; i++) { - sb.append("\tat ").append(stackTrace[i]).append("\n"); - } - if (stackTrace.length > 15) { - sb.append("\t... ").append(stackTrace.length - 15).append(" more\n"); - } - - details = sb.toString(); - } + var exception = diagnostic.getException(); List children = null; - List diagnosticChildren = diagnostic.getChildren(); + var diagnosticChildren = diagnostic.getChildren(); if (diagnosticChildren != null && !diagnosticChildren.isEmpty()) { children = diagnosticChildren.stream() .map(StatusReportHelper::fromDiagnostic) @@ -160,19 +136,75 @@ public static StatusReport fromDiagnostic(Diagnostic diagnostic) { var source = extractSource(diagnostic); var node = extractLocationData(diagnostic); + + List locations = null; + if (node != null || source != null) { + locations = new ArrayList<>(); + locations.add(new Location( + source, + node != null ? node.getStartLine() : null, + node != null ? node.getEndLine() : null, + node != null ? node.getOffset() : null, + node != null ? node.getLength() : null, + node != null ? node.getText() : null + )); + } return new StatusReport( + "unknown", // pluginId - unknown from Diagnostic Severity.fromValue(diagnostic.getSeverity()), diagnostic.getMessage(), - source, diagnostic.getCode() == 0 ? null : diagnostic.getCode(), - details, - node!= null ? node.getStartLine(): null, - node!= null ? node.getEndLine(): null, - node!= null ? node.getOffset(): null, - node!= null ? node.getLength(): null, - node!= null ? node.getText(): null , - children + null, + locations, + children, + (Exception) exception + ); + } + + /** + * Converts an IStatus to a StatusReport. + * If the IStatus is already an instance of StatusReport, it is returned as-is. + * Otherwise, a new StatusReport is created from the IStatus data. + * + * @param status the IStatus to convert + * @return a StatusReport based on the IStatus, or null if status is null + */ + public static StatusReport fromIStatus(IStatus status) { + if (status == null) { + return null; + } + + // If already a StatusReport, return it directly + if (status instanceof StatusReport statusReport) { + return statusReport; + } + + // Convert IStatus to StatusReport + List children = null; + var statusChildren = status.getChildren(); + if (statusChildren != null && statusChildren.length > 0) { + children = new ArrayList<>(); + for (var child : statusChildren) { + var childReport = fromIStatus(child); + if (childReport != null) { + children.add(childReport); + } + } + if (children.isEmpty()) { + children = null; + } + } + + return new StatusReport( + status.getPlugin(), // pluginId + Severity.fromValue(status.getSeverity()), + status.getMessage(), + status.getCode() == 0 ? null : status.getCode(), + null, // details - not available from IStatus + null, // locations - not available from IStatus + children, + (Exception) status.getException() ); } @@ -203,21 +235,19 @@ public static StatusReport infoReport(String errorMessage, List ch public static StatusReport okReport(String errorMessage, List children) { return fromMessage(Severity.OK, errorMessage, children); } + + private static StatusReport fromMessage(Severity severity, String errorMessage, List children) { - return new StatusReport( + null, // pluginId - unknown severity, errorMessage, - null, // source - unknown - null, // code - default + null, // code - default null, // details - none - null, // startLine - unknown - null, // endLine - unknown - null, // offset - unknown - null, // length - unknown - null, // text - unknown - children + null, // locations - unknown + children, + null // exception - none ); } @@ -240,10 +270,10 @@ private static String extractSource(Diagnostic diagnostic) { if (firstData instanceof EObject eObject) { Resource resource = eObject.eResource(); if (resource != null) { - return resource.getURI().toString(); + return resource.getURI().path(); } } else if (firstData instanceof Resource resource) { - return resource.getURI().toString(); + return resource.getURI().path(); } } @@ -294,55 +324,35 @@ private static StatusReport fromException(Throwable exception, String userMessag seenExceptions.add(exception); - // Build details string with class name and message - StringBuilder details = new StringBuilder(); - details.append(exception.getClass().getName()); - if (exception.getMessage() != null) { - details.append(": ").append(exception.getMessage()); - } - details.append("\n"); - - // Append stack trace (limited to 15 elements) - StackTraceElement[] stackTrace = exception.getStackTrace(); - int limit = Math.min(stackTrace.length, 15); - for (int i = 0; i < limit; i++) { - details.append("\tat ").append(stackTrace[i]).append("\n"); - } - if (stackTrace.length > 15) { - details.append("\t... ").append(stackTrace.length - 15).append(" more\n"); - } - // Process cause as child (if exists) - List allChildren = new ArrayList(); + var allChildren = new ArrayList(); if (children != null) { allChildren.addAll(children); } - Throwable cause = exception.getCause(); + var cause = exception.getCause(); if (cause != null) { - StatusReport childReport = fromException(cause, null, null, seenExceptions); + var childReport = fromException(cause, null, null, seenExceptions); if (childReport != null) { allChildren.add(childReport); } } // Create StatusReport with defaults for unknown fields - StringBuffer message = new StringBuffer(); + // Details will be filled automatically from exception if not provided + var message = new StringBuffer(); if (userMessage != null && !userMessage.isEmpty()) { message.append(userMessage).append("\n"); } message.append(exception.getLocalizedMessage() != null ? exception.getLocalizedMessage() : exception.getClass().getSimpleName()); return new StatusReport( + null, // pluginId - unknown Severity.ERROR, message.toString(), - null, // source - unknown null, // code - default - details.toString(), - null, // startLine - unknown - null, // endLine - unknown - null, // offset - unknown - null, // length - unknown - null, // text - unknown - allChildren + null, // details - will be filled from exception automatically + null, // locations - unknown + allChildren, + (Exception) exception // pass the exception to StatusReport ); } diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java deleted file mode 100644 index 5079199..0000000 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReporting.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2024, 2026 TNO-ESI - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package nl.esi.xtext.common.lang.reporting; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * An interface for reporting status messages, allowing implementations to - * handle the reporting of validation messages. - */ -public class StatusReporting implements IStatusReporting { - List reports = Collections.synchronizedList(new ArrayList<>()); - /** - * Reports a status message. - * - * @param report the status report to be reported - */ - @Override - public void addReport(StatusReport report) { - reports.add(report); - } - - public List getReports() { - return Collections.unmodifiableList(reports); - } -} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingEclipseRuntime.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingEclipseRuntime.java new file mode 100644 index 0000000..f42578e --- /dev/null +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportingEclipseRuntime.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.common.lang.reporting; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Plugin; +import org.eclipse.emf.common.util.Logger; + +import com.google.inject.Inject; + +/** + * An interface for reporting status messages, allowing implementations to + * handle the reporting of validation messages. + */ +public class StatusReportingEclipseRuntime implements IStatusReporting { + + private final Logger logger; + private final String pluginId; + + private boolean skipOkStatus = true; + + @Inject + public StatusReportingEclipseRuntime(Plugin plugin) { + if( plugin instanceof Logger logger) { + this.logger = logger; + this.pluginId = plugin.getBundle().getSymbolicName(); + } + else { + throw new IllegalArgumentException("Plugin must implement Logger interface"); + } + } + /** + * Reports a status message. + * + * @param report the status report to be reported + */ + @Override + public void addReport(IStatus report) { + if (skipOkStatus && report.getSeverity() == IStatus.OK) { + return; // Skip reporting OK status messages + } + if(report instanceof StatusReport statusReport) { + // If the report is a StatusReport, set the plugin ID + statusReport.setPluginId(pluginId); + } + // Using the standard eclipse logger to log the status + this.logger.log(report); + } + + /** + * Reports an exception as a status message. + * + * @param exception the status report to be reported + */ + @Override + public void addReport(Exception exception) { + this.logger.log(exception); + } + +} \ No newline at end of file diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend index 07c5402..e1aa22a 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/CmdLineContext.xtend @@ -11,9 +11,8 @@ package nl.esi.xtext.types.generator import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator -import nl.esi.xtext.common.lang.reporting.StatusReporting -class CmdLineContext extends StatusReporting implements IGeneratorContext { +class CmdLineContext implements IGeneratorContext { final static String context = "CMD_LINE" diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend index eaf28ce..b560321 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/SpecDiffContext.xtend @@ -11,9 +11,8 @@ package nl.esi.xtext.types.generator import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator -import nl.esi.xtext.common.lang.reporting.StatusReporting -class SpecDiffContext extends StatusReporting implements IGeneratorContext { +class SpecDiffContext implements IGeneratorContext { public String oriFeaturePath public String updFeaturePath diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend index 68d2d3c..80c5b38 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/StepsParserContext.xtend @@ -9,12 +9,11 @@ */ package nl.esi.xtext.types.generator -import java.util.List import org.eclipse.xtext.generator.IGeneratorContext import org.eclipse.xtext.util.CancelIndicator -import nl.esi.xtext.common.lang.reporting.StatusReporting +import java.util.List -class StepsParserContext extends StatusReporting implements IGeneratorContext { +class StepsParserContext implements IGeneratorContext { public String stepsFilePath public List testContext diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend index cc73d4f..4089cdf 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/TestSpecContext.xtend @@ -9,11 +9,10 @@ */ package nl.esi.xtext.types.generator -import java.nio.file.Path import org.eclipse.xtext.generator.IGeneratorContext -import nl.esi.xtext.common.lang.reporting.StatusReporting +import java.nio.file.Path -class TestSpecContext extends StatusReporting implements IGeneratorContext { +class TestSpecContext implements IGeneratorContext { public Path tspecPath diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend index caf67b1..bcfc31b 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/generator/XPlusMain.xtend @@ -18,11 +18,12 @@ import java.nio.file.LinkOption import java.nio.file.Path import java.nio.file.Paths import java.text.MessageFormat -import java.util.ArrayList import java.util.Comparator import java.util.List +import nl.esi.xtext.common.lang.reporting.IStatusReporting import nl.esi.xtext.common.lang.reporting.Severity import nl.esi.xtext.common.lang.reporting.StatusReport +import nl.esi.xtext.common.lang.reporting.StatusReportCollector import nl.esi.xtext.common.lang.reporting.StatusReportHelper import org.apache.commons.cli.CommandLine import org.apache.commons.cli.DefaultParser @@ -139,6 +140,8 @@ class XPlusMain { @Inject GeneratorDelegate generator @Inject JavaIoFileSystemAccess fileAccess + + @Inject IStatusReporting statusReporting String[] args String description @@ -302,17 +305,15 @@ class XPlusMain { val outputdir = getOutputdir(cmdLine, locationPath, OPT_OUTPUT) System.out.println(INFO_OUTPUT + outputdir) cleanOutput(cmdLine, outputdir) - var statusReports = new ArrayList() as List if (Files.isDirectory(locationPath, LinkOption.NOFOLLOW_LINKS)) { println(MessageFormat.format(INFO_SEARCHING, language, ext, locationPath.toString)) val dir = new File(locationPath.toString) val prjFiles = dir.listFiles(createFileFilter(ext)); for (file : prjFiles) { - val reports = runGeneration(file.absolutePath, outputdir.toString, validation) - statusReports.addAll(reports) - if( reports.exists[severity == Severity.ERROR]) { - exit(StatusReportHelper.errorReport(INFO_STOP, statusReports), null) + runGeneration(file.absolutePath, outputdir.toString, validation) + if( reports.exists[getSeverityLevel == Severity.ERROR]) { + exit(StatusReportHelper.errorReport(INFO_STOP, getReports()), null) } } if (prjFiles.empty) { @@ -320,16 +321,15 @@ class XPlusMain { } } else { - val reports = runGeneration(locationPath.toString, outputdir.toString, validation) - statusReports.addAll(reports) + runGeneration(locationPath.toString, outputdir.toString, validation) if( reports.exists[severity == Severity.ERROR]) { - exit(StatusReportHelper.errorReport(INFO_STOP, statusReports), null) + exit(StatusReportHelper.errorReport(INFO_STOP, getReports()), null) } } System.out.println(INFO_GENERATION_FINISHED) System.out.println("") System.out.println(INFO_XPLUS_FINISHED) - exit(StatusReportHelper.okReport(INFO_GENERATION_FINISHED, statusReports), null) + exit(StatusReportHelper.okReport(INFO_GENERATION_FINISHED, getReports()), null) } def Options createOptions() { @@ -487,7 +487,7 @@ class XPlusMain { generator.generate(resource, fileAccess, cliContext) } - def List runGeneration(String string, String outputdir, Boolean validation) { + def void runGeneration(String string, String outputdir, Boolean validation) { // Load the resource System.out.println(INFO_READING + string) @@ -498,10 +498,10 @@ class XPlusMain { // Validate the resource if (validation) { val report = StatusReportHelper.validate(resource) - context.addReport(report) + statusReporting.addReport(report) if (report.error) { // stop when error else continue - return context.reports + return } } // Configure and start the generator @@ -513,10 +513,10 @@ class XPlusMain { } catch (Exception e) { //create a status report val report = StatusReportHelper.fromException(e, INFO_STOP, null); - context.addReport(report) + statusReporting.addReport(report) } - return context.reports + return } def cleanOutput(CommandLine cmdLine, Path outputdir) { @@ -562,19 +562,19 @@ class XPlusMain { } private def void abort(String message, Options options) { - exit(StatusReportHelper.errorReport(message,null), options) + exit(StatusReportHelper.errorReport(message,getReports()), options) } private def void exit(StatusReport statusReport, Options options) { //be backwards compatible - System.err.println(statusReport.message()) + System.err.println(statusReport.getMessage()) saveReport(statusReport) if(options !== null) { showInfo(description, options) } - System.out.println(String.format("Exiting with status %s(%d)", statusReport.severity().name, statusReport.severity().value)) - System.err.println(String.format("Exiting with status %s(%d)", statusReport.severity().name, statusReport.severity().value)) - System.exit(statusReport.severity().value) + System.out.println(String.format("Exiting with status %s(%d)", statusReport.getSeverityLevel().name, statusReport.getSeverityLevel().value)) + System.err.println(String.format("Exiting with status %s(%d)", statusReport.getSeverityLevel().name, statusReport.getSeverityLevel().value)) + System.exit(statusReport.getSeverityLevel().value) } private def void saveReport(StatusReport report) { @@ -583,4 +583,11 @@ class XPlusMain { val charSequence = StatusReportHelper.toJson(report) fileAccess.generateFile(resource, charSequence); } + + private def List getReports(){ + if( statusReporting instanceof StatusReportCollector) { + return statusReporting.reports + } + throw new IllegalArgumentException("Excepting an instance of StatusReportCollector") + } } \ No newline at end of file From 74c9d76136bdf8574bdeceda3c905e9823e2deb2 Mon Sep 17 00:00:00 2001 From: Paul Nelissen Date: Mon, 27 Jul 2026 10:29:24 +0200 Subject: [PATCH 5/6] Move source to report --- .../reporting/StatusReportHelperTest.xtend | 21 ++++++------ .../xtext/common/lang/reporting/Location.java | 1 - .../common/lang/reporting/StatusReport.java | 34 +++++++++++-------- .../lang/reporting/StatusReportHelper.java | 14 ++++---- 4 files changed, 39 insertions(+), 31 deletions(-) diff --git a/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend b/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend index bcb69e9..40f8822 100644 --- a/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend +++ b/bundles/nl.esi.xtext.common.lang.tests/src/nl/esi/xtext/common/lang/tests/reporting/StatusReportHelperTest.xtend @@ -146,21 +146,22 @@ class StatusReportHelperTest { */ @Test def void testStatusReportFields() { - var locations = newArrayList(new Location("test.source", 1, 2, 3, 4, "5")) + var location = new Location(1, 2, 3, 4, "5") var statusReport = new StatusReport( null, Severity.INFO, "Test message", + "test.source", 42, "Some details about the validation", - locations, + location, null, null ) Assertions.assertEquals(Severity.INFO, statusReport.getSeverityLevel()) - Assertions.assertTrue(statusReport.getLocations().isPresent()) - var loc = statusReport.getLocations().get().get(0) + Assertions.assertTrue(statusReport.getLocation().isPresent()) + var loc = statusReport.getLocation().get() Assertions.assertEquals(1, loc.startLine().intValue()) Assertions.assertEquals(2, loc.endLine().intValue()) Assertions.assertEquals(3, loc.offset().intValue()) @@ -182,7 +183,7 @@ class StatusReportHelperTest { val child3 = createStatusReport(Severity.INFO, "Info message", null) // Create parent with OK severity but ERROR children - val parent = new StatusReport(null, Severity.OK, "test", null, null, null, #[child1, child2, child3], null) + val parent = new StatusReport(null, Severity.OK, "test", null, null, null, null, #[child1, child2, child3], null) // Verify severity was elevated to ERROR (the highest in children) Assertions.assertEquals(Severity.ERROR, parent.getSeverityLevel(), "Parent severity should be elevated to ERROR") @@ -199,7 +200,7 @@ class StatusReportHelperTest { val child = createStatusReport(Severity.CANCEL, "Cancelled", null) // Create parent with INFO severity - val parent = new StatusReport(null, Severity.INFO, "test", null, null, null, #[child], null) + val parent = new StatusReport(null, Severity.INFO, "test", null, null, null, null, #[child], null) // Verify severity was elevated to CANCEL (the highest possible) Assertions.assertEquals(Severity.CANCEL, parent.getSeverityLevel(), "Parent severity should be elevated to CANCEL") @@ -217,7 +218,7 @@ class StatusReportHelperTest { val child2 = createStatusReport(Severity.WARNING, "Warning 2", null) // Create parent with ERROR severity (higher than children) - val parent = new StatusReport(null, Severity.ERROR, "test", null, null, null, #[child1, child2], null) + val parent = new StatusReport(null, Severity.ERROR, "test", null, null, null, null, #[child1, child2], null) // Verify severity remains ERROR Assertions.assertEquals(Severity.ERROR, parent.getSeverityLevel(), "Parent severity should remain ERROR") @@ -230,12 +231,12 @@ class StatusReportHelperTest { @Test def void testSeverityWithNullOrEmptyChildren() { // Test with null children - val parentWithNull = new StatusReport(null, Severity.INFO, "test", null, null, null, null, null) + val parentWithNull = new StatusReport(null, Severity.INFO, "test", null, null, null, null, null, null) Assertions.assertEquals(Severity.INFO, parentWithNull.getSeverityLevel(), "Severity should remain INFO with null children") // Test with empty children - val parentWithEmpty = new StatusReport(null, Severity.WARNING, "test", null, null, null, #[], null) + val parentWithEmpty = new StatusReport(null, Severity.WARNING, "test", null, null, null, null, #[], null) Assertions.assertEquals(Severity.WARNING, parentWithEmpty.getSeverityLevel(), "Severity should remain WARNING with empty children") } @@ -248,7 +249,7 @@ class StatusReportHelperTest { String message, List children ) { - return new StatusReport(null, severity, message, null, null, null, children, null) + return new StatusReport(null, severity, message, null, null, null, null, children, null) } } \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java index 582f9c3..622dc2e 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/Location.java @@ -14,7 +14,6 @@ * Captures details such as line numbers, character offset, length, and text content. */ public record Location( - String source, Integer startLine, Integer endLine, Integer offset, diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java index e17dc18..b070a29 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReport.java @@ -20,12 +20,13 @@ */ public final class StatusReport implements IStatus { - private String pluginId; + private String plugin; private final int code; private final String message; + private final String source; private final Severity severity; private final String details; - private final List locations; // Supports multiple sources for one status message + private final Location location; private final List children; private transient final Exception exception; // covered by details, not serialized @@ -36,7 +37,7 @@ public final class StatusReport implements IStatus { * found in the children list, ensuring that the parent always reflects the most * severe condition among its children. * - * @param pluginId the plugin identifier for this status + * @param plugin the plugin identifier for this status * @param severity the initial severity level (will be elevated if children * have higher severity) * @param message the message describing the situation @@ -51,12 +52,13 @@ public final class StatusReport implements IStatus { * @param exception the exception associated with this status (transient, not * serialized) */ - public StatusReport(String pluginId, Severity severity, String message, Integer code, String details, - List locations, List children, Exception exception) { - this.pluginId = pluginId; + public StatusReport(String plugin, Severity severity, String message, String source, Integer code, String details, + Location location, List children, Exception exception) { + this.plugin = plugin; this.code = code != null ? code : 0; this.message = message; this.exception = exception; + this.source = source; // If details is null but exception is not null, generate stack trace string var effectiveDetails = details == null && exception != null ? getStackTraceAsString(exception) : details; @@ -70,7 +72,7 @@ public StatusReport(String pluginId, Severity severity, String message, Integer this.severity = effectiveSeverity; this.details = effectiveDetails; - this.locations = (locations != null && !locations.isEmpty()) ? locations : null; + this.location = location; this.children = (children != null && !children.isEmpty()) ? children : null; } @@ -96,13 +98,17 @@ public String getMessage() { return message; } + public String getSource() { + return source; + } + @Override public String getPlugin() { - return pluginId; + return plugin; } public void setPluginId(String pluginId) { - this.pluginId = pluginId; + this.plugin = pluginId; } @Override @@ -146,8 +152,8 @@ public Severity getSeverityLevel() { * @return an Optional containing the list of locations, or empty if no * locations */ - public Optional> getLocations() { - return Optional.ofNullable(locations); + public Optional getLocation() { + return Optional.ofNullable(location); } /** @@ -174,7 +180,7 @@ public Optional> getChildReports() { * @return an Optional containing the plugin ID, or empty if not set */ public Optional getPluginId() { - return Optional.ofNullable(pluginId); + return Optional.ofNullable(plugin); } /** @@ -257,8 +263,8 @@ private static int mapSeverityToEclipse(Severity severity) { @Override public String toString() { - return "StatusReport [pluginId=" + pluginId + ", code=" + code + ", message=" + message + ", severity=" - + severity + ", details=" + details + ", locations=" + locations + ", children=" + children + "]"; + return "StatusReport [pluginId=" + plugin + ", code=" + code + ", message=" + message + ", severity=" + + severity + ", details=" + details + ", location=" + location + ", children=" + children + "]"; } } \ No newline at end of file diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java index 0119c0f..051d29b 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java @@ -137,26 +137,25 @@ public static StatusReport fromDiagnostic(Diagnostic diagnostic) { var source = extractSource(diagnostic); var node = extractLocationData(diagnostic); - List locations = null; + Location location = null; if (node != null || source != null) { - locations = new ArrayList<>(); - locations.add(new Location( - source, + location = new Location( node != null ? node.getStartLine() : null, node != null ? node.getEndLine() : null, node != null ? node.getOffset() : null, node != null ? node.getLength() : null, node != null ? node.getText() : null - )); + ); } return new StatusReport( "unknown", // pluginId - unknown from Diagnostic Severity.fromValue(diagnostic.getSeverity()), diagnostic.getMessage(), + source, diagnostic.getCode() == 0 ? null : diagnostic.getCode(), null, - locations, + location, children, (Exception) exception ); @@ -200,6 +199,7 @@ public static StatusReport fromIStatus(IStatus status) { status.getPlugin(), // pluginId Severity.fromValue(status.getSeverity()), status.getMessage(), + status.getPlugin(), status.getCode() == 0 ? null : status.getCode(), null, // details - not available from IStatus null, // locations - not available from IStatus @@ -243,6 +243,7 @@ private static StatusReport fromMessage(Severity severity, String errorMessage, null, // pluginId - unknown severity, errorMessage, + null, null, // code - default null, // details - none null, // locations - unknown @@ -348,6 +349,7 @@ private static StatusReport fromException(Throwable exception, String userMessag null, // pluginId - unknown Severity.ERROR, message.toString(), + null, // source - unknown null, // code - default null, // details - will be filled from exception automatically null, // locations - unknown From 469b41c17778fe28828db9625530139557ec815f Mon Sep 17 00:00:00 2001 From: Paul Nelissen Date: Tue, 28 Jul 2026 07:48:45 +0200 Subject: [PATCH 6/6] Location does not contain source --- .../nl/esi/xtext/common/lang/reporting/StatusReportHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java index 051d29b..604eb52 100644 --- a/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java +++ b/bundles/nl.esi.xtext.common.lang/src/nl/esi/xtext/common/lang/reporting/StatusReportHelper.java @@ -138,7 +138,7 @@ public static StatusReport fromDiagnostic(Diagnostic diagnostic) { var node = extractLocationData(diagnostic); Location location = null; - if (node != null || source != null) { + if (node != null) { location = new Location( node != null ? node.getStartLine() : null, node != null ? node.getEndLine() : null,