diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors
index 9e324e1b417..471020f98a4 100644
--- a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors
+++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/additionalschemas-conflict.errors
@@ -1,3 +1,2 @@
[ERROR] smithy.example#AdditionalSchemasConflictResource: The `bar` property of the generated `AdditionalSchemasConflictResource` CloudFormation resource targets multiple shapes: [smithy.api#Boolean, smithy.api#String]. Reusing member names that target different shapes can cause confusion for users of the API. This target discrepancy must either be resolved in the model or one of the members must be excluded from the conversion. | CfnResourceProperty
-[NOTE] smithy.example#AdditionalSchemasConflictProperties: The structure shape is not connected to from any service shape. | UnreferencedShape
[WARNING] smithy.example#AdditionalSchemasConflictResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait
diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors
index da36a2fc359..4bda42c1381 100644
--- a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors
+++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/deconflict-by-excluding.errors
@@ -1,3 +1,2 @@
-[NOTE] smithy.example#AdditionalSchemasDeconflictedProperties: The structure shape is not connected to from any service shape. | UnreferencedShape
[WARNING] smithy.example#CreateAdditionalSchemasDeconflictedResourceRequest$bar: This shape applies a trait that is unstable: aws.cloudformation#cfnExcludeProperty | UnstableTrait
[WARNING] smithy.example#AdditionalSchemasDeconflictedResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait
diff --git a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-additional-schemas-shape.errors b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-additional-schemas-shape.errors
index 0d7f38476a7..3fe72f1e10c 100644
--- a/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-additional-schemas-shape.errors
+++ b/smithy-aws-cloudformation-traits/src/test/resources/software/amazon/smithy/aws/cloudformation/traits/errorfiles/invalid-additional-schemas-shape.errors
@@ -1,3 +1,2 @@
[ERROR] smithy.example#InvalidAdditionalSchemasShapeResource: Error validating trait `aws.cloudformation#cfnResource`.additionalSchemas.0: Shape ID `smithy.example#ListShape` does not match selector `structure` | TraitValue
-[NOTE] smithy.example#ListShape: The list shape is not connected to from any service shape. | UnreferencedShape
[WARNING] smithy.example#InvalidAdditionalSchemasShapeResource: This shape applies a trait that is unstable: aws.cloudformation#cfnResource | UnstableTrait
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/IdRefShapeRelationships.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/IdRefShapeRelationships.java
new file mode 100644
index 00000000000..7466afb6174
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/IdRefShapeRelationships.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.neighbor;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.StringNode;
+import software.amazon.smithy.model.selector.PathFinder;
+import software.amazon.smithy.model.selector.Selector;
+import software.amazon.smithy.model.shapes.MemberShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.IdRefTrait;
+import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.traits.TraitDefinition;
+
+/**
+ * Finds all {@link RelationshipType#ID_REF} relationships in a model.
+ *
+ *
This works by finding all paths from {@link TraitDefinition} shapes
+ * to shapes with {@link IdRefTrait}, then using those paths to search
+ * the node values of each application of the trait to extract the {@link ShapeId}
+ * value. Because we don't have a fixed set of traits known to potentially have
+ * idRef members, this has to be done dynamically.
+ */
+final class IdRefShapeRelationships {
+ private static final Selector WITH_ID_REF = Selector.parse("[trait|idRef]");
+
+ private final Model model;
+ private final Map> relationships = new HashMap<>();
+
+ IdRefShapeRelationships(Model model) {
+ this.model = model;
+ }
+
+ Map> getRelationships() {
+ PathFinder finder = PathFinder.create(model);
+ for (Shape traitDef : model.getShapesWithTrait(TraitDefinition.class)) {
+ if (traitDef.hasTrait(IdRefTrait.class)) {
+ // PathFinder doesn't handle the case where the trait def has the idRef
+ NodeQuery query = new NodeQuery().self();
+ addRelationships(traitDef, query);
+ continue;
+ }
+ List paths = finder.search(traitDef, WITH_ID_REF);
+ if (!paths.isEmpty()) {
+ for (PathFinder.Path path : paths) {
+ NodeQuery query = buildNodeQuery(path);
+ addRelationships(traitDef, query);
+ }
+ }
+ }
+ return relationships;
+ }
+
+ private void addRelationships(Shape traitDef, NodeQuery query) {
+ model.getShapesWithTrait(traitDef.getId()).forEach(shape -> {
+ Trait trait = shape.findTrait(traitDef.getId()).get(); // We already know the shape has the trait.
+ Node node = trait.toNode();
+ // Invalid shape ids are handled by the idRef trait validator, so ignore them here.
+ query.execute(node).forEach(n -> n.asStringNode()
+ .flatMap(StringNode::asShapeId)
+ .flatMap(model::getShape)
+ .map(referenced -> Relationship.create(shape, RelationshipType.ID_REF, referenced))
+ .ifPresent(rel -> relationships
+ .computeIfAbsent(rel.getShape().getId(), id -> new HashSet<>()).add(rel)));
+ });
+ }
+
+ private static NodeQuery buildNodeQuery(PathFinder.Path path) {
+ NodeQuery query = new NodeQuery();
+ // The path goes from trait definition -> shape with idRef
+ for (Relationship relationship : path) {
+ if (!relationship.getNeighborShape().isPresent()) {
+ break;
+ }
+ switch (relationship.getRelationshipType()) {
+ case MAP_KEY:
+ query.anyMemberName();
+ break;
+ case MAP_VALUE:
+ query.anyMember();
+ break;
+ case LIST_MEMBER:
+ case SET_MEMBER:
+ query.anyElement();
+ break;
+ case UNION_MEMBER:
+ case STRUCTURE_MEMBER:
+ MemberShape member = (MemberShape) relationship.getNeighborShape().get();
+ query.member(member.getMemberName());
+ break;
+ default:
+ // Other relationship types don't produce meaningful edges to search the node.
+ break;
+ }
+ }
+ return query;
+ }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborProvider.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborProvider.java
index 76978d85c55..00f8dedf367 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborProvider.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborProvider.java
@@ -26,6 +26,7 @@
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.IdRefTrait;
import software.amazon.smithy.utils.ListUtils;
/**
@@ -91,6 +92,29 @@ static NeighborProvider withTraitRelationships(Model model, NeighborProvider nei
};
}
+ /**
+ * Creates a NeighborProvider that includes {@link RelationshipType#ID_REF}
+ * relationships.
+ *
+ * @param model Model to use to look up shapes referenced by {@link IdRefTrait}.
+ * @param neighborProvider Provider to wrap.
+ * @return Returns the created neighbor provider.
+ */
+ static NeighborProvider withIdRefRelationships(Model model, NeighborProvider neighborProvider) {
+ Map> idRefRelationships = new IdRefShapeRelationships(model).getRelationships();
+ return shape -> {
+ List relationships = neighborProvider.getNeighbors(shape);
+
+ if (!idRefRelationships.containsKey(shape.getId())) {
+ return relationships;
+ }
+
+ relationships = new ArrayList<>(relationships);
+ relationships.addAll(idRefRelationships.get(shape.getId()));
+ return relationships;
+ };
+ }
+
/**
* Creates a NeighborProvider that precomputes the neighbors of a model.
*
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NodeQuery.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NodeQuery.java
new file mode 100644
index 00000000000..c01738d1850
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NodeQuery.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.neighbor;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Queue;
+import software.amazon.smithy.model.node.Node;
+
+/**
+ * Searches {@link Node}s to find matching children. Each search
+ * condition is executed on the result of the previous search,
+ * and the results are aggregated.
+ */
+final class NodeQuery {
+ private static final Query SELF = (node, result) -> result.add(node);
+
+ private static final Query ANY_MEMBER = (node, result) -> {
+ if (node == null || !node.isObjectNode()) {
+ return;
+ }
+ result.addAll(node.expectObjectNode().getMembers().values());
+ };
+
+ private static final Query ANY_ELEMENT = (node, result) -> {
+ if (node == null || !node.isArrayNode()) {
+ return;
+ }
+ result.addAll(node.expectArrayNode().getElements());
+ };
+
+ private static final Query ANY_MEMBER_NAME = (node, result) -> {
+ if (node == null || !node.isObjectNode()) {
+ return;
+ }
+ result.addAll(node.expectObjectNode().getMembers().keySet());
+ };
+
+ private final List queries = new ArrayList<>();
+
+ NodeQuery() {
+ }
+
+ NodeQuery self() {
+ queries.add(SELF);
+ return this;
+ }
+
+ NodeQuery member(String name) {
+ queries.add((node, result) -> {
+ if (node == null || !node.isObjectNode()) {
+ return;
+ }
+ node.expectObjectNode().getMember(name).ifPresent(result::add);
+ });
+ return this;
+ }
+
+ NodeQuery anyMember() {
+ queries.add(ANY_MEMBER);
+ return this;
+ }
+
+ NodeQuery anyElement() {
+ queries.add(ANY_ELEMENT);
+ return this;
+ }
+
+ NodeQuery anyMemberName() {
+ queries.add(ANY_MEMBER_NAME);
+ return this;
+ }
+
+ Collection execute(Node node) {
+ Queue previousResult = new ArrayDeque<>();
+
+ if (queries.isEmpty()) {
+ return previousResult;
+ }
+
+ previousResult.add(node);
+ for (Query query : queries) {
+ // Each time a query runs, it adds to the queue, but we only want it to
+ // run on the nodes added by the previous query.
+ for (int i = previousResult.size(); i > 0; i--) {
+ query.run(previousResult.poll(), previousResult);
+ }
+ }
+
+ return previousResult;
+ }
+
+ @FunctionalInterface
+ interface Query {
+ void run(Node node, Queue result);
+ }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java
index 97aaceb5b0d..633154676aa 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java
@@ -26,8 +26,10 @@
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ResourceShape;
import software.amazon.smithy.model.shapes.SetShape;
+import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.UnionShape;
+import software.amazon.smithy.model.traits.IdRefTrait;
import software.amazon.smithy.model.traits.TraitDefinition;
import software.amazon.smithy.utils.SmithyInternalApi;
@@ -214,7 +216,37 @@ public enum RelationshipType {
* Relationship that exists between a structure or union and a mixin applied
* to the shape.
*/
- MIXIN("mixin", RelationshipDirection.DIRECTED);
+ MIXIN("mixin", RelationshipDirection.DIRECTED),
+
+ /**
+ * Relationships that exist between a shape and another shape referenced by an
+ * {@link IdRefTrait}.
+ *
+ * This relationship is formed by applying a trait with a value containing a
+ * reference to another {@link ShapeId}. For
+ * example:
+ *
+ * {@code
+ * @trait
+ * structure myRef {
+ * @idRef
+ * shape: String
+ * }
+ *
+ * // @myRef trait applied, and the value references the shape `Referenced`
+ * @myRef(shape: Referenced)
+ * structure WithMyRef {}
+ *
+ * string Referenced
+ * }
+ *
+ *
+ * This kind of relationship is not returned by default from a
+ * {@link NeighborProvider}. You must explicitly wrap a {@link NeighborProvider}
+ * with {@link NeighborProvider#withIdRefRelationships(Model, NeighborProvider)}
+ * in order to yield idRef relationships.
+ */
+ ID_REF(null, RelationshipDirection.DIRECTED);
private String selectorLabel;
private RelationshipDirection direction;
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
index 2de3ce2b592..e412f885a48 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
@@ -27,7 +27,9 @@
/**
* Finds shapes that are not connected to a service shape, are not trait
- * definitions, and are not referenced by trait definitions.
+ * definitions, are not referenced by trait definitions, and are not
+ * referenced in trait values through
+ * {@link software.amazon.smithy.model.traits.IdRefTrait}.
*
*
Prelude shapes are never considered unreferenced.
*/
@@ -53,7 +55,9 @@ public UnreferencedShapes(Predicate keepFilter) {
* @return Returns the unreferenced shapes.
*/
public Set compute(Model model) {
- Walker shapeWalker = new Walker(NeighborProviderIndex.of(model).getProvider());
+ NeighborProvider baseProvider = NeighborProviderIndex.of(model).getProvider();
+ NeighborProvider providerWithIdRefRelationships = NeighborProvider.withIdRefRelationships(model, baseProvider);
+ Walker shapeWalker = new Walker(providerWithIdRefRelationships);
// Find all shapes connected to any service shape.
Set connected = new HashSet<>();
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/StringNode.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/StringNode.java
index c7775108715..81b001dccbc 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/node/StringNode.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/StringNode.java
@@ -168,6 +168,19 @@ public ShapeId expectShapeId() {
}
}
+ /**
+ * Gets the value of the string as a ShapeId if it is a valid Shape ID.
+ *
+ * @return Returns the optional Shape ID.
+ */
+ public Optional asShapeId() {
+ try {
+ return Optional.of(ShapeId.from(getValue()));
+ } catch (ShapeIdSyntaxException e) {
+ return Optional.empty();
+ }
+ }
+
@Override
public boolean equals(Object other) {
return other instanceof StringNode && value.equals(((StringNode) other).getValue());
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborProviderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborProviderTest.java
index eedc0d27da7..15162e5a240 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborProviderTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborProviderTest.java
@@ -1,13 +1,18 @@
package software.amazon.smithy.model.neighbor;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import java.util.List;
+import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.traits.SensitiveTrait;
@@ -42,4 +47,87 @@ public void canGetTraitRelationshipsFromShapeWithNoTraits() {
assertThat(relationships, empty());
}
+
+ @ParameterizedTest
+ @CsvSource({
+ "One,Ref1",
+ "Two,Ref2",
+ "Three,Ref3",
+ "Four,Ref4",
+ "Five,Ref5",
+ "Six,Ref6",
+ "Seven,Ref7",
+ "Eight,Ref8",
+ "Nine,Ref9",
+ "Ten,Ref10",
+ "Eleven,Ref11",
+ "Twelve,Ref12",
+ "Thirteen,Ref13",
+ "Fourteen,Ref14"
+ })
+ public void canGetIdRefRelationships(String shapeName, String referencedShapeName) {
+ Model model = Model.assembler()
+ .addImport(getClass().getResource("idref-neighbors.smithy"))
+ .assemble()
+ .unwrap();
+ NeighborProvider provider = NeighborProvider.of(model);
+ provider = NeighborProvider.withIdRefRelationships(model, provider);
+
+ Shape shape = model.expectShape(ShapeId.fromParts("com.foo", shapeName));
+ Shape ref = model.expectShape(ShapeId.fromParts("com.foo", referencedShapeName));
+ List relationships = provider.getNeighbors(shape).stream()
+ .filter(relationship -> relationship.getRelationshipType().equals(RelationshipType.ID_REF))
+ .collect(Collectors.toList());
+
+ assertThat(relationships, containsInAnyOrder(
+ equalTo(Relationship.create(shape, RelationshipType.ID_REF, ref))));
+ }
+
+ @Test
+ public void canGetIdRefRelationshipsThroughTraitDefs() {
+ Model model = Model.assembler()
+ .addImport(getClass().getResource("idref-neighbors-in-trait-def.smithy"))
+ .assemble()
+ .unwrap();
+ NeighborProvider provider = NeighborProvider.of(model);
+ provider = NeighborProvider.withIdRefRelationships(model, provider);
+
+ Shape shape = model.expectShape(ShapeId.from("com.foo#WithRefStructTrait"));
+ Shape ref = model.expectShape(ShapeId.from("com.foo#OtherReferenced"));
+ List relationships = provider.getNeighbors(shape).stream()
+ .filter(relationship -> relationship.getRelationshipType().equals(RelationshipType.ID_REF))
+ .collect(Collectors.toList());
+ Shape shape1 = model.expectShape(ShapeId.from("com.foo#refStruct$other"));
+ Shape ref1 = model.expectShape(ShapeId.from("com.foo#ReferencedInTraitDef"));
+ List relationships1 = provider.getNeighbors(shape1).stream()
+ .filter(relationship -> relationship.getRelationshipType().equals(RelationshipType.ID_REF))
+ .collect(Collectors.toList());
+
+ assertThat(relationships, containsInAnyOrder(Relationship.create(shape, RelationshipType.ID_REF, ref)));
+ assertThat(relationships1, containsInAnyOrder(Relationship.create(shape1, RelationshipType.ID_REF, ref1)));
+ }
+
+ @Test
+ public void canGetIdRefRelationshipsThroughMultipleLevelsOfIdRef() {
+ Model model = Model.assembler()
+ .addImport(getClass().getResource("idref-neighbors-multiple-levels.smithy"))
+ .assemble()
+ .unwrap();
+ NeighborProvider provider = NeighborProvider.of(model);
+ provider = NeighborProvider.withIdRefRelationships(model, provider);
+
+ Shape shape = model.expectShape(ShapeId.from("com.foo#WithIdRef"));
+ Shape ref = model.expectShape(ShapeId.from("com.foo#Referenced"));
+ List relationships = provider.getNeighbors(shape).stream()
+ .filter(relationship -> relationship.getRelationshipType().equals(RelationshipType.ID_REF))
+ .collect(Collectors.toList());
+ Shape shape1 = model.expectShape(ShapeId.from("com.foo#ConnectedThroughReferenced"));
+ Shape ref1 = model.expectShape(ShapeId.from("com.foo#AnotherReferenced"));
+ List relationships1 = provider.getNeighbors(shape1).stream()
+ .filter(relationship -> relationship.getRelationshipType().equals(RelationshipType.ID_REF))
+ .collect(Collectors.toList());
+
+ assertThat(relationships, containsInAnyOrder(Relationship.create(shape, RelationshipType.ID_REF, ref)));
+ assertThat(relationships1, containsInAnyOrder(Relationship.create(shape1, RelationshipType.ID_REF, ref1)));
+ }
}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NodeQueryTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NodeQueryTest.java
new file mode 100644
index 00000000000..51ab639251f
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NodeQueryTest.java
@@ -0,0 +1,183 @@
+package software.amazon.smithy.model.neighbor;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasSize;
+
+import java.util.Collection;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.StringNode;
+
+public class NodeQueryTest {
+ @Test
+ public void noQueriesGivesNoResults() {
+ Node node = Node.from("{}");
+
+ Collection result = new NodeQuery().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void self() {
+ Node node = Node.from("{}");
+
+ Collection result = new NodeQuery().self().execute(node);
+
+ assertThat(result, containsInAnyOrder(node));
+ }
+
+ @Test
+ public void selfCanBeAppliedMultipleTimes() {
+ Node node = Node.from("{}");
+
+ Collection result = new NodeQuery().self().self().self().execute(node);
+
+ assertThat(result, containsInAnyOrder(node));
+ }
+
+ @Test
+ public void member() {
+ Node member = StringNode.from("bar");
+ Node node = Node.objectNode().withMember("foo", member);
+
+ Collection result = new NodeQuery().member("foo").execute(node);
+
+ assertThat(result, containsInAnyOrder(member));
+ }
+
+ @Test
+ public void anyMember() {
+ Node member1 = StringNode.from("member-one");
+ Node member2 = StringNode.from("member-two");
+ Node node = Node.objectNode().withMember("one", member1).withMember("two", member2);
+
+ Collection result = new NodeQuery().anyMember().execute(node);
+
+ assertThat(result, containsInAnyOrder(member1, member2));
+ }
+
+ @Test
+ public void anyElement() {
+ Node element1 = StringNode.from("element-one");
+ Node element2 = StringNode.from("element-two");
+ Node node = Node.arrayNode(element1, element2);
+
+ Collection result = new NodeQuery().anyElement().execute(node);
+
+ assertThat(result, containsInAnyOrder(element1, element2));
+ }
+
+ @Test
+ public void anyMemberName() {
+ StringNode key1 = StringNode.from("one");
+ StringNode key2 = StringNode.from("two");
+ Node member1 = StringNode.from("member-one");
+ Node member2 = StringNode.from("member-two");
+ Node node = Node.objectNode().withMember(key1, member1).withMember(key2, member2);
+
+ Collection result = new NodeQuery().anyMemberName().execute(node);
+
+ assertThat(result, containsInAnyOrder(key1, key2));
+ }
+
+ @Test
+ public void memberGivesNoResultsOnNonObjectNode() {
+ Node node = Node.from("[{\"foo\": 0}]");
+
+ Collection result = new NodeQuery().member("foo").execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void memberGivesNoResultsIfMemberNameNotFound() {
+ Node node = Node.from("{\"a\": 0, \"b\": 0}");
+
+ Collection result = new NodeQuery().member("foo").execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void anyMemberGivesNoResultsOnNonObjectNode() {
+ Node node = Node.from("[{\"foo\": 0}]");
+
+ Collection result = new NodeQuery().anyMember().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void anyMemberGivesNoResultsOnEmptyObjectNode() {
+ Node node = Node.from("{}");
+
+ Collection result = new NodeQuery().anyMember().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void anyElementGivesNoResultsOnNonArrayNode() {
+ Node node = Node.from("{\"foo\": [0]}");
+
+ Collection result = new NodeQuery().anyElement().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void anyElementGivesNoResultsOnEmptyArrayNode() {
+ Node node = Node.from("[]");
+
+ Collection result = new NodeQuery().anyElement().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void anyMemberNameGivesNoResultsOnNonObjectNode() {
+ Node node = Node.from("1");
+
+ Collection result = new NodeQuery().anyMemberName().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void anyMemberNameGivesNoResultsOnEmptyObject() {
+ Node node = Node.from("{}");
+
+ Collection result = new NodeQuery().anyMemberName().execute(node);
+
+ assertThat(result, hasSize(0));
+ }
+
+ @Test
+ public void eachQueryExecuteOnResultOfPreviousQuery() {
+ Node element1 = Node.from(0);
+ Node element2 = Node.from("{}");
+ Node element3 = Node.from("element3");
+ Node obj = Node.objectNode().withMember("foo", Node.objectNode()
+ .withMember("arr1", Node.arrayNode(element1))
+ .withMember("arr2", Node.arrayNode(element2))
+ .withMember("arr3", Node.arrayNode(element3)));
+ Node node = Node.arrayNode(obj, obj);
+
+ Collection result = new NodeQuery()
+ .anyElement()
+ .member("foo")
+ .anyMember()
+ .anyElement()
+ .execute(node);
+
+ assertThat(result, containsInAnyOrder(
+ element1,
+ element2,
+ element3,
+ element1,
+ element2,
+ element3));
+ }
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java
index 41e4d75897f..7164cedd623 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java
@@ -80,4 +80,32 @@ private Model createPrivateShapeModel(ShapeId id) {
.assemble()
.unwrap();
}
+
+ @Test
+ public void checksShapeReferencesThroughIdRef() {
+ Model m = Model.assembler()
+ .addImport(getClass().getResource("idref-neighbors.smithy"))
+ .assemble()
+ .unwrap();
+
+ Set shapes = new UnreferencedShapes().compute(m);
+
+ assertThat(shapes, empty());
+ }
+
+ @Test
+ public void doesNotCheckShapeReferencesThroughIdRefOnUnconnectedShapes() {
+ Model m = Model.assembler()
+ .addImport(getClass().getResource("idref-neighbors-unconnected.smithy"))
+ .assemble()
+ .unwrap();
+
+ Set ids = new UnreferencedShapes().compute(m).stream().map(Shape::getId).collect(Collectors.toSet());
+
+ assertThat(ids, containsInAnyOrder(
+ ShapeId.from("com.foo#WithTrait"),
+ ShapeId.from("com.foo#Referenced"),
+ ShapeId.from("com.foo#Unconnected")
+ ));
+ }
}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-in-trait-def.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-in-trait-def.smithy
new file mode 100644
index 00000000000..59c2303047f
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-in-trait-def.smithy
@@ -0,0 +1,34 @@
+$version: "2.0"
+
+namespace com.foo
+
+service FooService {
+ version: "2024-01-22"
+ operations: [GetFoo]
+}
+
+operation GetFoo {
+ input := {
+ withRefStructTrait: WithRefStructTrait
+ }
+}
+
+@trait
+@idRef(failWhenMissing: true)
+string ref
+
+@trait
+structure refStruct {
+ @ref(ReferencedInTraitDef)
+ other: String
+
+ @idRef(failWhenMissing: true)
+ ref: String
+}
+
+string ReferencedInTraitDef
+
+@refStruct(other: "foo", ref: OtherReferenced)
+structure WithRefStructTrait {}
+
+string OtherReferenced
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-multiple-levels.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-multiple-levels.smithy
new file mode 100644
index 00000000000..211cf736d59
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-multiple-levels.smithy
@@ -0,0 +1,32 @@
+$version: "2.0"
+
+namespace com.foo
+
+service FooService {
+ version: "2024-01-22"
+ operations: [GetFoo]
+}
+
+operation GetFoo {
+ input := {
+ withIdRef: WithIdRef
+ }
+}
+
+@trait
+@idRef(failWhenMissing: true)
+string ref
+
+@ref(Referenced)
+structure WithIdRef {}
+
+structure Referenced {
+ connectedThroughReferenced: ConnectedThroughReferenced
+}
+
+// Only connected through `Referenced`, which itself is only
+// connected via idRef.
+@ref(AnotherReferenced)
+structure ConnectedThroughReferenced {}
+
+string AnotherReferenced
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-unconnected.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-unconnected.smithy
new file mode 100644
index 00000000000..1d1e2443427
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors-unconnected.smithy
@@ -0,0 +1,32 @@
+$version: "2.0"
+
+namespace com.foo
+
+service FooService {
+ version: "2024-01-22"
+ operations: [GetFoo]
+}
+
+operation GetFoo {
+ input := {
+ withReferencedByUnconnected: WithReferencedByUnconnected
+ }
+}
+
+@trait
+@idRef(failWhenMissing: true)
+string ref
+
+@ref(Referenced)
+structure WithTrait {}
+
+structure Referenced {}
+
+@ref(ReferencedByUnconnected)
+structure WithReferencedByUnconnected {}
+
+string ReferencedByUnconnected
+
+structure Unconnected {
+ ref: ReferencedByUnconnected
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors.smithy
new file mode 100644
index 00000000000..ab45d60fd9d
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/idref-neighbors.smithy
@@ -0,0 +1,236 @@
+$version: "2.0"
+
+namespace com.foo
+
+service FooService {
+ version: "2024-01-18"
+ operations: [GetFoo]
+}
+
+operation GetFoo {
+ input := {
+ one: One
+ two: Two
+ three: Three
+ four: Four
+ five: Five
+ six: Six
+ seven: Seven
+ eight: Eight
+ nine: Nine
+ ten: Ten
+ eleven: Eleven
+ twelve: Twelve
+ thirteen: Thirteen
+ fourteen: Fourteen
+ fifteen: Fifteen
+ }
+}
+
+// --
+@trait
+structure withIdRefOnMember {
+ @idRef(failWhenMissing: true)
+ ref: String
+}
+
+@withIdRefOnMember(ref: Ref1)
+structure One {}
+
+structure Ref1 {}
+
+// --
+@trait
+structure withIdRefOnMemberTarget {
+ ref: OnTarget
+}
+
+@idRef(failWhenMissing: true)
+string OnTarget
+
+@withIdRefOnMemberTarget(ref: Ref2)
+structure Two {}
+
+structure Ref2 {}
+
+// --
+@trait
+structure withIdRefOnNestedStructureMember {
+ struct: Nested
+}
+
+structure Nested {
+ @idRef(failWhenMissing: true)
+ member: String
+}
+
+@withIdRefOnNestedStructureMember(
+ struct: {
+ member: Ref3
+ }
+)
+structure Three {}
+
+structure Ref3 {}
+
+
+// --
+@trait
+list withIdRefOnListMemberTarget {
+ member: OnListMemberTarget
+}
+
+@idRef(failWhenMissing: true)
+string OnListMemberTarget
+
+@withIdRefOnListMemberTarget([
+ Ref4
+])
+structure Four {}
+
+structure Ref4 {}
+
+// --
+@trait
+@idRef(failWhenMissing: true)
+string withIdRefOnSelf
+
+@withIdRefOnSelf(Ref5)
+structure Five {}
+
+structure Ref5 {}
+
+// --
+@withIdRefOnSelf(Ref6)
+structure Six {}
+
+structure Ref6 {
+ ref: RefByRef6
+}
+
+structure RefByRef6 {}
+
+// --
+@trait
+list withIdRefOnNestedStruct {
+ member: Nested
+}
+
+@withIdRefOnNestedStruct([
+ {
+ member: Ref7
+ }
+])
+structure Seven {}
+
+structure Ref7 {}
+
+// --
+@trait
+structure withIdRefThroughMixin with [ThroughMixin] {}
+
+@mixin
+structure ThroughMixin {
+ @idRef(failWhenMissing: true)
+ ref: String
+}
+
+@withIdRefThroughMixin(ref: Ref8)
+structure Eight {}
+
+structure Ref8 {}
+
+// --
+@trait
+map withIdRefOnMapValue {
+ key: String
+ value: OnMap
+}
+
+@idRef(failWhenMissing: true)
+string OnMap
+
+@withIdRefOnMapValue({
+ foo: Ref9
+})
+structure Nine {}
+
+structure Ref9 {}
+
+// --
+@trait
+map withIdRefOnNestedMapValue {
+ key: String
+ value: Nested
+}
+
+@withIdRefOnNestedMapValue({
+ foo: {
+ member: Ref10
+ }
+})
+structure Ten {}
+
+structure Ref10 {}
+
+// --
+@trait
+map withIdRefOnMapKey {
+ key: OnMap
+ value: String
+}
+
+@withIdRefOnMapKey({
+ "com.foo#Ref11": "foo"
+})
+structure Eleven {}
+
+structure Ref11 {}
+
+// --
+@trait
+@idRef(failWhenMissing: true)
+string ref
+
+@ref(Ref12)
+structure Twelve {}
+
+structure Ref12 {
+ connectedToRef13: ConnectedToRef13
+}
+
+structure ConnectedToRef13 {
+ ref13: Ref13
+}
+
+@ref(Ref13)
+structure Thirteen {}
+
+structure Ref13 {
+ connectedToRef14: ConnectedToRef14
+}
+
+structure ConnectedToRef14 {
+ ref14: Ref14
+}
+
+@ref(Ref14)
+structure Fourteen {}
+
+string Ref14
+
+// --
+@trait
+structure withIdRefOnEnum {
+ refEnum: RefEnum
+}
+
+@idRef(failWhenMissing: true)
+enum RefEnum {
+ REF15 = "com.foo#Ref15"
+}
+
+@withIdRefOnEnum(refEnum: "com.foo#Ref15")
+structure Fifteen {}
+
+structure Ref15 {}