Repository: smithy-lang/smithy
Pull request #2358 (2024-07-26 00:17:06)

Add validator to warn on ignored idempotency token trait
#### Background
* What do these changes do? 

Added a validator to warn on ignored use of @idempotencyToken trait. As per [spec](https://smithy.io/2.0/spec/behavior-traits.html#idempotencytoken-trait), 

> Only a single member of the input of an operation can be targeted by the idempotencyToken trait; only top-level structure members of the input of an operation are considered.

This validator warns when we find a member using the trait in a context where it will be ignored. We cannot ERROR here as the structures might be reused in valid and invalid context, thus we only warning about the context where it will be ignored.

* Why are they important?

#### Testing
* How did you test these changes?

#### Links
* Links to additional context, if necessary
* Issue #, if applicable (see [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for a list of keywords to use for linking issues)

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/docs/Makefile b/docs/Makefile
index 0f1ddfa97..1401abd9f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -4,9 +4,10 @@ SPHINXBUILD   = python3 -msphinx
 SPHINXPROJ    = Smithy
 SHELL         := /bin/bash
 
-install:
+install: requirements.txt
 	python3 -m venv build/venv
 	source build/venv/bin/activate && pip3 install -r requirements.txt . && pip3 install -e .
+	touch install
 
 clean:
 	-rm -rf build/*
@@ -28,4 +29,7 @@ merge-versions:
 openhtml:
 	open "build/html/index.html"
 
+dev: install
+	@source build/venv/bin/activate && sphinx-autobuild "source-2.0" "build/2.0" $(SPHINXOPTS) $(O)
+
 .PHONY: Makefile clean
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 9e7b654c0..e50794f67 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -10,3 +10,4 @@ Sphinx-Substitution-Extensions==2022.2.16
 sphinx-tabs>=3.4.4
 
 sphinx_copybutton==0.5.0
+sphinx-autobuild>=2024.4.16
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/IdempotencyTokenIgnoredValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/IdempotencyTokenIgnoredValidator.java
new file mode 100644
index 000000000..5adf13fe0
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/IdempotencyTokenIgnoredValidator.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.validation.validators;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
+import software.amazon.smithy.model.neighbor.NeighborProvider;
+import software.amazon.smithy.model.neighbor.Relationship;
+import software.amazon.smithy.model.neighbor.RelationshipType;
+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.shapes.StructureShape;
+import software.amazon.smithy.model.traits.IdempotencyTokenTrait;
+import software.amazon.smithy.model.traits.MixinTrait;
+import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+
+/**
+ * Emits warnings when a structure member has an idempotency token trait that will be ignored.
+ */
+public final class IdempotencyTokenIgnoredValidator extends AbstractValidator {
+
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        if (!model.getAppliedTraits().contains(IdempotencyTokenTrait.ID)) {
+            return Collections.emptyList();
+        }
+        List<ValidationEvent> events = new ArrayList<>();
+        NeighborProvider reverse = NeighborProviderIndex.of(model).getReverseProvider();
+        for (MemberShape memberShape : model.getMemberShapesWithTrait(IdempotencyTokenTrait.class)) {
+            Shape container = model.expectShape(memberShape.getContainer());
+            // Skip non-structures (invalid) and mixins (handled at mixed site).
+            if (!container.isStructureShape() || container.hasTrait(MixinTrait.class)) {
+                continue;
+            }
+            Trait trait = memberShape.expectTrait(IdempotencyTokenTrait.class);
+            checkRelationships(container.asStructureShape().get(), memberShape, trait, reverse, events);
+        }
+        return events;
+    }
+
+    private void checkRelationships(
+        StructureShape containerShape,
+        MemberShape memberShape,
+        Trait trait,
+        NeighborProvider reverse,
+        List<ValidationEvent> events
+    ) {
+
+        // Store relationships so we can emit one event per ignored binding.
+        Map<RelationshipType, List<ShapeId>> ignoredRelationships = new TreeMap<>();
+        List<Relationship> relationships = reverse.getNeighbors(containerShape);
+        for (Relationship relationship : relationships) {
+            // Skip members of the container.
+            if (relationship.getRelationshipType() == RelationshipType.MEMBER_CONTAINER) {
+                continue;
+            }
+            if (relationship.getRelationshipType() != RelationshipType.INPUT) {
+                ignoredRelationships.computeIfAbsent(relationship.getRelationshipType(), x -> new ArrayList<>())
+                                    .add(relationship.getShape().getId());
+            }
+        }
+
+        // If we detected invalid relationships, build the right event message.
+        if (!ignoredRelationships.isEmpty()) {
+            events.add(emit(memberShape, trait, ignoredRelationships));
+        }
+    }
+
+    private ValidationEvent emit(
+        MemberShape memberShape,
+        Trait trait,
+        Map<RelationshipType, List<ShapeId>> ignoredRelationships
+    ) {
+        String message =
+            "The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, "
+            + "but it was applied and ignored in the following contexts: "
+            + formatIgnoredRelationships(ignoredRelationships);
+        return warning(memberShape, trait, message);
+    }
+
+    private String formatIgnoredRelationships(Map<RelationshipType, List<ShapeId>> ignoredRelationships) {
+        List<String> relationshipTypeBindings = new ArrayList<>();
+        for (Map.Entry<RelationshipType, List<ShapeId>> ignoredRelationshipType : ignoredRelationships.entrySet()) {
+            StringBuilder buf = new StringBuilder(ignoredRelationshipType.getKey().toString()
+                                                                         .toLowerCase(Locale.US)
+                                                                         .replace("_", " "));
+            Set<String> bindings = ignoredRelationshipType.getValue().stream()
+                                                          .map(ShapeId::toString)
+                                                          .collect(Collectors.toCollection(TreeSet::new));
+            buf.append(": [").append(String.join(", ", bindings)).append("]");
+            relationshipTypeBindings.add(buf.toString());
+        }
+        return "{" + String.join(", ", relationshipTypeBindings) + "}";
+    }
+}
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index b70dbef6a..2c4af4288 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -22,6 +22,7 @@ software.amazon.smithy.model.validation.validators.HttpResponseCodeSemanticsVali
 software.amazon.smithy.model.validation.validators.HttpUriGreedyLabelValidator
 software.amazon.smithy.model.validation.validators.HttpUriConflictValidator
 software.amazon.smithy.model.validation.validators.HttpUriFormatValidator
+software.amazon.smithy.model.validation.validators.IdempotencyTokenIgnoredValidator
 software.amazon.smithy.model.validation.validators.JsonNameValidator
 software.amazon.smithy.model.validation.validators.LengthTraitValidator
 software.amazon.smithy.model.validation.validators.MediaTypeValidator


Test patch:
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/idempotency-token-trait-ignored.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/idempotency-token-trait-ignored.errors
new file mode 100644
index 000000000..d8401773a
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/idempotency-token-trait-ignored.errors
@@ -0,0 +1,3 @@
+[WARNING] io.smithy.example#StructureOne$stringMember: The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, but it was applied and ignored in the following contexts: {member target: [io.smithy.example#StructureTwo$structureOne]} | IdempotencyTokenIgnored
+[WARNING] io.smithy.example#StructureThree$stringMember: The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, but it was applied and ignored in the following contexts: {output: [io.smithy.example#OperationFour]} | IdempotencyTokenIgnored
+
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/idempotency-token-trait-ignored.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/idempotency-token-trait-ignored.smithy
new file mode 100644
index 000000000..dff5b778f
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/idempotency-token-trait-ignored.smithy
@@ -0,0 +1,62 @@
+$version: "2"
+
+namespace io.smithy.example
+
+service SmithyExample {
+    operations: [
+        OperationOne
+        OperationTwo
+        OperationThree
+        OperationFour
+    ]
+}
+
+operation OperationOne {
+    input := {
+        @idempotencyToken
+        stringMember: String
+        integerMember: Integer
+    }
+    output: Unit
+}
+
+operation OperationTwo {
+    input: StructureOne
+    output: Unit
+}
+
+operation OperationThree {
+    input: StructureTwo
+    output: Unit
+}
+
+operation OperationFour {
+    input: StructureThree
+    output: StructureThree
+}
+
+
+structure StructureOne {
+    @idempotencyToken
+    stringMember: String
+    integerMember: Integer
+}
+
+
+structure StructureTwo {
+    stringMember: String
+    structureOne: StructureOne
+}
+
+
+@mixin
+structure StructureMixin {
+    @idempotencyToken
+    stringMember: String
+}
+
+
+structure StructureThree with [StructureMixin] {
+    integerMember: Integer
+}
+