Repository: smithy-lang/smithy (java)

Issue:
Does the `ChangedMemberOrder` validation in `smithy diff` apply to anything other than C or C++ ?
I have created a `smithy diff` step in my release system to check for breaking changes. I started getting `ChangedMemberOrder` errors with this message

> union shape members were reordered. This can cause ABI compatibility issues in languages like C and C++ where the layout and alignment of a data structure matters.

## Questions

1. Does this validation apply to anything other than C or C++? I'm most interested in Javascript, Python, Java, and Rust. We don't use C or C++.
2. Is there a way to suppress specific errors codes from `smithy diff`? I don't see anything in `smithy diff --help` which I can use. As long as my languages aren't impacted by `ChangedMemberOrder`, I'd like to be able to do something like `smithy diff --suppress ChangedMemberOrder`

Pull request:
Add support for suppressions to smithy-diff
Closes #1861

*Issue #, if available:*

*Description of changes:*


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/smithy-diff/README.md b/smithy-diff/README.md
index 98c86a7c9..4ad5dd42b 100644
--- a/smithy-diff/README.md
+++ b/smithy-diff/README.md
@@ -14,6 +14,9 @@ Model modelB = loadModelB();
 List<ValidationEvent> events = ModelDiff.compare(modelA, modelB);
 ```
 
+Smithy Diff will load and apply metadata suppressions and severityOverrides
+from the new model to any emitted diff events. 
+
 # Adding a custom DiffEvaluator
 
 This library finds all instances of `DiffEvaluator`
diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java
index 3e18a774d..cb5c6b881 100644
--- a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java
+++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java
@@ -27,6 +27,8 @@ import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidatedResult;
 import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventDecorator;
+import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator;
 import software.amazon.smithy.utils.SmithyBuilder;
 
 /**
@@ -280,8 +282,17 @@ public final class ModelDiff {
             List<DiffEvaluator> evaluators = new ArrayList<>();
             ServiceLoader.load(DiffEvaluator.class, classLoader).forEach(evaluators::add);
             Differences differences = Differences.detect(oldModel, newModel);
+
+            // Applies suppressions and elevates event severities.
+            ValidationEventDecorator decoratorResult = new ModelBasedEventDecorator()
+                    .createDecorator(newModel)
+                    .getResult()
+                    .orElse(ValidationEventDecorator.IDENTITY);
+
             List<ValidationEvent> diffEvents = evaluators.parallelStream()
                     .flatMap(evaluator -> evaluator.evaluate(differences).stream())
+                    // No need to call canDecorate first since that method will always return true in any code path.
+                    .map(decoratorResult::decorate)
                     .collect(Collectors.toList());
 
             return new Result(differences, diffEvents, oldModelEvents, newModelEvents);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java
index af9284f52..85edc2c20 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java
@@ -24,22 +24,18 @@ import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import software.amazon.smithy.model.Model;
-import software.amazon.smithy.model.SourceException;
 import software.amazon.smithy.model.SourceLocation;
-import software.amazon.smithy.model.node.ObjectNode;
-import software.amazon.smithy.model.shapes.Shape;
-import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.traits.SuppressTrait;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidatedResult;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.model.validation.ValidationEventDecorator;
 import software.amazon.smithy.model.validation.Validator;
 import software.amazon.smithy.model.validation.ValidatorFactory;
+import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator;
 import software.amazon.smithy.model.validation.suppressions.SeverityOverride;
-import software.amazon.smithy.model.validation.suppressions.Suppression;
 import software.amazon.smithy.model.validation.validators.ResourceCycleValidator;
 import software.amazon.smithy.model.validation.validators.TargetValidator;
+import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SetUtils;
 import software.amazon.smithy.utils.SmithyBuilder;
 
@@ -65,9 +61,6 @@ import software.amazon.smithy.utils.SmithyBuilder;
  */
 final class ModelValidator implements Validator {
 
-    private static final String SUPPRESSIONS = "suppressions";
-    private static final String SEVERITY_OVERRIDES = "severityOverrides";
-
     // Lazy initialization holder class idiom to hold a default validator factory.
     private static final class LazyValidatorFactoryHolder {
         static final ValidatorFactory INSTANCE = ValidatorFactory.createServiceFactory(
@@ -194,7 +187,6 @@ final class ModelValidator implements Validator {
     private static final class LoadedModelValidator {
 
         private final Model model;
-        private final List<Suppression> suppressions = new ArrayList<>();
         private final List<SeverityOverride> severityOverrides;
         private final List<Validator> validators;
         private final List<ValidationEvent> events = new ArrayList<>();
@@ -203,56 +195,38 @@ final class ModelValidator implements Validator {
 
         private LoadedModelValidator(Model model, ModelValidator validator) {
             this.model = model;
-            this.validationEventDecorator = validator.validationEventDecorator;
             this.eventListener = validator.eventListener;
             this.severityOverrides = new ArrayList<>(validator.severityOverrides);
             this.validators = new ArrayList<>(validator.validators);
 
-            loadMetadataSuppressions();
-            loadMetadataSeverityOverrides();
+            // Suppressing and elevating events is handled by composing a given decorator with a
+            // ModelBasedEventDecorator.
+            ModelBasedEventDecorator modelBasedEventDecorator = new ModelBasedEventDecorator();
+            modelBasedEventDecorator.severityOverrides(validator.severityOverrides);
+            ValidatedResult<ValidationEventDecorator> result = modelBasedEventDecorator.createDecorator(model);
+            this.validationEventDecorator = result.getResult()
+                    .map(decorator -> ValidationEventDecorator.compose(
+                            ListUtils.of(decorator, validator.validationEventDecorator)))
+                    .orElse(validator.validationEventDecorator);
+
+            // Events encountered while loading suppressions and overrides have been modified by everything the
+            // modelBasedEventDecorator knows about, but has not been modified by any custom decorator (if any).
+            for (ValidationEvent event : result.getValidationEvents()) {
+                if (validationEventDecorator.canDecorate(event)) {
+                    event = validationEventDecorator.decorate(event);
+                }
+                events.add(event);
+            }
 
-            // Given events have already been emitted and decorated, but have not been suppressed/elevated.
+            // Now that the decorator is available, emit/decorate/suppress/collect explicitly provided events.
             for (ValidationEvent event : validator.events) {
-                events.add(modifyEventSeverity(event));
+                pushEvent(event);
             }
 
+            // The decorator itself doesn't handle loading and applying validators, just modifying events.
             loadModelValidators(validator.validatorFactory);
         }
 
-        private void loadMetadataSeverityOverrides() {
-            model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> {
-                try {
-                    List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
-                    for (ObjectNode rule : values) {
-                        try {
-                            severityOverrides.add(SeverityOverride.fromMetadata(rule));
-                        } catch (SourceException e) {
-                            pushEvent(ValidationEvent.fromSourceException(e));
-                        }
-                    }
-                } catch (SourceException e) {
-                    pushEvent(ValidationEvent.fromSourceException(e));
-                }
-            });
-        }
-
-        private void loadMetadataSuppressions() {
-            model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> {
-                try {
-                    List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
-                    for (ObjectNode rule : values) {
-                        try {
-                            suppressions.add(Suppression.fromMetadata(rule));
-                        } catch (SourceException e) {
-                            pushEvent(ValidationEvent.fromSourceException(e));
-                        }
-                    }
-                } catch (SourceException e) {
-                    pushEvent(ValidationEvent.fromSourceException(e));
-                }
-            });
-        }
-
         private void loadModelValidators(ValidatorFactory validatorFactory) {
             // Load validators defined in metadata.
             ValidatedResult<List<ValidatorDefinition>> loaded = ValidationLoader
@@ -291,8 +265,9 @@ final class ModelValidator implements Validator {
         }
 
         private void pushEvent(ValidationEvent event) {
-            event = modifyEventSeverity(event);
-            event = validationEventDecorator.decorate(event);
+            if (validationEventDecorator.canDecorate(event)) {
+                event = validationEventDecorator.decorate(event);
+            }
             events.add(event);
             eventListener.accept(event);
         }
@@ -310,8 +285,7 @@ final class ModelValidator implements Validator {
             List<ValidationEvent> result = validators.parallelStream()
                     .flatMap(validator -> validator.validate(model).stream())
                     .filter(this::filterPrelude)
-                    .map(this::modifyEventSeverity)
-                    .map(validationEventDecorator::decorate)
+                    .map(e -> validationEventDecorator.canDecorate(e) ? validationEventDecorator.decorate(e) : e)
                     // Emit events as they occur during validation.
                     .peek(eventListener)
                     .collect(Collectors.toList());
@@ -330,52 +304,5 @@ final class ModelValidator implements Validator {
                     .filter(Prelude::isPreludeShape)
                     .isPresent();
         }
-
-        private ValidationEvent modifyEventSeverity(ValidationEvent event) {
-            // Use a suppress trait if present.
-            if (event.getShapeId().isPresent()) {
-                ShapeId target = event.getShapeId().get();
-                Shape shape = model.getShape(target).orElse(null);
-                if (shape != null) {
-                    if (shape.hasTrait(SuppressTrait.class)) {
-                        Suppression suppression = Suppression.fromSuppressTrait(shape);
-                        if (suppression.test(event)) {
-                            return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
-                        }
-                    }
-                }
-            }
-
-            // Check metadata and manual suppressions.
-            for (Suppression suppression : suppressions) {
-                if (suppression.test(event)) {
-                    return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
-                }
-            }
-
-            Severity appliedSeverity = event.getSeverity();
-            for (SeverityOverride override : severityOverrides) {
-                Severity overrideResult = override.apply(event);
-                if (overrideResult.ordinal() > appliedSeverity.ordinal()) {
-                    appliedSeverity = overrideResult;
-                }
-            }
-
-            return changeSeverity(event, appliedSeverity, null);
-        }
-
-        private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) {
-            if (event.getSeverity() == severity) {
-                return event;
-            } else {
-                // The event was suppressed so change the severity and reason.
-                ValidationEvent.Builder builder = event.toBuilder();
-                builder.severity(severity);
-                if (reason != null) {
-                    builder.suppressionReason(reason);
-                }
-                return builder.build();
-            }
-        }
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java
index 8cfba31d1..7090ea17f 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java
@@ -24,6 +24,20 @@ import java.util.List;
  * relevant only to the context where Smithy is being used.
  */
 public interface ValidationEventDecorator {
+
+    /** A decorator that does nothing. */
+    ValidationEventDecorator IDENTITY = new ValidationEventDecorator() {
+        @Override
+        public boolean canDecorate(ValidationEvent ev) {
+            return false;
+        }
+
+        @Override
+        public ValidationEvent decorate(ValidationEvent ev) {
+            return ev;
+        }
+    };
+
     /**
      * Returns true if this decorator knows how to decorate this event, usually by looking at the event id.
      *
@@ -48,17 +62,7 @@ public interface ValidationEventDecorator {
      */
     static ValidationEventDecorator compose(List<ValidationEventDecorator> decorators) {
         if (decorators.isEmpty()) {
-            return new ValidationEventDecorator() {
-                @Override
-                public boolean canDecorate(ValidationEvent ev) {
-                    return false;
-                }
-
-                @Override
-                public ValidationEvent decorate(ValidationEvent ev) {
-                    return ev;
-                }
-            };
+            return IDENTITY;
         } else {
             return new ValidationEventDecorator() {
                 @Override
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java
new file mode 100644
index 000000000..a90ce944c
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecorator.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 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.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.model.validation.suppressions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.SourceException;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.SuppressTrait;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidatedResult;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventDecorator;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+
+/**
+ * Creates a {@link ValidationEventDecorator} that applies custom suppressions, custom severity overrides,
+ * suppressions parsed from model metadata, and severity overrides parsed from model metadata.
+ */
+@SmithyUnstableApi
+public final class ModelBasedEventDecorator {
+
+    private static final String SUPPRESSIONS = "suppressions";
+    private static final String SEVERITY_OVERRIDES = "severityOverrides";
+
+    private final List<Suppression> suppressions = new ArrayList<>();
+    private final List<SeverityOverride> severityOverrides = new ArrayList<>();
+
+    /**
+     * Sets custom suppressions not found in the model.
+     *
+     * @param suppressions Suppressions to set.
+     * @return Returns the ModelBasedEventDecorator.
+     */
+    public ModelBasedEventDecorator suppressions(Collection<? extends Suppression> suppressions) {
+        this.suppressions.clear();
+        this.suppressions.addAll(suppressions);
+        return this;
+    }
+
+    /**
+     * Sets custom severity overrides not found in the model.
+     *
+     * @param severityOverrides Severity overrides to set.
+     * @return Returns the ModelBasedEventDecorator.
+     */
+    public ModelBasedEventDecorator severityOverrides(Collection<? extends SeverityOverride> severityOverrides) {
+        this.severityOverrides.clear();
+        this.severityOverrides.addAll(severityOverrides);
+        return this;
+    }
+
+    /**
+     * Creates a ValidationEventDecorator for the given Model.
+     *
+     * <p>Validators, suppressions, and severity overrides found in the model are used each time
+     * {@link ValidationEventDecorator#decorate(ValidationEvent)} is called. The
+     * {@link ValidationEventDecorator#canDecorate(ValidationEvent)} always returns true.
+     *
+     * @param model Model to load validation events from.
+     * @return Returns a decorator that can be used to modify the severity and suppression reason of each given event.
+     */
+    public ValidatedResult<ValidationEventDecorator> createDecorator(Model model) {
+        // Create dedicated arrays to separate the state of the created decorator from the builder.
+        List<ValidationEvent> events = new ArrayList<>();
+        List<Suppression> loadedSuppressions = new ArrayList<>(suppressions);
+        loadMetadataSuppressions(model, loadedSuppressions, events);
+        List<SeverityOverride> loadedSeverityOverrides = new ArrayList<>(severityOverrides);
+        loadMetadataSeverityOverrides(model, loadedSeverityOverrides, events);
+
+        // Modify severities and overrides of each encountered event.
+        for (int i = 0; i < events.size(); i++) {
+            events.set(i, modifyEventSeverity(model, events.get(i), loadedSuppressions, loadedSeverityOverrides));
+        }
+
+        return new ValidatedResult<>(new ValidationEventDecorator() {
+            @Override
+            public boolean canDecorate(ValidationEvent ev) {
+                return true;
+            }
+
+            @Override
+            public ValidationEvent decorate(ValidationEvent ev) {
+                return modifyEventSeverity(model, ev, loadedSuppressions, loadedSeverityOverrides);
+            }
+        }, events);
+    }
+
+    private static void loadMetadataSeverityOverrides(
+            Model model,
+            List<SeverityOverride> severityOverrides,
+            List<ValidationEvent> events
+    ) {
+        model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> {
+            try {
+                List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
+                for (ObjectNode rule : values) {
+                    try {
+                        severityOverrides.add(SeverityOverride.fromMetadata(rule));
+                    } catch (SourceException e) {
+                        events.add(ValidationEvent.fromSourceException(e));
+                    }
+                }
+            } catch (SourceException e) {
+                events.add(ValidationEvent.fromSourceException(e));
+            }
+        });
+    }
+
+    private static void loadMetadataSuppressions(
+            Model model,
+            List<Suppression> suppressions,
+            List<ValidationEvent> events
+    ) {
+        model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> {
+            try {
+                List<ObjectNode> values = value.expectArrayNode().getElementsAs(ObjectNode.class);
+                for (ObjectNode rule : values) {
+                    try {
+                        suppressions.add(Suppression.fromMetadata(rule));
+                    } catch (SourceException e) {
+                        events.add(ValidationEvent.fromSourceException(e));
+                    }
+                }
+            } catch (SourceException e) {
+                events.add(ValidationEvent.fromSourceException(e));
+            }
+        });
+    }
+
+    private static ValidationEvent modifyEventSeverity(
+            Model model,
+            ValidationEvent event,
+            List<Suppression> suppressions,
+            List<SeverityOverride> severityOverrides
+    ) {
+        // Use a suppress trait if present.
+        if (event.getShapeId().isPresent()) {
+            ShapeId target = event.getShapeId().get();
+            Shape shape = model.getShape(target).orElse(null);
+            if (shape != null) {
+                if (shape.hasTrait(SuppressTrait.class)) {
+                    Suppression suppression = Suppression.fromSuppressTrait(shape);
+                    if (suppression.test(event)) {
+                        return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
+                    }
+                }
+            }
+        }
+
+        // Check metadata and manual suppressions.
+        for (Suppression suppression : suppressions) {
+            if (suppression.test(event)) {
+                return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null));
+            }
+        }
+
+        Severity appliedSeverity = event.getSeverity();
+        for (SeverityOverride override : severityOverrides) {
+            Severity overrideResult = override.apply(event);
+            if (overrideResult.ordinal() > appliedSeverity.ordinal()) {
+                appliedSeverity = overrideResult;
+            }
+        }
+
+        return changeSeverity(event, appliedSeverity, null);
+    }
+
+    private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) {
+        if (event.getSeverity() == severity) {
+            return event;
+        } else {
+            // The event was suppressed so change the severity and reason.
+            ValidationEvent.Builder builder = event.toBuilder();
+            builder.severity(severity);
+            if (reason != null) {
+                builder.suppressionReason(reason);
+            }
+            return builder.build();
+        }
+    }
+}


Test patch:
diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java
index ab6b72d0f..cbc22a416 100644
--- a/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java
+++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/ModelDiffTest.java
@@ -94,4 +94,33 @@ public class ModelDiffTest {
 
         assertThat(result.isDiffBreaking(), is(false));
     }
+
+    @Test
+    public void appliesSuppressionsToDiff() {
+        Model oldModel = Model.assembler()
+                .addImport(getClass().getResource("suppressions-a.smithy"))
+                .assemble()
+                .unwrap();
+        Model newModel = Model.assembler()
+                .addImport(getClass().getResource("suppressions-b.smithy"))
+                .assemble()
+                .unwrap();
+
+        ModelDiff.Result result = ModelDiff.builder()
+                .oldModel(oldModel)
+                .newModel(newModel)
+                .compare();
+
+        assertThat(result.isDiffBreaking(), is(false));
+
+        boolean found = false;
+        for (ValidationEvent event : result.getDiffEvents()) {
+            if (event.getId().equals("ChangedMemberOrder")) {
+                assertThat(event.getSeverity(), equalTo(Severity.SUPPRESSED));
+                found = true;
+            }
+        }
+
+        assertThat(found, is(true));
+    }
 }
diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy
new file mode 100644
index 000000000..4095cadf7
--- /dev/null
+++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-a.smithy
@@ -0,0 +1,16 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        id: "ChangedMemberOrder"
+        namespace: "smithy.example"
+    }
+]
+
+namespace smithy.example
+
+structure Foo {
+    a: String
+    b: String
+    c: String
+}
diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy
new file mode 100644
index 000000000..b08378e4e
--- /dev/null
+++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/suppressions-b.smithy
@@ -0,0 +1,16 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        id: "ChangedMemberOrder"
+        namespace: "smithy.example"
+    }
+]
+
+namespace smithy.example
+
+structure Foo {
+    c: String
+    a: String
+    b: String
+}
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java
new file mode 100644
index 000000000..21243e69c
--- /dev/null
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/suppressions/ModelBasedEventDecoratorTest.java
@@ -0,0 +1,95 @@
+package software.amazon.smithy.model.validation.suppressions;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidatedResult;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventDecorator;
+import software.amazon.smithy.model.validation.Validator;
+import software.amazon.smithy.model.validation.ValidatorFactory;
+
+public class ModelBasedEventDecoratorTest {
+    @Test
+    public void erroneousSuppressionsEmitEvents() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(getClass().getResource("bad-suppressions.smithy"))
+                // Ensure that events found while loading the model are decorated too.
+                .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build()))
+                .assemble();
+
+        assertThat(result.getValidationEvents(Severity.ERROR), not(empty()));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(), containsString("member `id`"));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getHint(), equalTo(Optional.of("hi")));
+    }
+
+    @Test
+    public void erroneousOverridesEmitEvents() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(getClass().getResource("bad-severityOverrides.smithy"))
+                // Ensure that events found while loading the model are decorated too.
+                .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build()))
+                .assemble();
+
+        assertThat(result.getValidationEvents(Severity.ERROR), not(empty()));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getMessage(), containsString("member `id`"));
+        assertThat(result.getValidationEvents(Severity.ERROR).get(0).getHint(), equalTo(Optional.of("hi")));
+    }
+
+    @Test
+    public void loadsSuppressionsAndOverrides() {
+        ValidatedResult<Model> result = Model.assembler()
+                .addImport(Model.class.getResource(
+                        "errorfiles/validators/severityOverrides/suppressions-take-precedence.smithy"))
+                .validatorFactory(testFactory(event -> event.toBuilder().hint("hi").build()))
+                .assemble();
+
+        assertThat(result.getValidationEvents(Severity.ERROR), empty());
+        assertThat(result.getValidationEvents(), not(empty()));
+
+        // Every event should have the applied hint.
+        for (ValidationEvent event : result.getValidationEvents()) {
+            assertThat(event.getHint(), equalTo(Optional.of("hi")));
+        }
+    }
+
+    private static ValidatorFactory testFactory(Function<ValidationEvent, ValidationEvent> d) {
+        return new ValidatorFactory() {
+            @Override
+            public List<Validator> loadBuiltinValidators() {
+                return Collections.emptyList();
+            }
+
+            @Override
+            public Optional<Validator> createValidator(String name, ObjectNode configuration) {
+                return Optional.empty();
+            }
+
+            @Override
+            public List<ValidationEventDecorator> loadDecorators() {
+                return Collections.singletonList(new ValidationEventDecorator() {
+                    @Override
+                    public boolean canDecorate(ValidationEvent ev) {
+                        return true;
+                    }
+
+                    @Override
+                    public ValidationEvent decorate(ValidationEvent ev) {
+                        return d.apply(ev);
+                    }
+                });
+            }
+        };
+    }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy
new file mode 100644
index 000000000..ba198649d
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-severityOverrides.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+metadata severityOverrides = [
+    {
+        // missing id
+    }
+]
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy
new file mode 100644
index 000000000..a942f4dab
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/suppressions/bad-suppressions.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+metadata suppressions = [
+    {
+        // missing id
+    }
+]