From: Samuel Schneider Date: Fri, 3 Mar 2023 07:42:50 +0000 (+0100) Subject: Retain grouping/uses instantiation vectors X-Git-Tag: v13.0.0~65 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=mdsal.git;a=commitdiff_plain;h=b1e88583f0d16ef8b8dab5d43f61978e064e27e5 Retain grouping/uses instantiation vectors We need the ability fo find all instantiations of a grouping for closed-world analysis of BindingRuntimeTypes. This analysis is need to determine, for example: - possible types of 'type leafref's pointing outside a grouping, to determine which Binding/DOM codecs are applicable - YANG/Binding overload mapping, i.e. whether a 'container' defined in a grouping is instantiated only once or multiple types, to use a strongly-bound CodecDataObject it the former case This patch exposes a GroupingRuntimeType.instantiations(), which exposes exactly this information. DefaultGroupingRuntimeType stores this information in the form of a set of vectors, each pointing either to another grouping or to a concrete instantiation. GeneratorReactor collects this information sufficiently early so that it can be also used to perform partial closed-world analysis during compile-time. JIRA: MDSAL-669 Change-Id: I2e21a6b93ce30d9bd1022be5747d44663b6198fc Signed-off-by: Samuel Schneider Signed-off-by: Robert Varga --- diff --git a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractCompositeGenerator.java b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractCompositeGenerator.java index 20cdd0146e..5d74b61ad0 100644 --- a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractCompositeGenerator.java +++ b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractCompositeGenerator.java @@ -14,6 +14,7 @@ import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; @@ -266,6 +267,29 @@ public abstract class AbstractCompositeGenerator skippedChildren) { + // Link to used groupings IFF we have a corresponding generated Java class + switch (classPlacement()) { + case NONE: + case PHANTOM: + break; + default: + for (var grouping : groupings()) { + grouping.addUser(this); + } + } + + for (var child : childGenerators) { + if (child instanceof GroupingGenerator grouping) { + skippedChildren.add(grouping); + } else if (child instanceof AbstractCompositeGenerator composite) { + composite.linkUsedGroupings(skippedChildren); + } + } + } + final void startUsesAugmentLinkage(final List requirements) { for (var child : childGenerators) { if (child instanceof UsesAugmentGenerator uses) { diff --git a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GeneratorReactor.java b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GeneratorReactor.java index fde702375b..57d477d87b 100644 --- a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GeneratorReactor.java +++ b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GeneratorReactor.java @@ -17,6 +17,7 @@ import com.google.common.collect.Maps; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -182,15 +183,20 @@ public final class GeneratorReactor extends GeneratorContext implements Mutable */ linkDependencies(children); - // Step five: resolve all 'type leafref' and 'type identityref' statements, so they point to their - // corresponding Java type representation. + // Step 5: resolve grouping usage, so that each GroupingGenerator has links to their instantiation sites and + // any unused + resolveGroupingUsers(); + freezeGroupingUsers(children); + + // Step 6: resolve all 'type leafref' and 'type identityref' statements, so they point to their corresponding + // Java type representation. bindTypeDefinition(children); - // Step six: walk all composite generators and link ChildOf/ChoiceIn relationships with parents. We have taken - // care of this step during tree construction, hence this now a no-op. + // Step 7: walk all composite generators and link ChildOf/ChoiceIn relationships with parents. We have taken + // care of this step during tree construction, hence this now a no-op. /* - * Step seven: assign java packages and JavaTypeNames + * Step 8: assign java packages and JavaTypeNames * * This is a really tricky part, as we have large number of factors to consider: * - we are mapping grouping, typedef, identity and schema tree namespaces into Fully Qualified Class Names, @@ -222,11 +228,13 @@ public final class GeneratorReactor extends GeneratorContext implements Mutable } } while (haveUnresolved); - // Step eight: generate actual Types - // - // We have now properly cross-linked all generators and have assigned their naming roots, so from this point - // it looks as though we are performing a simple recursive execution. In reality, though, the actual path taken - // through generators is dictated by us as well as generator linkage. + /* + * Step 9: generate actual Types + * + * We have now properly cross-linked all generators and have assigned their naming roots, so from this point + * it looks as though we are performing a simple recursive execution. In reality, though, the actual path taken + * through generators is dictated by us as well as generator linkage. + */ for (var module : children) { module.ensureType(builderFactory); } @@ -418,4 +426,58 @@ public final class GeneratorReactor extends GeneratorContext implements Mutable stack.pop(); } } + + private void resolveGroupingUsers() { + // Primary pass on modules, collecting all groupings which were left unprocessed + // TODO: use a plain List + final var remaining = new HashSet(); + for (var module : children) { + module.linkUsedGroupings(remaining); + } + LOG.debug("Grouping pass 1 found {} groupings", remaining.size()); + + // Secondary passes: if any unprocessed groupings have been marked as used, process their children, potentially + // adding more work + int passes = 2; + int processed; + do { + // Do not process groupings again unless we make some progress + processed = 0; + + final var found = new HashSet(); + final var it = remaining.iterator(); + while (it.hasNext()) { + final var next = it.next(); + if (next.hasUser()) { + // Process this grouping and remember we need to iterate again, as groupings we have already visited + // may become used as a side-effect. + it.remove(); + next.linkUsedGroupings(found); + processed++; + } + } + + final var foundSize = found.size(); + LOG.debug("Grouping pass {} processed {} and found {} grouping(s)", passes, processed, foundSize); + if (foundSize != 0) { + // we have some more groupings to process, shove them into the next iteration + remaining.addAll(found); + } + + passes++; + } while (processed != 0); + + LOG.debug("Grouping usage completed after {} pass(es) with unused {} grouping(s)", passes, remaining.size()); + } + + private static void freezeGroupingUsers(final Iterable parent) { + for (var child : parent) { + if (child instanceof AbstractCompositeGenerator composite) { + if (composite instanceof GroupingGenerator grouping) { + grouping.freezeUsers(); + } + freezeGroupingUsers(composite); + } + } + } } diff --git a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GroupingGenerator.java b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GroupingGenerator.java index 8697f97bc3..5df0779613 100644 --- a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GroupingGenerator.java +++ b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GroupingGenerator.java @@ -7,12 +7,12 @@ */ package org.opendaylight.mdsal.binding.generator.impl.reactor; -import static com.google.common.base.Verify.verify; - +import com.google.common.base.VerifyException; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import org.opendaylight.mdsal.binding.generator.impl.rt.DefaultGroupingRuntimeType; import org.opendaylight.mdsal.binding.model.api.GeneratedType; -import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedTypeBuilder; import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedTypeBuilderBase; import org.opendaylight.mdsal.binding.model.ri.BindingTypes; import org.opendaylight.mdsal.binding.runtime.api.AugmentRuntimeType; @@ -26,10 +26,35 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; * Generator corresponding to a {@code grouping} statement. */ final class GroupingGenerator extends AbstractCompositeGenerator { + // Linkage towards concrete data tree instantiations of this grouping. This can contain two different kinds of + // generators: + // - GroupingGenerators which provide next step in the linkage + // - other composite generators, which are the actual instantiations + private List> users; + GroupingGenerator(final GroupingEffectiveStatement statement, final AbstractCompositeGenerator parent) { super(statement, parent); } + void addUser(final AbstractCompositeGenerator user) { + if (users == null) { + // We are adding the first user: allocate a small set and notify the groupings we use that we are a user + users = new ArrayList<>(); + for (var grouping : groupings()) { + grouping.addUser(this); + } + } + users.add(user); + } + + boolean hasUser() { + return users != null; + } + + void freezeUsers() { + users = users == null ? List.of() : users.stream().distinct().collect(Collectors.toUnmodifiableList()); + } + @Override StatementNamespace namespace() { return StatementNamespace.GROUPING; @@ -42,13 +67,13 @@ final class GroupingGenerator extends AbstractCompositeGenerator createBuilder( final GroupingEffectiveStatement statement) { + final var local = users; + if (local == null) { + throw new VerifyException(this + " has unresolved users"); + } + + final var vectors = local.stream() + .map(AbstractCompositeGenerator::getRuntimeType) + .distinct() + .collect(Collectors.toUnmodifiableList()); + return new CompositeRuntimeTypeBuilder<>(statement) { @Override GroupingRuntimeType build(final GeneratedType type, final GroupingEffectiveStatement statement, final List children, final List augments) { // Groupings cannot be targeted by 'augment' - verify(augments.isEmpty(), "Unexpected augments %s", augments); - return new DefaultGroupingRuntimeType(type, statement, children); + if (augments.isEmpty()) { + return new DefaultGroupingRuntimeType(type, statement, children, vectors); + } + throw new VerifyException("Unexpected augments " + augments); } }; } - } diff --git a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/rt/DefaultGroupingRuntimeType.java b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/rt/DefaultGroupingRuntimeType.java index 5b81473851..a0abdcefbb 100644 --- a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/rt/DefaultGroupingRuntimeType.java +++ b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/rt/DefaultGroupingRuntimeType.java @@ -7,16 +7,49 @@ */ package org.opendaylight.mdsal.binding.generator.impl.rt; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Objects; +import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.binding.model.api.GeneratedType; +import org.opendaylight.mdsal.binding.runtime.api.CompositeRuntimeType; import org.opendaylight.mdsal.binding.runtime.api.GroupingRuntimeType; import org.opendaylight.mdsal.binding.runtime.api.RuntimeType; import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement; public final class DefaultGroupingRuntimeType extends AbstractCompositeRuntimeType implements GroupingRuntimeType { + /** + * These are vectors towards concrete instantiations of this type -- i.e. the manifestation in the effective data + * tree. Each item in this list represents either: + *
    + *
  • a concrete instantiation, or
  • + *
  • another {@link GroupingRuntimeType}
  • + *
+ * We use these vectors to create {@link #instantiations()}. + */ + private final @Nullable Object instantiationVectors; + public DefaultGroupingRuntimeType(final GeneratedType bindingType, final GroupingEffectiveStatement statement, - final List children) { + final List children, final List instantiationVectors) { super(bindingType, statement, children); + this.instantiationVectors = switch (instantiationVectors.size()) { + case 0 -> null; + case 1 -> Objects.requireNonNull(instantiationVectors.get(0)); + default -> instantiationVectors.stream().map(Objects::requireNonNull).toArray(CompositeRuntimeType[]::new); + }; + } + + @Override + public List directUsers() { + final var local = instantiationVectors; + if (local == null) { + return List.of(); + } else if (local instanceof CompositeRuntimeType[] array) { + return Collections.unmodifiableList(Arrays.asList(array)); + } else { + return List.of((CompositeRuntimeType) local); + } } } diff --git a/binding/mdsal-binding-generator/src/test/java/org/opendaylight/mdsal/binding/generator/impl/Mdsal669Test.java b/binding/mdsal-binding-generator/src/test/java/org/opendaylight/mdsal/binding/generator/impl/Mdsal669Test.java new file mode 100644 index 0000000000..4bf407d20d --- /dev/null +++ b/binding/mdsal-binding-generator/src/test/java/org/opendaylight/mdsal/binding/generator/impl/Mdsal669Test.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.mdsal.binding.generator.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.opendaylight.mdsal.binding.model.api.JavaTypeName; +import org.opendaylight.mdsal.binding.runtime.api.BindingRuntimeTypes; +import org.opendaylight.mdsal.binding.runtime.api.GroupingRuntimeType; +import org.opendaylight.mdsal.binding.runtime.api.RuntimeType; +import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils; + +class Mdsal669Test { + private static final BindingRuntimeTypes RUNTIME_TYPES = new DefaultBindingRuntimeGenerator() + .generateTypeMapping(YangParserTestUtils.parseYangResource("/mdsal669.yang")); + + @Test + void barIsUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "Bar"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "Foo"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "Target1"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev.used.augmented", "ToBeAugmented1"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev.used.augmented.indirect", + "ToBeAugmented1")); + } + + @Test + void bazIsUsedByOneAndTwo() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "Baz"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "One"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "Two")); + } + + @Test + void unusedIsNotUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "Unused")); + } + + @Test + void fooAsStringIsNotUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "FooAsString")); + } + + @Test + void unusedBarIsNotUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UnusedBar")); + } + + @Test + void unusedAugmendIsNotUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UnusedAugmented")); + } + + @Test + void unusedIntermediateAugmentedIsNotUsed() { + assertInstances( + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UnusedIntermediateAugmentedUser")); + assertInstances( + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UnusedIntermediateAugmented")); + } + + @Test + void usedAugmentedIndirectIsUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedIndirectGrp"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedIndirectUser")); + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedIndirect"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedIndirectUser")); + } + + @Test + void usedAugmentedIsUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmented"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedUser")); + } + + @Test + void toBeAugmentedIsUsed() { + assertInstances(JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "ToBeAugmented"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedIndirectUser"), + JavaTypeName.create("org.opendaylight.yang.gen.v1.mdsal669.norev", "UsedAugmentedUser")); + } + + private static void assertInstances(final JavaTypeName groupingTypeName, final JavaTypeName... instanceTypeNames) { + assertEquals( + Arrays.stream(instanceTypeNames).map(Mdsal669Test::assertType).collect(Collectors.toSet()), + Set.copyOf(assertGrouping(groupingTypeName).instantiations())); + } + + private static GroupingRuntimeType assertGrouping(final JavaTypeName typeName) { + return assertInstanceOf(GroupingRuntimeType.class, assertType(typeName)); + } + + private static RuntimeType assertType(final JavaTypeName typeName) { + return RUNTIME_TYPES.findSchema(typeName).orElseThrow(); + } +} diff --git a/binding/mdsal-binding-generator/src/test/resources/mdsal669.yang b/binding/mdsal-binding-generator/src/test/resources/mdsal669.yang new file mode 100644 index 0000000000..eb88850dea --- /dev/null +++ b/binding/mdsal-binding-generator/src/test/resources/mdsal669.yang @@ -0,0 +1,157 @@ +module mdsal669 { + namespace mdsal669; + prefix mdsal669; + + grouping bar { + container bar { + leaf-list bar { + type leafref { + path ../../foo; + } + } + } + } + + container foo { + leaf foo { + type instance-identifier; + } + + uses bar; + } + + grouping baz { + leaf baz { + type leafref { + path "../bar"; + } + } + } + + container one { + leaf bar { + type string; + } + uses baz; + } + + container two { + leaf bar { + type uint16; + } + uses baz; + } + + grouping unused { + leaf foo { + type leafref { + path "../bar"; + } + } + } + + // this grouping is used ... + grouping foo-as-string { + leaf foo { + type string; + } + + uses bar; + } + + // ... but this grouping is not ... + grouping unused-bar { + container qux { + // ... and hence this is not count as an instantiation + uses foo-as-string; + } + } + + // Direct use via augment + container target; + + augment /target { + leaf foo { + type uint32; + } + + uses bar; + } + + // Multiple use cases for uses/augment: this is the base grouping + grouping to-be-augmented { + container to-be-augmented; + } + + // This grouping is not used + grouping unused-augmented { + uses to-be-augmented { + augment to-be-augmented { + leaf foo { + type boolean; + } + + uses bar; + } + } + } + + // This grouping is used only ... + grouping unused-intermediate-augmented { + uses to-be-augmented { + augment to-be-augmented { + leaf foo { + type uint64; + } + + uses bar; + } + } + } + + // ... by this grouping, which itself is not used + grouping unused-intermediate-augmented-user { + uses unused-intermediate-augmented; + } + + // This grouping is used directly ... + grouping used-augmented { + uses to-be-augmented { + augment to-be-augmented { + leaf foo { + type uint8; + } + + uses bar; + } + } + } + + // ... by this container + container used-augmented-user { + uses used-augmented; + } + + // ... this grouping is used ... + grouping used-augmented-indirect { + uses to-be-augmented { + augment to-be-augmented { + leaf foo { + type empty; + } + + uses bar; + } + } + } + + // ... by another grouping, which itself is used ... + grouping used-augmented-indirect-grp { + uses used-augmented-indirect; + } + + // ... by this container + container used-augmented-indirect-user { + uses used-augmented-indirect-grp; + } +} diff --git a/binding/mdsal-binding-runtime-api/src/main/java/org/opendaylight/mdsal/binding/runtime/api/GroupingRuntimeType.java b/binding/mdsal-binding-runtime-api/src/main/java/org/opendaylight/mdsal/binding/runtime/api/GroupingRuntimeType.java index 02c62a2800..6e7b31c904 100644 --- a/binding/mdsal-binding-runtime-api/src/main/java/org/opendaylight/mdsal/binding/runtime/api/GroupingRuntimeType.java +++ b/binding/mdsal-binding-runtime-api/src/main/java/org/opendaylight/mdsal/binding/runtime/api/GroupingRuntimeType.java @@ -7,6 +7,10 @@ */ package org.opendaylight.mdsal.binding.runtime.api; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement; /** @@ -15,4 +19,89 @@ import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement public interface GroupingRuntimeType extends CompositeRuntimeType { @Override GroupingEffectiveStatement statement(); + + /** + * Return the set of all concrete data tree instantiations of this {@code grouping}. This is necessary to completely + * resolve type information for {@code leafref}s. + * + *

+ * As an example, consider {@link GroupingRuntimeType} of {@code grouping baz} and it's instantiations roots + * {@code container one} and {@code container two} define in these three models: + *

{@code
+     *   module baz {
+     *     namespace baz;
+     *     prefix baz;
+     *
+     *     grouping baz {
+     *       leaf baz {
+     *         type leafref {
+     *           path "../bar";
+     *         }
+     *       }
+     *     }
+     *   }
+     *
+     *   module one {
+     *     namespace one;
+     *     prefix one;
+     *     import baz { prefix baz; }
+     *
+     *     container one {
+     *       leaf bar {
+     *         type string;
+     *       }
+     *       uses baz:baz;
+     *     }
+     *   }
+     *
+     *   module two {
+     *     namespace two;
+     *     prefix two;
+     *     import baz { prefix baz; }
+     *
+     *     container two {
+     *       leaf bar {
+     *         type uint16;
+     *       }
+     *       uses baz:baz;
+     *     }
+     *   }
+     * }
+ * + *

+ * Since these are separate modules, each of them can be part of its own compilation unit and therefore + * {@code grouping baz} compile-time analysis cannot definitely determine the return type of {@code getBaz()} and + * must fall back to {@code Object}. + * + *

+ * At run-time, though, we have a closed world, and therefore we can provide accurate information about + * instantiation sites: this method will return the {@link CompositeRuntimeType}s for {@code one} and {@code two}. + * We can then use this information to know that {@code getBaz()} can either be a {@code String} or an + * {@code Uint32} and which type is appropriate at a particular point in YANG data tree. + * + * @return The set instantiated {@link CompositeRuntimeType}s which use this grouping + */ + default @NonNull List instantiations() { + final var users = directUsers(); + return switch (users.size()) { + case 0 -> List.of(); + case 1 -> { + final var user = users.get(0); + yield user instanceof GroupingRuntimeType grouping ? grouping.instantiations() : List.of(user); + } + default -> users.stream() + .flatMap(user -> user instanceof GroupingRuntimeType grouping ? grouping.instantiations().stream() + : Stream.of(user)) + .distinct() + .collect(Collectors.toUnmodifiableList()); + }; + } + + /** + * Support method for {@link #instantiations()}. This method's return, unlike {@link #instantiations()} can contain + * other {@link GroupingRuntimeType}s. + * + * @return Direct users of this grouping + */ + @NonNull List directUsers(); }