newPath = new ArrayList<>(path.size() + 1);
- newPath.add(relationship);
- newPath.addAll(path);
- traverseUp(relationship.getShape(), newPath, newVisited);
+ if (filter.test(relationship)) {
+ queue.add(new Path(relationship, currentPath));
+ }
}
}
}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ShapeRecursionValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ShapeRecursionValidator.java
index 4f75f32ac8d..d125667aa21 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ShapeRecursionValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ShapeRecursionValidator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
@@ -15,29 +15,28 @@
package software.amazon.smithy.model.validation.validators;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.StringJoiner;
import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.selector.PathFinder;
import software.amazon.smithy.model.shapes.ListShape;
import software.amazon.smithy.model.shapes.MapShape;
-import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.SetShape;
import software.amazon.smithy.model.shapes.Shape;
-import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.shapes.ShapeVisitor;
+import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.utils.FunctionalUtils;
/**
* Ensures that list, set, and map shapes are not directly recursive,
* meaning that if they do have a recursive reference to themselves,
* one or more references that form the recursive path travels through
- * a structure or union shape.
+ * a structure or union shape. And ensures that structure members are
+ * not infinitely mutually recursive using the required trait.
*
* This check removes an entire class of problems from things like
* code generators where a list of itself or a list of maps of itself
@@ -47,82 +46,66 @@ public final class ShapeRecursionValidator extends AbstractValidator {
@Override
public List validate(Model model) {
- return model.shapes()
- .map(shape -> validateShape(model, shape))
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
+ PathFinder finder = PathFinder.create(model);
+ List events = new ArrayList<>();
+ validateListMapSetShapes(finder, model, events);
+ validateStructurePaths(finder, model, events);
+ return events;
}
- private ValidationEvent validateShape(Model model, Shape shape) {
- return new RecursiveNeighborVisitor(model, shape).visit(shape);
- }
-
- private final class RecursiveNeighborVisitor extends ShapeVisitor.Default {
+ private void validateListMapSetShapes(PathFinder finder, Model model, List events) {
+ finder.relationshipFilter(rel -> !(rel.getShape().isStructureShape() || rel.getShape().isUnionShape()));
- private final Model model;
- private final Shape root;
- private final Set visited = new HashSet<>();
- private final Deque context = new ArrayDeque<>();
-
- RecursiveNeighborVisitor(Model model, Shape root) {
- this.root = root;
- this.model = model;
+ for (ListShape shape : model.getListShapes()) {
+ validateListMapSetShapes(shape, finder, events);
}
- ValidationEvent visit(Shape shape) {
- ValidationEvent event = hasShapeBeenVisited(shape);
- return event != null ? event : shape.accept(this);
+ for (SetShape shape : model.getSetShapes()) {
+ validateListMapSetShapes(shape, finder, events);
}
- private ValidationEvent hasShapeBeenVisited(Shape shape) {
- if (!visited.contains(shape.getId())) {
- return null;
- }
-
- return error(shape, String.format(
- "Found invalid shape recursion: %s. A recursive list, set, or map shape is only valid if "
- + "an intermediate reference is through a union or structure.",
- String.join(" > ", context)));
+ for (MapShape shape : model.getMapShapes()) {
+ validateListMapSetShapes(shape, finder, events);
}
- @Override
- protected ValidationEvent getDefault(Shape shape) {
- return null;
- }
+ finder.relationshipFilter(FunctionalUtils.alwaysTrue());
+ }
- @Override
- public ValidationEvent listShape(ListShape shape) {
- return validateMember(shape, shape.getMember());
+ private void validateListMapSetShapes(Shape shape, PathFinder finder, List events) {
+ for (PathFinder.Path path : finder.search(shape, Collections.singletonList(shape))) {
+ events.add(error(shape, String.format(
+ "Found invalid shape recursion: %s. A recursive list, set, or map shape is only "
+ + "valid if an intermediate reference is through a union or structure.", formatPath(path))));
}
+ }
- @Override
- public ValidationEvent setShape(SetShape shape) {
- return validateMember(shape, shape.getMember());
- }
+ private void validateStructurePaths(PathFinder finder, Model model, List events) {
+ finder.relationshipFilter(rel -> {
+ if (rel.getShape().isStructureShape()) {
+ return rel.getNeighborShape().get().hasTrait(RequiredTrait.class);
+ } else {
+ return rel.getShape().isMemberShape();
+ }
+ });
- @Override
- public ValidationEvent mapShape(MapShape shape) {
- return validateMember(shape, shape.getValue());
+ for (StructureShape shape : model.getStructureShapes()) {
+ for (PathFinder.Path path : finder.search(shape, Collections.singletonList(shape))) {
+ events.add(error(shape, String.format(
+ "Found invalid shape recursion: %s. A structure cannot be mutually recursive through all "
+ + "required members.", formatPath(path))));
+ }
}
+ }
- private ValidationEvent validateMember(Shape container, MemberShape member) {
- ValidationEvent event = null;
- Shape target = model.getShape(member.getTarget()).orElse(null);
-
- if (target != null) {
- // Add to the visited set and the context deque before visiting,
- // the remove from them after done visiting this shape.
- visited.add(container.getId());
- // Eventually, this would look like: member-id > shape-id[ > member-id > shape-id [ > [...]]
- context.addLast(member.getId().toString());
- context.addLast(member.getTarget().toString());
- event = visit(target);
- context.removeLast();
- context.removeLast();
- visited.remove(container.getId());
+ private String formatPath(PathFinder.Path path) {
+ StringJoiner joiner = new StringJoiner(" > ");
+ List shapes = path.getShapes();
+ for (int i = 0; i < shapes.size(); i++) {
+ // Skip the first shape (the subject) to shorten the error message.
+ if (i > 0) {
+ joiner.add(shapes.get(i).getId().toString());
}
-
- return event;
}
+ return joiner.toString();
}
}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/shape-recursion-required-trait.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/shape-recursion-required-trait.errors
new file mode 100644
index 00000000000..8b37bb9ab7a
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/shape-recursion-required-trait.errors
@@ -0,0 +1,2 @@
+[ERROR] smithy.example#RecursiveShape1: Found invalid shape recursion: smithy.example#RecursiveShape1$recursiveMember > smithy.example#RecursiveShape2 > smithy.example#RecursiveShape2$recursiveMember > smithy.example#RecursiveShape1. A structure cannot be mutually recursive through all required members. | ShapeRecursion
+[ERROR] smithy.example#RecursiveShape2: Found invalid shape recursion: smithy.example#RecursiveShape2$recursiveMember > smithy.example#RecursiveShape1 > smithy.example#RecursiveShape1$recursiveMember > smithy.example#RecursiveShape2. A structure cannot be mutually recursive through all required members. | ShapeRecursion
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/shape-recursion-required-trait.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/shape-recursion-required-trait.smithy
new file mode 100644
index 00000000000..4173fca238d
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/shape-recursion-required-trait.smithy
@@ -0,0 +1,15 @@
+namespace smithy.example
+
+structure RecursiveShape1 {
+ @required
+ recursiveMember: RecursiveShape2
+}
+
+structure RecursiveShape2 {
+ // Bad
+ @required
+ recursiveMember: RecursiveShape1,
+
+ // Ok
+ recursiveMember2: RecursiveShape1
+}