From d96613aab55263f181754def8347100dbcd44d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 29 Oct 2026 11:51:25 +0100 Subject: [PATCH] fix(xtext): count selector fragment siblings the way they are resolved computeSelectorFragmentSegment counted earlier siblings with the same selector value using equals(), while getEObjectFromSegment matches them ignoring case unless the class is an ICaseSensitiveNamedElement. For a case-insensitive class with siblings "foo" and "FOO", the fragment of "FOO" got index 0 and resolved to "foo". Both sides now use the same comparison. Fragments change only for case-insensitive siblings whose selector values differ only in case. Co-Authored-By: Claude Opus 5.5 --- .../AbstractSelectorFragmentProviderTest.java | 96 +++++++++++++++++++ .../AbstractSelectorFragmentProvider.java | 23 ++++- 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProviderTest.java b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProviderTest.java index 1b8f16be9..5cbc25a9c 100644 --- a/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProviderTest.java +++ b/com.avaloq.tools.ddk.xtext.test/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProviderTest.java @@ -13,9 +13,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; +import java.util.List; + +import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.emf.ecore.EcorePackage; +import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.AbstractElement; import org.eclipse.xtext.AbstractRule; @@ -42,6 +49,9 @@ public class AbstractSelectorFragmentProviderTest { // CHECKSTYLE:ON + private static final String NAME_FEATURE = "name"; + private static final String CHILDREN_FEATURE = "children"; + @Inject private ParseHelper parseHelper; @@ -78,6 +88,85 @@ public void testEscapedSelectorValue() { assertFragmentMatchesAndResolves(grammar.eResource(), "/0/5(0='Foo')#0/2(3='foo.bar#')", grammar.getRules().get(0).getAlternatives()); } + @Test + public void testCaseSensitiveSelectorValue() { + EObject container = createContainer(true, "foo", "FOO"); + for (EObject child : children(container)) { + assertSame(child, resolve(container, segment(child))); + } + } + + @Test + public void testCaseInsensitiveSelectorValue() { + EObject container = createContainer(false, "foo"); + EObject child = children(container).get(0); + assertSame(child, resolve(container, segment(child).replace("'foo'", "'FOO'"))); + } + + @Test + public void testCaseInsensitiveSiblingsDifferingInCase() { + EObject container = createContainer(false, "foo", "FOO"); + for (EObject child : children(container)) { + assertSame(child, resolve(container, segment(child))); + } + } + + /** + * Creates a container whose children are named by the given names, using a dynamic model whose child class is case sensitive if requested. + */ + private static EObject createContainer(final boolean caseSensitive, final String... names) { + EcoreFactory factory = EcoreFactory.eINSTANCE; + EPackage ePackage = factory.createEPackage(); + ePackage.setName("test"); + ePackage.setNsURI("http://www.foo.com/test/" + caseSensitive); + EClass childClass = factory.createEClass(); + childClass.setName("Child"); + if (caseSensitive) { + // the provider only checks the name of the super type + EClass marker = factory.createEClass(); + marker.setName("ICaseSensitiveNamedElement"); + marker.setInterface(true); + marker.setAbstract(true); + ePackage.getEClassifiers().add(marker); + childClass.getESuperTypes().add(marker); + } + EAttribute name = factory.createEAttribute(); + name.setName(NAME_FEATURE); + name.setEType(EcorePackage.Literals.ESTRING); + childClass.getEStructuralFeatures().add(name); + EClass containerClass = factory.createEClass(); + containerClass.setName("Container"); + EReference childrenReference = factory.createEReference(); + childrenReference.setName(CHILDREN_FEATURE); + childrenReference.setEType(childClass); + childrenReference.setContainment(true); + childrenReference.setUpperBound(-1); + containerClass.getEStructuralFeatures().add(childrenReference); + ePackage.getEClassifiers().add(childClass); + ePackage.getEClassifiers().add(containerClass); + + EObject container = EcoreUtil.create(containerClass); + for (String childName : names) { + EObject child = EcoreUtil.create(childClass); + child.eSet(name, childName); + children(container).add(child); + } + return container; + } + + @SuppressWarnings("unchecked") + private static List children(final EObject container) { + return (List) container.eGet(container.eClass().getEStructuralFeature(CHILDREN_FEATURE)); + } + + private static String segment(final EObject child) { + return new TestSelectorFragmentProvider().nameSegment(child); + } + + private static EObject resolve(final EObject container, final String segment) { + return new TestSelectorFragmentProvider().getEObjectFromSegment(container, segment); + } + private void assertFragmentMatchesAndResolves(final Resource res, final String expectedFragment, final EObject obj) { String fragment = res.getURIFragment(obj); assertEquals(expectedFragment, fragment); @@ -121,6 +210,13 @@ public boolean appendFragmentSegment(final EObject object, final StringBuilder b return super.appendFragmentSegment(object, builder); } + // the test runs in another bundle, so protected methods are only reachable from this subclass + String nameSegment(final EObject obj) { + StringBuilder builder = new StringBuilder(); + computeSelectorFragmentSegment(obj, obj.eClass().getEStructuralFeature(NAME_FEATURE), false, builder); + return builder.toString(); + } + private boolean appendFragmentSegment(final Grammar obj, final StringBuilder builder) { return computeSelectorFragmentSegment(obj, XtextPackage.Literals.GRAMMAR__NAME, true, builder); } diff --git a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProvider.java b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProvider.java index 658c431b8..7dc34966c 100644 --- a/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProvider.java +++ b/com.avaloq.tools.ddk.xtext/src/com/avaloq/tools/ddk/xtext/resource/AbstractSelectorFragmentProvider.java @@ -88,8 +88,9 @@ protected boolean computeSelectorFragmentSegment(final EObject obj, final EStruc int selectorIndex = 0; final int objectIndex = containmentList.indexOf(obj); for (int i = 0; i < objectIndex; i++) { - final Object value = containmentList.get(i).eGet(selectorFeature); - if ((selectorValue == null && value == null) || (selectorValue != null && selectorValue.equals(value))) { + final EObject sibling = containmentList.get(i); + final Object value = sibling.eGet(selectorFeature); + if (selectorValue == null ? value == null : value != null && matches(sibling, value.toString(), selectorValue.toString())) { selectorIndex++; } } @@ -133,6 +134,21 @@ private static Boolean isCaseSensitive(final EClass eClass) { return caseSensitive; } + /** + * Returns whether the given selector value text matches the expected value, ignoring case unless the object's class is case sensitive. + * + * @param object + * object whose selector value is matched + * @param text + * text of the object's selector value + * @param expected + * expected selector value, may be {@code null} + * @return {@code true} if the values match + */ + private static boolean matches(final EObject object, final String text, final String expected) { + return text.equalsIgnoreCase(expected) && (!isCaseSensitive(object.eClass()) || text.equals(expected)); + } + @Override @SuppressWarnings({"unchecked", "PMD.NPathComplexity"}) public EObject getEObjectFromSegment(final EObject container, final String segment) { @@ -165,8 +181,7 @@ public EObject getEObjectFromSegment(final EObject container, final String segme return object; } } else { - String text = value.toString(); - if (text.equalsIgnoreCase(matchedValue) && (!isCaseSensitive(object.eClass()) || text.equals(matchedValue)) && index++ == matchedIndex) { + if (matches(object, value.toString(), matchedValue) && index++ == matchedIndex) { return object; } }