diff --git a/ECoreNetto.Tests/Resource/GuardedGetEObjectTestFixture.cs b/ECoreNetto.Tests/Resource/GuardedGetEObjectTestFixture.cs new file mode 100644 index 0000000..202f755 --- /dev/null +++ b/ECoreNetto.Tests/Resource/GuardedGetEObjectTestFixture.cs @@ -0,0 +1,191 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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 ECoreNetto.Resource; + + using NUnit.Framework; + + /// + /// Suite of tests that verify the typed, guarded resolver and the + /// unsafe-cast call sites that use it (see issue #29 / #32). Unresolved or wrong-typed references must + /// yield a clear that names the offending fragment, instead of a + /// vague , or stack overflow. + /// + [TestFixture] + public class GuardedGetEObjectTestFixture + { + private ResourceSet resourceSet = null!; + private Resource resource = null!; + + [SetUp] + public void SetUp() + { + this.resourceSet = new ResourceSet(); + this.resource = new Resource { ResourceSet = this.resourceSet }; + this.resourceSet.Resources.Add(this.resource); + } + + [Test] + public void Verify_that_a_cached_object_of_the_expected_type_is_returned() + { + var eClass = new EClass(this.resource); + this.resource.Cache.Add("a-fragment", eClass); + + Assert.That(this.resource.GetEObject("a-fragment"), Is.SameAs(eClass)); + } + + [Test] + public void Verify_that_resolving_an_EClass_reference_to_a_wrong_type_throws_a_descriptive_exception() + { + // an EDataType is an EClassifier but not an EClass; this is what an EClass.eSuperTypes + // pointing at a datatype would yield + this.resource.Cache.Add("super-type", new EDataType(this.resource)); + + var exception = Assert.Throws( + () => this.resource.GetEObject("super-type")); + var message = exception!.Message; + + Assert.Multiple(() => + { + Assert.That(message, Does.Contain("super-type")); + Assert.That(message, Does.Contain(nameof(EClass))); + Assert.That(message, Does.Contain(nameof(EDataType))); + }); + } + + [Test] + public void Verify_that_resolving_an_EClassifier_reference_to_a_wrong_type_throws_a_descriptive_exception() + { + // an EReference is not an EClassifier; this is what an eType pointing at a feature would yield + this.resource.Cache.Add("the-type", new EReference(this.resource)); + + var exception = Assert.Throws( + () => this.resource.GetEObject("the-type")); + var message = exception!.Message; + + Assert.Multiple(() => + { + Assert.That(message, Does.Contain("the-type")); + Assert.That(message, Does.Contain(nameof(EClassifier))); + }); + } + + [Test] + public void Verify_that_resolving_an_EReference_reference_to_a_wrong_type_throws_a_descriptive_exception() + { + // an EAttribute is an EStructuralFeature but not an EReference; this is what an eOpposite + // pointing at an attribute would yield + this.resource.Cache.Add("the-opposite", new EAttribute(this.resource)); + + var exception = Assert.Throws( + () => this.resource.GetEObject("the-opposite")); + var message = exception!.Message; + + Assert.Multiple(() => + { + Assert.That(message, Does.Contain("the-opposite")); + Assert.That(message, Does.Contain(nameof(EReference))); + }); + } + + [Test] + public void Verify_that_resolving_an_unresolvable_fragment_throws_a_descriptive_exception() + { + // the fragment is not in the cache, is not a known ECore type, and does not point at another + // .ecore resource: the base GetEObject returns null and the typed resolver reports it clearly + var exception = Assert.Throws( + () => this.resource.GetEObject("does-not-exist")); + var message = exception!.Message; + + Assert.Multiple(() => + { + Assert.That(message, Does.Contain("does-not-exist")); + Assert.That(message, Does.Contain("null (unresolved)")); + }); + } + + [Test] + public void Verify_that_the_base_resolver_returns_null_for_an_unresolvable_fragment() + { + Assert.That(this.resource.GetEObject("does-not-exist"), Is.Null); + } + + [Test] + public void Verify_that_an_unresolved_eSuperTypes_reference_is_reported_when_loading() + { + // EClass.SetProperties resolves eSuperTypes via GetEObject + const string model = + "\r\n" + + "\r\n" + + " \r\n" + + ""; + + // the file name must match the root package name so the implicit '#//' reference, + // which is rewritten to '.ecore#//...', resolves back to this resource + var resource = this.CreateResourceForContent("superpkg.ecore", model); + + var exception = Assert.Throws(() => resource.Load(null)); + Assert.That(exception!.Message, Does.Contain("DoesNotExist")); + } + + [Test] + public void Verify_that_an_unresolved_eType_reference_is_reported_when_loading() + { + // ETypedElement.SetProperties resolves eType via GetEObject + const string model = + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + + // the file name must match the root package name so the implicit '#//' reference, + // which is rewritten to '.ecore#//...', resolves back to this resource + var resource = this.CreateResourceForContent("typepkg.ecore", model); + + var exception = Assert.Throws(() => resource.Load(null)); + Assert.That(exception!.Message, Does.Contain("Missing")); + } + + /// + /// 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/NamedElement/Classifier/EClass.cs b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs index 3d4b4d0..28c5f1a 100644 --- a/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs +++ b/ECoreNetto/ModelElement/NamedElement/Classifier/EClass.cs @@ -183,7 +183,7 @@ internal override void SetProperties() var typeNames = output.Split(' '); foreach (var typeName in typeNames) { - this.ESuperTypes.Add((EClass)this.EResource.GetEObject(typeName)); + this.ESuperTypes.Add(this.EResource.GetEObject(typeName)); } } } diff --git a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs index b02b107..458a8e0 100644 --- a/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs +++ b/ECoreNetto/ModelElement/NamedElement/ETypedElement.cs @@ -136,7 +136,7 @@ internal override void SetProperties() var parts = output.Split(' '); var typeName = parts[parts.Length - 1]; - this.EType = (EClassifier)this.EResource.GetEObject(typeName); + this.EType = this.EResource.GetEObject(typeName); } } } diff --git a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs index 7f2fb18..b36f667 100644 --- a/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs +++ b/ECoreNetto/ModelElement/NamedElement/TypedElement/StructuralFeature/EReference.cs @@ -119,7 +119,7 @@ internal override void SetProperties() if (this.Attributes.TryGetValue("eOpposite", out output)) { var typeName = output; - this.EOpposite = (EReference)this.EResource.GetEObject($"EStructuralFeature::{typeName}"); + this.EOpposite = this.EResource.GetEObject($"EStructuralFeature::{typeName}"); } } } diff --git a/ECoreNetto/Resource/Resource.cs b/ECoreNetto/Resource/Resource.cs index 0c16cae..f429811 100644 --- a/ECoreNetto/Resource/Resource.cs +++ b/ECoreNetto/Resource/Resource.cs @@ -225,7 +225,7 @@ public string GetURIFragment(EObject eObject) /// /// The resolved object for the given fragment, or null if it can't be resolved. /// - public EObject GetEObject(string uriFragment) + public EObject? GetEObject(string uriFragment) { this.logger.LogTrace("Getting EObject for resources {0}", uriFragment); @@ -254,7 +254,11 @@ public EObject GetEObject(string uriFragment) var uriFragments = uriFragment.Split('#'); if (!uriFragments[0].Contains(".ecore")) { - throw new ArgumentException($"The resource {uriFragments[0]} is invalid."); + // the fragment does not point at another .ecore resource and was not found in + // this resource's cache or the known ECore types: it cannot be resolved. + this.logger.LogTrace("EObject using uri fragment '{0}' could not be resolved", uriFragment); + + return null; } var index = this.URI.AbsolutePath.LastIndexOf('/'); @@ -274,11 +278,50 @@ public EObject GetEObject(string uriFragment) resource.Load(null); } + if (resource == this) + { + // the fragment points back at the current resource but was not found in its cache: + // it cannot be resolved. Returning here avoids unbounded self-recursion. + this.logger.LogTrace("EObject using uri fragment '{0}' could not be resolved", uriFragment); + + return null; + } + return resource.GetEObject(uriFragment); } /// - /// Saves the resource using the specified options. + /// Resolves the given and returns it typed as . + /// + /// + /// The expected subtype of the resolved object. + /// + /// + /// the fragment to resolve. + /// + /// + /// The resolved object, typed as . + /// + /// + /// Thrown when the cannot be resolved, or resolves to an object + /// that is not a . The exception message names the offending fragment. + /// + public T GetEObject(string uriFragment) where T : EObject + { + var eObject = this.GetEObject(uriFragment); + + if (eObject is not T typedEObject) + { + throw new InvalidOperationException( + $"The reference '{uriFragment}' could not be resolved to a '{typeof(T).Name}'; " + + $"it resolved to '{(eObject == null ? "null (unresolved)" : eObject.GetType().Name)}'."); + } + + return typedEObject; + } + + /// + /// Saves the resource using the specified options. /// /// /// Options are handled generically as feature-to-setting entries; the resource will ignore options it doesn't recognize.