diff --git a/ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs b/ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs
new file mode 100644
index 0000000..1dbd433
--- /dev/null
+++ b/ECoreNetto.Tests/Resource/DiagnosticsTestFixture.cs
@@ -0,0 +1,145 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// Copyright 2017-2025 Starion Group S.A.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+namespace ECoreNetto.Tests.Resource
+{
+ using System;
+ using System.IO;
+ using System.Linq;
+
+ using ECoreNetto.Resource;
+
+ using NUnit.Framework;
+
+ ///
+ /// Suite of tests that verify the records a
+ /// in for malformed or unrecognized input before aborting the load (see issue #35).
+ ///
+ [TestFixture]
+ public class DiagnosticsTestFixture
+ {
+ private ResourceSet resourceSet = null!;
+
+ [SetUp]
+ public void SetUp()
+ {
+ this.resourceSet = new ResourceSet();
+ }
+
+ [Test]
+ public void Verify_that_a_malformed_boolean_attribute_is_recorded_as_an_error_and_aborts_the_load()
+ {
+ var model = Package("malformed-bool", "");
+ var resource = this.CreateResourceForContent("malformed-bool.ecore", model);
+
+ Assert.Throws(() => resource.Load(null));
+
+ var error = resource.Errors.SingleOrDefault();
+ Assert.That(error, Is.Not.Null);
+ Assert.Multiple(() =>
+ {
+ Assert.That(error!.Message, Does.Contain("abstract"));
+ Assert.That(error.Message, Does.Contain("notabool"));
+ Assert.That(error.Location, Is.EqualTo(resource.URI.AbsoluteUri));
+ });
+ }
+
+ [Test]
+ public void Verify_that_a_malformed_integer_attribute_is_recorded_as_an_error_and_aborts_the_load()
+ {
+ var model = Package(
+ "malformed-int",
+ "\r\n" +
+ " \r\n" +
+ " ");
+ var resource = this.CreateResourceForContent("malformed-int.ecore", model);
+
+ Assert.Throws(() => resource.Load(null));
+
+ var error = resource.Errors.SingleOrDefault();
+ Assert.That(error, Is.Not.Null);
+ Assert.Multiple(() =>
+ {
+ Assert.That(error!.Message, Does.Contain("value"));
+ Assert.That(error.Message, Does.Contain("notanint"));
+ });
+ }
+
+ [Test]
+ public void Verify_that_an_unrecognized_classifier_type_is_recorded_as_an_error_and_aborts_the_load()
+ {
+ var model = Package("unknown-classifier", "");
+ var resource = this.CreateResourceForContent("unknown-classifier.ecore", model);
+
+ Assert.Throws(() => resource.Load(null));
+
+ var error = resource.Errors.SingleOrDefault();
+ Assert.That(error, Is.Not.Null);
+ Assert.That(error!.Message, Does.Contain("Bogus"));
+ }
+
+ [Test]
+ public void Verify_that_an_unrecognized_structural_feature_type_is_recorded_as_an_error_and_aborts_the_load()
+ {
+ var model = Package(
+ "unknown-feature",
+ "\r\n" +
+ " \r\n" +
+ " ");
+ var resource = this.CreateResourceForContent("unknown-feature.ecore", model);
+
+ Assert.Throws(() => resource.Load(null));
+
+ var error = resource.Errors.SingleOrDefault();
+ Assert.That(error, Is.Not.Null);
+ Assert.That(error!.Message, Does.Contain("Bogus"));
+ }
+
+ ///
+ /// Wraps the supplied classifier markup in a minimal Ecore package whose name matches
+ /// .
+ ///
+ private static string Package(string packageName, string body)
+ {
+ return
+ "\r\n" +
+ "\r\n" +
+ $" {body}\r\n" +
+ "";
+ }
+
+ ///
+ /// Writes the provided to a file in the test directory and
+ /// creates a for it.
+ ///
+ private Resource CreateResourceForContent(string fileName, string content)
+ {
+ var path = Path.Combine(TestContext.CurrentContext.TestDirectory, fileName);
+ File.WriteAllText(path, content);
+
+ var uri = new Uri(Path.GetFullPath(path));
+
+ return this.resourceSet.CreateResource(uri);
+ }
+ }
+}
diff --git a/ECoreNetto/ModelElement/EObject.cs b/ECoreNetto/ModelElement/EObject.cs
index b5a26ce..51c4309 100644
--- a/ECoreNetto/ModelElement/EObject.cs
+++ b/ECoreNetto/ModelElement/EObject.cs
@@ -356,7 +356,65 @@ protected void SaveAttributes(XmlNode reader)
this.Attributes.Add(readerAttribute.Name, attributeValue);
}
}
-
+
+ ///
+ /// Parses the value of a boolean attribute, recording an error and
+ /// throwing when the value is malformed.
+ ///
+ ///
+ /// The name of the attribute being parsed; used in the diagnostic message.
+ ///
+ ///
+ /// The raw attribute value to parse.
+ ///
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Thrown when is not a valid boolean. The issue is recorded in
+ /// before the load is aborted.
+ ///
+ protected bool ParseBoolean(string attributeName, string rawValue)
+ {
+ if (bool.TryParse(rawValue, out var value))
+ {
+ return value;
+ }
+
+ var message = $"The '{attributeName}' attribute of '{this.Identifier}' has a malformed boolean value '{rawValue}'.";
+ this.EResource.AddError(message);
+ throw new FormatException(message);
+ }
+
+ ///
+ /// Parses the value of an integer attribute, recording an error and
+ /// throwing when the value is malformed.
+ ///
+ ///
+ /// The name of the attribute being parsed; used in the diagnostic message.
+ ///
+ ///
+ /// The raw attribute value to parse.
+ ///
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Thrown when is not a valid integer. The issue is recorded in
+ /// before the load is aborted.
+ ///
+ protected int ParseInt32(string attributeName, string rawValue)
+ {
+ if (int.TryParse(rawValue, out var value))
+ {
+ return value;
+ }
+
+ var message = $"The '{attributeName}' attribute of '{this.Identifier}' has a malformed integer value '{rawValue}'.";
+ this.EResource.AddError(message);
+ throw new FormatException(message);
+ }
+
///
/// Instantiate new from the current node of the
///
diff --git a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
index 28c5f1a..2be59a6 100644
--- a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
+++ b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs
@@ -170,12 +170,12 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue(EcoreAbstractKeyword, out var output))
{
- this.Abstract = bool.Parse(output);
+ this.Abstract = this.ParseBoolean(EcoreAbstractKeyword, output);
}
if (this.Attributes.TryGetValue(EcoreInterfaceKeyword, out output))
{
- this.Interface = bool.Parse(output);
+ this.Interface = this.ParseBoolean(EcoreInterfaceKeyword, output);
}
if (this.Attributes.TryGetValue(EcoreSuperTypeKeyword, out output))
@@ -219,7 +219,9 @@ protected override void DeserializeChildNode(XmlNode reader)
ecoreAttribute.ReadXml(reader);
break;
default:
- throw new InvalidOperationException($"Type of structural feature not recognized: {ecoreType}");
+ var featureError = $"Type of structural feature not recognized: {ecoreType}";
+ this.EResource.AddError(featureError);
+ throw new InvalidOperationException(featureError);
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs b/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs
index 7930e88..0340412 100644
--- a/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs
+++ b/ECoreNetto/ModelElement/NamedElement/Classifier/EDataType.cs
@@ -70,7 +70,7 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("serializable", out var output))
{
- this.Serializable = bool.Parse(output);
+ this.Serializable = this.ParseBoolean("serializable", output);
}
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs b/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs
index d8de023..8ac0fca 100644
--- a/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs
+++ b/ECoreNetto/ModelElement/NamedElement/EEnumLiteral.cs
@@ -68,7 +68,7 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("value", out var output))
{
- this.Value = int.Parse(output);
+ this.Value = this.ParseInt32("value", output);
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/EPackage.cs b/ECoreNetto/ModelElement/NamedElement/EPackage.cs
index 815ddeb..37a58a5 100644
--- a/ECoreNetto/ModelElement/NamedElement/EPackage.cs
+++ b/ECoreNetto/ModelElement/NamedElement/EPackage.cs
@@ -183,7 +183,9 @@ protected override void DeserializeChildNode(XmlNode reader)
ecoreEnum.ReadXml(reader);
break;
default:
- throw new InvalidOperationException($"Type of classifier not recognized: {ecoreType}");
+ var classifierError = $"Type of classifier not recognized: {ecoreType}";
+ this.EResource.AddError(classifierError);
+ throw new InvalidOperationException(classifierError);
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
index 458a8e0..cb0dc54 100644
--- a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
+++ b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs
@@ -103,32 +103,32 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("ordered", out var output))
{
- this.Ordered = bool.Parse(output);
+ this.Ordered = this.ParseBoolean("ordered", output);
}
if (this.Attributes.TryGetValue("unique", out output))
{
- this.Unique = bool.Parse(output);
+ this.Unique = this.ParseBoolean("unique", output);
}
if (this.Attributes.TryGetValue("many", out output))
{
- this.Many = bool.Parse(output);
+ this.Many = this.ParseBoolean("many", output);
}
if (this.Attributes.TryGetValue("required", out output))
{
- this.Required = bool.Parse(output);
+ this.Required = this.ParseBoolean("required", output);
}
if (this.Attributes.TryGetValue("lowerBound", out output))
{
- this.LowerBound = int.Parse(output);
+ this.LowerBound = this.ParseInt32("lowerBound", output);
}
if (this.Attributes.TryGetValue("upperBound", out output))
{
- this.UpperBound = int.Parse(output);
+ this.UpperBound = this.ParseInt32("upperBound", output);
}
if (this.Attributes.TryGetValue("eType", out output))
diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs
index 80d2f3b..fa51cbe 100644
--- a/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs
+++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/EStructuralFeature.cs
@@ -135,17 +135,17 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("changeable", out var output))
{
- this.Changeable = bool.Parse(output);
+ this.Changeable = this.ParseBoolean("changeable", output);
}
if (this.Attributes.TryGetValue("volatile", out output))
{
- this.Volatile = bool.Parse(output);
+ this.Volatile = this.ParseBoolean("volatile", output);
}
if (this.Attributes.TryGetValue("transient", out output))
{
- this.Transient = bool.Parse(output);
+ this.Transient = this.ParseBoolean("transient", output);
}
if (this.Attributes.TryGetValue("defaultValueLiteral", out output))
@@ -155,12 +155,12 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("unsettable", out output))
{
- this.Unsettable = bool.Parse(output);
+ this.Unsettable = this.ParseBoolean("unsettable", output);
}
if (this.Attributes.TryGetValue("derived", out output))
{
- this.Derived = bool.Parse(output);
+ this.Derived = this.ParseBoolean("derived", output);
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs
index 9eee076..294f70e 100644
--- a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs
+++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EAttribute.cs
@@ -70,7 +70,7 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("iD", out var output))
{
- this.ID = bool.Parse(output);
+ this.ID = this.ParseBoolean("iD", output);
}
}
}
diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs
index b36f667..660afb6 100644
--- a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs
+++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs
@@ -103,17 +103,17 @@ internal override void SetProperties()
if (this.Attributes.TryGetValue("container", out var output))
{
- this.IsContainer = bool.Parse(output);
+ this.IsContainer = this.ParseBoolean("container", output);
}
if (this.Attributes.TryGetValue("containment", out output))
{
- this.IsContainment = bool.Parse(output);
+ this.IsContainment = this.ParseBoolean("containment", output);
}
if (this.Attributes.TryGetValue("resolveProxies", out output))
{
- this.IsResolveProxies = bool.Parse(output);
+ this.IsResolveProxies = this.ParseBoolean("resolveProxies", output);
}
if (this.Attributes.TryGetValue("eOpposite", out output))
diff --git a/ECoreNetto/Resource/Resource.cs b/ECoreNetto/Resource/Resource.cs
index f429811..7414ce8 100644
--- a/ECoreNetto/Resource/Resource.cs
+++ b/ECoreNetto/Resource/Resource.cs
@@ -403,11 +403,26 @@ public void UnLoad()
this.isLoaded = false;
}
+ ///
+ /// Records an error that was encountered while loading the resource.
+ ///
+ ///
+ /// The translated message describing the issue.
+ ///
+ ///
+ /// The diagnostic is exposed through . The source location is the URI of this
+ /// resource; line and column are not available once parsing has reached the property-resolution phase.
+ ///
+ internal void AddError(string message)
+ {
+ this.errors.Add(new Diagnostic(0, 0, this.URI?.AbsoluteUri ?? string.Empty, message));
+ }
+
///
/// Gets an of the errors in the resource;
///
///
- /// These will typically be produced as the resource is loaded.
+ /// These will typically be produced as the resource is loaded.
///
public IEnumerable Errors => this.errors;