Repository: smithy-lang/smithy
Pull request #2075 (2023-12-15 14:43:08)

Add backticks to diff messages for trait changes
*Description of changes:*
Adds backticks (or triple backticks) around trait values within diff event messages. This better supports markdown rendering of the messages.

Also changes pretty-printing to use platform-specific line separator through "%n"

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


rchache: Tests fail since I used System.lineSeparator() instead of "\n", but the testcases use "\n". Should I change my code to use "\n" or change the testcases to check for System.lineSeparator()?

Patch:
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/FlattenNamespaces.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/FlattenNamespaces.java
index a8c0a5e76..6c4637795 100644
--- a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/FlattenNamespaces.java
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/FlattenNamespaces.java
@@ -28,8 +28,8 @@ import software.amazon.smithy.model.neighbor.Walker;
 import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.transform.ModelTransformer;
 import software.amazon.smithy.utils.FunctionalUtils;
+import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.Pair;
 
 /**
@@ -120,9 +120,23 @@ public final class FlattenNamespaces extends ConfigurableProjectionTransformer<F
             throw new SmithyBuildException(
                     "'namespace' and 'service' properties must be set on flattenNamespace transformer.");
         }
+
         Model model = context.getModel();
+        if (!model.getShape(config.getService()).isPresent()) {
+            throw new SmithyBuildException("Configured service, " + config.getService()
+                    + ", not found in model when performing flattenNamespaces transform.");
+        }
+
         Map<ShapeId, ShapeId> shapesToRename = getRenamedShapes(config, model);
-        return ModelTransformer.create().renameShapes(model, shapesToRename);
+        model = context.getTransformer().renameShapes(model, shapesToRename);
+
+        // Remove service renames since they've now been applied, so consumers don't fail service shape validation
+        ShapeId updatedServiceId = shapesToRename.get(config.getService());
+        ServiceShape updatedService = model.expectShape(updatedServiceId, ServiceShape.class)
+                .toBuilder()
+                .clearRename()
+                .build();
+        return context.getTransformer().replaceShapes(model, ListUtils.of(updatedService));
     }
 
     @Override
@@ -131,11 +145,6 @@ public final class FlattenNamespaces extends ConfigurableProjectionTransformer<F
     }
 
     private Map<ShapeId, ShapeId> getRenamedShapes(Config config, Model model) {
-        if (!model.getShape(config.getService()).isPresent()) {
-            throw new SmithyBuildException("Configured service, " + config.getService()
-                    + ", not found in model when performing flattenNamespaces transform.");
-        }
-
         Map<ShapeId, ShapeId> shapesToRename = getRenamedShapesConnectedToService(config, model);
         Set<ShapeId> taggedShapesToInclude = getTaggedShapesToInclude(config.getIncludeTagged(), model);
 
diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ModifiedTrait.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ModifiedTrait.java
index fc51e84a0..ec41e2667 100644
--- a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ModifiedTrait.java
+++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ModifiedTrait.java
@@ -42,6 +42,7 @@ import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait;
 import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait;
 import software.amazon.smithy.model.validation.Severity;
 import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationUtils;
 import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SetUtils;
 import software.amazon.smithy.utils.StringUtils;
@@ -227,7 +228,7 @@ public final class ModifiedTrait extends AbstractDiffEvaluator {
                 }
 
                 String message;
-                String pretty = Node.prettyPrintJson(right.toNode());
+                String pretty = ValidationUtils.tickedPrettyPrintedNode(right);
                 if (path.isEmpty()) {
                     message = String.format("Added trait `%s` with value %s", trait, pretty);
                 } else {
@@ -260,7 +261,7 @@ public final class ModifiedTrait extends AbstractDiffEvaluator {
                     return Collections.emptyList();
                 }
 
-                String pretty = Node.prettyPrintJson(left.toNode());
+                String pretty = ValidationUtils.tickedPrettyPrintedNode(left);
                 String message;
                 if (path.isEmpty()) {
                     message = String.format("Removed trait `%s`. Previous trait value: %s", trait, pretty);
@@ -294,8 +295,8 @@ public final class ModifiedTrait extends AbstractDiffEvaluator {
                     return Collections.emptyList();
                 }
 
-                String leftPretty = Node.prettyPrintJson(left.toNode());
-                String rightPretty = Node.prettyPrintJson(right.toNode());
+                String leftPretty = ValidationUtils.tickedPrettyPrintedNode(left);
+                String rightPretty = ValidationUtils.tickedPrettyPrintedNode(right);
                 String message;
                 if (path.isEmpty()) {
                     message = String.format("Changed trait `%s` from %s to %s", trait, leftPretty, rightPretty);
diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/TraitBreakingChange.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/TraitBreakingChange.java
index aacf2296c..a082f0247 100644
--- a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/TraitBreakingChange.java
+++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/TraitBreakingChange.java
@@ -30,6 +30,7 @@ import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.traits.Trait;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationUtils;
 import software.amazon.smithy.utils.StringUtils;
 
 /**
@@ -214,15 +215,15 @@ public final class TraitBreakingChange extends AbstractDiffEvaluator {
         }
 
         private String createBreakingMessage(TraitDefinition.ChangeType type, String path, Node left, Node right) {
-            String leftPretty = Node.prettyPrintJson(left.toNode());
-            String rightPretty = Node.prettyPrintJson(right.toNode());
+            String leftPretty = ValidationUtils.tickedPrettyPrintedNode(left);
+            String rightPretty = ValidationUtils.tickedPrettyPrintedNode(right);
 
             switch (type) {
                 case ADD:
                     if (!path.isEmpty()) {
                         return String.format("Added trait contents to `%s` at path `%s` with value %s",
                                              trait.getId(), path, rightPretty);
-                    } else if (rightPretty.equals("{}")) {
+                    } else if (Node.objectNode().equals(right)) {
                         return String.format("Added trait `%s`", trait.getId());
                     } else {
                         return String.format("Added trait `%s` with value %s", trait.getId(), rightPretty);
@@ -231,7 +232,7 @@ public final class TraitBreakingChange extends AbstractDiffEvaluator {
                     if (!path.isEmpty()) {
                         return String.format("Removed trait contents from `%s` at path `%s`. Removed value: %s",
                                              trait.getId(), path, leftPretty);
-                    } else if (leftPretty.equals("{}")) {
+                    } else if (Node.objectNode().equals(left)) {
                         return String.format("Removed trait `%s`", trait.getId());
                     } else {
                         return String.format("Removed trait `%s`. Previous trait value: %s",
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java
index bff196c1d..93ce65a61 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java
@@ -574,10 +574,11 @@ public final class ModelAssembler {
 
         try {
             List<ValidationEvent> mergedEvents = ModelValidator.builder()
-                    .validators(validators)
+                    .addValidators(validators)
                     .validatorFactory(validatorFactory, decorator)
                     .eventListener(validationEventListener)
                     .includeEvents(events)
+                    .legacyValidationMode((boolean) properties.getOrDefault("LEGACY_VALIDATION_MODE", false))
                     .build()
                     .validate(transformed);
             return new ValidatedResult<>(transformed, mergedEvents);
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 85edc2c20..9cc9d26be 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
@@ -19,24 +19,26 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.SourceLocation;
 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.ValidationUtils;
 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.validators.ResourceCycleValidator;
 import software.amazon.smithy.model.validation.validators.TargetValidator;
+import software.amazon.smithy.utils.BuilderRef;
 import software.amazon.smithy.utils.ListUtils;
-import software.amazon.smithy.utils.SetUtils;
+import software.amazon.smithy.utils.MapUtils;
 import software.amazon.smithy.utils.SmithyBuilder;
 
 /**
@@ -68,24 +70,27 @@ final class ModelValidator implements Validator {
     }
 
     /** If these validators fail, then many others will too. Validate these first. */
-    private static final Set<Class<? extends Validator>> CORE_VALIDATORS = SetUtils.of(
-            TargetValidator.class,
-            ResourceCycleValidator.class
+    private static final Map<Class<?>, Validator> CORRECTNESS_VALIDATORS = MapUtils.of(
+            TargetValidator.class, new TargetValidator(),
+            ResourceCycleValidator.class, new ResourceCycleValidator()
     );
 
     private final ValidatorFactory validatorFactory;
-    private final List<ValidationEvent> events = new ArrayList<>();
-    private final List<Validator> validators = new ArrayList<>();
-    private final List<SeverityOverride> severityOverrides = new ArrayList<>();
+    private final List<ValidationEvent> events;
+    private final List<Validator> validators;
+    private final List<Validator> criticalValidators;
     private final ValidationEventDecorator validationEventDecorator;
     private final Consumer<ValidationEvent> eventListener;
+    private final boolean legacyValidationMode;
 
     ModelValidator(Builder builder) {
         this.validatorFactory = builder.validatorFactory;
         this.eventListener = builder.eventListener;
         this.validationEventDecorator = builder.validationEventDecorator;
-        this.events.addAll(builder.includeEvents);
-        this.validators.addAll(builder.validators);
+        this.events = builder.includeEvents.copy();
+        this.validators = builder.validators.copy();
+        this.criticalValidators = builder.criticalValidators.copy();
+        this.legacyValidationMode = builder.legacyValidationMode;
     }
 
     @Override
@@ -103,34 +108,43 @@ final class ModelValidator implements Validator {
 
     static final class Builder implements SmithyBuilder<ModelValidator> {
 
-        private final List<Validator> validators = new ArrayList<>();
-        private final List<ValidationEvent> includeEvents = new ArrayList<>();
+        private final BuilderRef<List<Validator>> validators = BuilderRef.forList();
+        private final BuilderRef<List<Validator>> criticalValidators = BuilderRef.forList();
+        private final BuilderRef<List<ValidationEvent>> includeEvents = BuilderRef.forList();
         private ValidatorFactory validatorFactory = LazyValidatorFactoryHolder.INSTANCE;
         private Consumer<ValidationEvent> eventListener = event -> { };
         private ValidationEventDecorator validationEventDecorator;
+        private boolean legacyValidationMode = false;
 
         private Builder() {}
 
         /**
-         * Sets the custom {@link Validator}s to use when running the ModelValidator.
+         * Adds an array of {@link Validator}s to use when running the ModelValidator.
          *
-         * @param validators Validators to set.
-         * @return Returns the ModelValidator.
+         * @param validators Validators to add.
+         * @return Returns the builder.
          */
-        public Builder validators(Collection<? extends Validator> validators) {
-            this.validators.clear();
-            validators.forEach(this::addValidator);
+        public Builder addValidators(Collection<? extends Validator> validators) {
+            for (Validator validator : validators) {
+                addValidator(validator);
+            }
             return this;
         }
 
         /**
-         * Adds a custom {@link Validator}.
+         * Adds a {@link Validator}.
          *
          * @param validator Validator to add.
          * @return Returns the builder.
          */
         public Builder addValidator(Validator validator) {
-            validators.add(Objects.requireNonNull(validator));
+            if (!CORRECTNESS_VALIDATORS.containsKey(validator.getClass())) {
+                if (ValidationUtils.isCriticalValidator(validator.getClass())) {
+                    criticalValidators.get().add(validator);
+                } else {
+                    validators.get().add(validator);
+                }
+            }
             return this;
         }
 
@@ -165,21 +179,31 @@ final class ModelValidator implements Validator {
          * Includes a set of events that were already encountered in the result.
          *
          * <p>Suppressions and severity overrides will be applied to the given {@code events}. However, the validator
-         * assumes that the event has already been decroated and the event listener has already seen the event.
+         * assumes that the event has already been decorated and the event listener has already seen the event.
          *
          * @param events Events to include.
          * @return Returns the builder.
          */
         public Builder includeEvents(List<ValidationEvent> events) {
-            this.includeEvents.clear();
-            this.includeEvents.addAll(events);
+            this.includeEvents.get().addAll(events);
+            return this;
+        }
+
+        /**
+         * Enables legacy validation mode that does not fail if critical Validators emit an ERROR.
+         *
+         * @param legacyValidationMode Set to true to enable legacy validation mode.
+         * @return Returns the builder.
+         */
+        public Builder legacyValidationMode(boolean legacyValidationMode) {
+            this.legacyValidationMode = legacyValidationMode;
             return this;
         }
 
         @Override
         public ModelValidator build() {
-            validators.addAll(validatorFactory.loadBuiltinValidators());
-            validators.removeIf(v -> CORE_VALIDATORS.contains(v.getClass()));
+            // Adding built-in validators is deferred to allow for a custom factory to be set on the builder.
+            addValidators(validatorFactory.loadBuiltinValidators());
             return new ModelValidator(this);
         }
     }
@@ -187,22 +211,23 @@ final class ModelValidator implements Validator {
     private static final class LoadedModelValidator {
 
         private final Model model;
-        private final List<SeverityOverride> severityOverrides;
         private final List<Validator> validators;
+        private final List<Validator> criticalValidators;
         private final List<ValidationEvent> events = new ArrayList<>();
         private final ValidationEventDecorator validationEventDecorator;
         private final Consumer<ValidationEvent> eventListener;
+        private final boolean legacyValidationMode;
 
         private LoadedModelValidator(Model model, ModelValidator validator) {
             this.model = model;
             this.eventListener = validator.eventListener;
-            this.severityOverrides = new ArrayList<>(validator.severityOverrides);
             this.validators = new ArrayList<>(validator.validators);
+            this.criticalValidators = Collections.unmodifiableList(validator.criticalValidators);
+            this.legacyValidationMode = validator.legacyValidationMode;
 
             // 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(
@@ -265,35 +290,42 @@ final class ModelValidator implements Validator {
         }
 
         private void pushEvent(ValidationEvent event) {
+            events.add(updateAndEmitEvent(event));
+        }
+
+        private ValidationEvent updateAndEmitEvent(ValidationEvent event) {
             if (validationEventDecorator.canDecorate(event)) {
                 event = validationEventDecorator.decorate(event);
             }
-            events.add(event);
             eventListener.accept(event);
+            return event;
         }
 
         private List<ValidationEvent> validate() {
-            // Perform critical validation before other more granular semantic validators.
-            pushEvents(new TargetValidator().validate(model));
-            pushEvents(new ResourceCycleValidator().validate(model));
-
-            // Fail early if errors were detected since further validation will just obscure the root cause.
+            // Perform critical correctness validation before other critical validators.
+            events.addAll(streamEvents(CORRECTNESS_VALIDATORS.values().stream()));
             if (LoaderUtils.containsErrorEvents(events)) {
                 return events;
             }
 
-            List<ValidationEvent> result = validators.parallelStream()
+            // Same thing, but for other critical validators.
+            events.addAll(streamEvents(criticalValidators.parallelStream()));
+
+            // Only fail early here if legacy validation mode is enabled.
+            if (!legacyValidationMode && LoaderUtils.containsErrorEvents(events)) {
+                return events;
+            }
+
+            events.addAll(streamEvents(validators.parallelStream()));
+            return events;
+        }
+
+        private List<ValidationEvent> streamEvents(Stream<Validator> validators) {
+            return validators
                     .flatMap(validator -> validator.validate(model).stream())
                     .filter(this::filterPrelude)
-                    .map(e -> validationEventDecorator.canDecorate(e) ? validationEventDecorator.decorate(e) : e)
-                    // Emit events as they occur during validation.
-                    .peek(eventListener)
+                    .map(this::updateAndEmitEvent)
                     .collect(Collectors.toList());
-
-            // Add in events encountered while building up validators and suppressions.
-            result.addAll(events);
-
-            return result;
         }
 
         private boolean filterPrelude(ValidationEvent event) {
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/PrettyPrintWriter.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/PrettyPrintWriter.java
index bd04c6551..7847e2726 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/PrettyPrintWriter.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/internal/PrettyPrintWriter.java
@@ -95,7 +95,7 @@ final class PrettyPrintWriter extends JsonWriter {
         if (indentString == null) {
             return false;
         }
-        writer.write('\n');
+        writer.write(System.lineSeparator());
         for (int i = 0; i < indent; i++) {
             writer.write(indentString);
         }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java
index 39eeb3c02..c38d6212a 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java
@@ -18,6 +18,7 @@ package software.amazon.smithy.model.shapes;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Consumer;
@@ -31,6 +32,7 @@ import software.amazon.smithy.model.traits.EnumDefinition;
 import software.amazon.smithy.model.traits.EnumTrait;
 import software.amazon.smithy.model.traits.EnumValueTrait;
 import software.amazon.smithy.model.traits.InternalTrait;
+import software.amazon.smithy.model.traits.StringListTrait;
 import software.amazon.smithy.model.traits.TagsTrait;
 import software.amazon.smithy.model.traits.Trait;
 import software.amazon.smithy.model.traits.UnitTypeTrait;
@@ -286,8 +288,16 @@ public final class EnumShape extends StringShape {
                 .orElseThrow(() -> new IllegalStateException("Enum definitions can only be made for string enums."));
         builder.value(traitValue);
         member.getTrait(DocumentationTrait.class).ifPresent(docTrait -> builder.documentation(docTrait.getValue()));
-        member.getTrait(TagsTrait.class).ifPresent(tagsTrait -> builder.tags(tagsTrait.getValues()));
         member.getTrait(DeprecatedTrait.class).ifPresent(deprecatedTrait -> builder.deprecated(true));
+
+        List<String> tags = member.getTrait(TagsTrait.class)
+                .map(StringListTrait::getValues)
+                .orElse(Collections.emptyList());
+
+        builder.tags(tags);
+        if (member.hasTrait(InternalTrait.class) && !tags.contains("internal")) {
+            builder.addTag("internal");
+        }
         return builder.build();
     }
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java
index 7027aee32..459de686d 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java
@@ -26,21 +26,97 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.StringJoiner;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.shapes.ToShapeId;
+import software.amazon.smithy.model.validation.validators.DefaultTraitValidator;
+import software.amazon.smithy.model.validation.validators.EnumShapeValidator;
+import software.amazon.smithy.model.validation.validators.ExclusiveStructureMemberTraitValidator;
+import software.amazon.smithy.model.validation.validators.ResourceCycleValidator;
+import software.amazon.smithy.model.validation.validators.ShapeIdConflictValidator;
+import software.amazon.smithy.model.validation.validators.SingleOperationBindingValidator;
+import software.amazon.smithy.model.validation.validators.SingleResourceBindingValidator;
+import software.amazon.smithy.model.validation.validators.TargetValidator;
+import software.amazon.smithy.model.validation.validators.TraitConflictValidator;
+import software.amazon.smithy.model.validation.validators.TraitTargetValidator;
+import software.amazon.smithy.model.validation.validators.TraitValueValidator;
+import software.amazon.smithy.utils.SetUtils;
+import software.amazon.smithy.utils.SmithyInternalApi;
 
 /**
  * Utility methods used when validating.
  */
 public final class ValidationUtils {
+
+    // Critical validators must emit validation events with names that match their class name (Name in NameValidator).
+    private static final Set<Class<? extends Validator>> CRITICAL_VALIDATORS = SetUtils.of(
+            // These validators should run before even any other validators. These validators are considered
+            // correctness validators that ensure relationships are valid and to shapes that can be found and that
+            // the model has no cycles.
+            TargetValidator.class,
+            ResourceCycleValidator.class,
+
+            // This validator is considered critical because it ensures an enum shape itself is valid.
+            EnumShapeValidator.class,
+
+            // Ensure that traits are valid, according to the model, before other validators are run.
+            ExclusiveStructureMemberTraitValidator.class,
+            TraitTargetValidator.class,
+            TraitValueValidator.class,
+            TraitConflictValidator.class,
+
+            // Defaults need to be valid.
+            DefaultTraitValidator.class,
+
+            // Ensure the semantic model adheres to the requirements of shape IDs and service shape uniqueness.
+            ShapeIdConflictValidator.class,
+            SingleOperationBindingValidator.class,
+            SingleResourceBindingValidator.class
+    );
+
     private static final Pattern CAMEL_WORD_SPLITTER = Pattern.compile("(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])");
 
     private ValidationUtils() {}
 
+    /**
+     * Checks if the given validator class is a critical validator that should be applied before other downstream
+     * validators are applied.
+     *
+     * @param validator Validator class to check.
+     * @return Returns true if the given validator is considered a critical validator.
+     */
+    @SmithyInternalApi
+    public static boolean isCriticalValidator(Class<? extends Validator> validator) {
+        return CRITICAL_VALIDATORS.contains(validator);
+    }
+
+    /**
+     * Checks if the given validation event was emitted by a critical validator.
+     *
+     * @param eventId Event ID to check if it's a critical validation event.
+     * @return Returns true if the given event was emitted from a critical validator.
+     */
+    @SmithyInternalApi
+    public static boolean isCriticalEvent(String eventId) {
+        int slice = eventId.indexOf(".");
+        if (slice > 0) {
+            eventId = eventId.substring(0, slice);
+        }
+
+        for (Class<?> c : CRITICAL_VALIDATORS) {
+            if (c.getSimpleName().replace("Validator", "").equals(eventId)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Splits a camelCase word into a list of words.
      *
@@ -89,6 +165,24 @@ public final class ValidationUtils {
         return values.map(Object::toString).sorted().collect(Collectors.joining("`, `", "`", "`"));
     }
 
+    /**
+     * Writes the contents of a Node to a pretty-printed JSON string surrounded in backticks.
+     *
+     * @param node Node to write.
+     * @return Returns a string suitable for Markdown rendering.
+     */
+    public static String tickedPrettyPrintedNode(Node node) {
+        String prettyPrinted = Node.prettyPrintJson(node);
+        if (prettyPrinted.contains(System.lineSeparator()) || prettyPrinted.contains("\n")) {
+            return String.format("%n```%n%s%n```%n", prettyPrinted);
+        } else if (prettyPrinted.startsWith("\"") && prettyPrinted.endsWith("\"")) {
+            // for pure strings, replace the quotes with backticks
+            return "`" + prettyPrinted.substring(1, prettyPrinted.length() - 1) + "`";
+        } else {
+            return "`" + prettyPrinted + "`";
+        }
+    }
+
     @Deprecated
     public static <T extends ToShapeId> Map<String, List<ShapeId>> findDuplicateShapeNames(Collection<T> shapes) {
         return shapes.stream()


Test patch:
diff --git a/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizer-target.errors b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizer-target.errors
new file mode 100644
index 000000000..b39f0ed06
--- /dev/null
+++ b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizer-target.errors
@@ -0,0 +1,1 @@
+[ERROR] ns.foo#SomeService: Each `scheme` of the `aws.apigateway#authorizers` trait must target one of the auth schemes applied to the service (i.e., [`aws.auth#sigv4`]). The following mappings of authorizer names to schemes are invalid: ns.foo#invalidAuth -> ns.foo#invalidAuth | AuthorizersTrait
diff --git a/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizer-target.json b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizer-target.json
new file mode 100644
index 000000000..acb6008db
--- /dev/null
+++ b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizer-target.json
@@ -0,0 +1,26 @@
+{
+    "smithy": "2.0",
+    "shapes": {
+        "ns.foo#SomeService": {
+            "type": "service",
+            "version": "2018-03-17",
+            "traits": {
+                "aws.auth#sigv4": {
+                    "name": "someservice"
+                },
+                "aws.apigateway#authorizers": {
+                    "ns.foo#invalidAuth": {
+                        "scheme": "ns.foo#invalidAuth"
+                    }
+                }
+            }
+        },
+        "ns.foo#invalidAuth": {
+            "type": "structure",
+            "traits": {
+                "smithy.api#trait": {},
+                "smithy.api#authDefinition": {}
+            }
+        }
+    }
+}
diff --git a/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.errors b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.errors
index 05579a2d5..09b98aed9 100644
--- a/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.errors
+++ b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.errors
@@ -1,2 +1,1 @@
-[ERROR] ns.foo#SomeService: Each `scheme` of the `aws.apigateway#authorizers` trait must target one of the auth schemes applied to the service (i.e., [`aws.auth#sigv4`]). The following mappings of authorizer names to schemes are invalid: another -> smithy.api#httpBasicAuth, invalid -> ns.foo#invalid | AuthorizersTrait
-[ERROR] ns.foo#SomeService: Error validating trait `aws.apigateway#authorizers`.invalid.scheme: The scheme of an authorizer definition must reference an auth trait | TraitValue
+[ERROR] ns.foo#SomeService: Each `scheme` of the `aws.apigateway#authorizers` trait must target one of the auth schemes applied to the service (i.e., [`aws.auth#sigv4`]). The following mappings of authorizer names to schemes are invalid: another -> smithy.api#httpBasicAuth | AuthorizersTrait
diff --git a/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.json b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.json
index e3debf2e2..844630ffa 100644
--- a/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.json
+++ b/smithy-aws-apigateway-traits/src/test/resources/software/amazon/smithy/aws/apigateway/traits/errorfiles/invalid-authorizers.json
@@ -9,9 +9,6 @@
                     "name": "someservice"
                 },
                 "aws.apigateway#authorizers": {
-                    "invalid": {
-                        "scheme": "ns.foo#invalid"
-                    },
                     "another": {
                         "scheme": "smithy.api#httpBasicAuth",
                         "type": "token",
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/FlattenNamespacesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/FlattenNamespacesTest.java
index 681b83fda..4f530e210 100644
--- a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/FlattenNamespacesTest.java
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/FlattenNamespacesTest.java
@@ -16,11 +16,13 @@
 package software.amazon.smithy.build.transforms;
 
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anEmptyMap;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.not;
 
 import java.nio.file.Paths;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -31,7 +33,11 @@ import software.amazon.smithy.model.loader.Prelude;
 import software.amazon.smithy.model.node.ExpectationNotMetException;
 import software.amazon.smithy.model.node.Node;
 import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.validation.ValidatedResult;
+import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.utils.FunctionalUtils;
 
 public class FlattenNamespacesTest {
@@ -143,6 +149,52 @@ public class FlattenNamespacesTest {
                 "ns.bar#Widget", "foo.example#Widget")));
     }
 
+    @Test
+    public void removesServiceRenames() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("flatten-namespaces-with-renames.json"))
+                .assemble()
+                .unwrap();
+        TransformContext context = TransformContext.builder()
+                .model(model)
+                .settings(Node.objectNode()
+                        .withMember("namespace", "ns.qux")
+                        .withMember("service", "ns.foo#MyService"))
+                .build();
+
+        Model result = new FlattenNamespaces().transform(context);
+        ShapeId transformedServiceId = ShapeId.from("ns.qux#MyService");
+
+        assertThat(result.getShape(transformedServiceId), not(Optional.empty()));
+        assertThat(result.expectShape(transformedServiceId, ServiceShape.class).getRename(), anEmptyMap());
+    }
+
+    @Test
+    public void serviceShapeIsValidAfterTransform() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("flatten-namespaces-with-renames.json"))
+                .assemble()
+                .unwrap();
+        TransformContext context = TransformContext.builder()
+                .model(model)
+                .settings(Node.objectNode()
+                        .withMember("namespace", "ns.qux")
+                        .withMember("service", "ns.foo#MyService"))
+                .build();
+
+        Model result = new FlattenNamespaces().transform(context);
+        ValidatedResult<Model> validatedResult = Model.assembler()
+                .addModel(result)
+                .assemble();
+        List<ShapeId> validationEventShapeIds = validatedResult.getValidationEvents().stream()
+                .map(ValidationEvent::getShapeId)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+
+        assertThat(validationEventShapeIds, not(containsInAnyOrder(ShapeId.from("ns.qux#MyService"))));
+    }
+
     @Test
     public void throwsWhenServiceIsNotConfigured() {
         Model model = Model.assembler()
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/FormatCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/FormatCommandTest.java
index efc888aac..da96fdd5a 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/FormatCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/FormatCommandTest.java
@@ -32,14 +32,14 @@ public class FormatCommandTest {
         IntegUtils.run("bad-formatting", ListUtils.of("format", "model/other.smithy"), result -> {
             assertThat(result.getExitCode(), equalTo(0));
 
-            String model = result.getFile("model/other.smithy").replace("\r\n", "\n");
-            assertThat(model, equalTo("$version: \"2.0\"\n"
-                                      + "\n"
-                                      + "namespace smithy.example\n"
-                                      + "\n"
-                                      + "string MyString\n"
-                                      + "\n"
-                                      + "string MyString2\n"));
+            String model = result.getFile("model/other.smithy");
+            assertThat(model, equalTo(String.format("$version: \"2.0\"%n"
+                                      + "%n"
+                                      + "namespace smithy.example%n"
+                                      + "%n"
+                                      + "string MyString%n"
+                                      + "%n"
+                                      + "string MyString2%n")));
         });
     }
 
@@ -48,24 +48,24 @@ public class FormatCommandTest {
         IntegUtils.run("bad-formatting", ListUtils.of("format", "model"), result -> {
             assertThat(result.getExitCode(), equalTo(0));
 
-            String main = result.getFile("model/main.smithy").replace("\r\n", "\n");
-            assertThat(main, equalTo("$version: \"2.0\"\n"
-                                     + "\n"
-                                     + "metadata this_is_a_long_string = {\n"
-                                     + "    this_is_a_long_string1: \"a\"\n"
-                                     + "    this_is_a_long_string2: \"b\"\n"
-                                     + "    this_is_a_long_string3: \"c\"\n"
-                                     + "    this_is_a_long_string4: \"d\"\n"
-                                     + "}\n"));
+            String main = result.getFile("model/main.smithy");
+            assertThat(main, equalTo(String.format("$version: \"2.0\"%n"
+                                     + "%n"
+                                     + "metadata this_is_a_long_string = {%n"
+                                     + "    this_is_a_long_string1: \"a\"%n"
+                                     + "    this_is_a_long_string2: \"b\"%n"
+                                     + "    this_is_a_long_string3: \"c\"%n"
+                                     + "    this_is_a_long_string4: \"d\"%n"
+                                     + "}%n")));
 
-            String other = result.getFile("model/other.smithy").replace("\r\n", "\n");
-            assertThat(other, equalTo("$version: \"2.0\"\n"
-                                      + "\n"
-                                      + "namespace smithy.example\n"
-                                      + "\n"
-                                      + "string MyString\n"
-                                      + "\n"
-                                      + "string MyString2\n"));
+            String other = result.getFile("model/other.smithy");
+            assertThat(other, equalTo(String.format("$version: \"2.0\"%n"
+                                      + "%n"
+                                      + "namespace smithy.example%n"
+                                      + "%n"
+                                      + "string MyString%n"
+                                      + "%n"
+                                      + "string MyString2%n")));
         });
     }
 }
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
index efb9551a8..b3f2fcf32 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ValidateCommandTest.java
@@ -87,7 +87,7 @@ public class ValidateCommandTest {
         assertThat(result, containsString("EmitNotes"));
         assertThat(result, containsString("EmitWarnings"));
         assertThat(result, containsString("EmitDangers"));
-        assertThat(result, containsString("TraitTarget"));
+        assertThat(result, containsString("HttpLabelTrait"));
     }
 
     @Test
@@ -99,7 +99,7 @@ public class ValidateCommandTest {
         assertThat(result, containsString("─ EmitNotes"));
         assertThat(result, containsString("─ EmitWarnings"));
         assertThat(result, containsString("─ EmitDangers"));
-        assertThat(result, containsString("─ TraitTarget"));
+        assertThat(result, containsString("─ HttpLabelTrait"));
     }
 
     @Test
@@ -111,7 +111,7 @@ public class ValidateCommandTest {
         assertThat(result, not(containsString("EmitNotes")));
         assertThat(result, containsString("EmitWarnings"));
         assertThat(result, containsString("EmitDangers"));
-        assertThat(result, containsString("TraitTarget"));
+        assertThat(result, containsString("HttpLabelTrait"));
     }
 
     @Test
@@ -123,7 +123,7 @@ public class ValidateCommandTest {
         assertThat(result, not(containsString("EmitNotes")));
         assertThat(result, not(containsString("EmitWarnings")));
         assertThat(result, containsString("EmitDangers"));
-        assertThat(result, containsString("TraitTarget"));
+        assertThat(result, containsString("HttpLabelTrait"));
     }
 
     @Test
@@ -135,7 +135,7 @@ public class ValidateCommandTest {
         assertThat(result, not(containsString("EmitNotes")));
         assertThat(result, not(containsString("EmitWarnings")));
         assertThat(result, not(containsString("EmitDangers")));
-        assertThat(result, containsString("TraitTarget"));
+        assertThat(result, containsString("HttpLabelTrait"));
     }
 
     private CliUtils.Result runValidationEventsTest(Severity severity) throws Exception {
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/validation-events.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/validation-events.smithy
index 8060a6231..b159b690f 100644
--- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/validation-events.smithy
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/validation-events.smithy
@@ -46,5 +46,7 @@ string Warning
 
 string Danger
 
-@required // this trait is invalid and causes an error.
-string Error
+// The uri will trigger an ERROR.
+@http(method: "GET", uri: "/hi/{missingLabel}")
+@readonly
+operation Error {}
diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedPaginatedTraitTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedPaginatedTraitTest.java
index de67316c2..71b0f5bb0 100644
--- a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedPaginatedTraitTest.java
+++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedPaginatedTraitTest.java
@@ -74,7 +74,7 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Removed trait contents from `smithy.api#paginated` at path `/items`. Removed value: \"things\""));
+                   containsString("Removed trait contents from `smithy.api#paginated` at path `/items`. Removed value: `things`"));
     }
 
     @Test
@@ -91,7 +91,7 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Added trait contents to `smithy.api#paginated` at path `/items` with value \"things\""));
+                   containsString("Added trait contents to `smithy.api#paginated` at path `/items` with value `things`"));
     }
 
     @Test
@@ -108,7 +108,7 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Changed trait contents of `smithy.api#paginated` at path `/items` from \"things\" to \"otherThings\""));
+                   containsString("Changed trait contents of `smithy.api#paginated` at path `/items` from `things` to `otherThings`"));
     }
 
     @Test
@@ -125,7 +125,7 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Removed trait contents from `smithy.api#paginated` at path `/pageSize`. Removed value: \"maxResults\""));
+                   containsString("Removed trait contents from `smithy.api#paginated` at path `/pageSize`. Removed value: `maxResults`"));
     }
 
     @Test
@@ -157,7 +157,7 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Changed trait contents of `smithy.api#paginated` at path `/pageSize` from \"maxResults\" to \"otherMaxResults\""));
+                   containsString("Changed trait contents of `smithy.api#paginated` at path `/pageSize` from `maxResults` to `otherMaxResults`"));
     }
 
     @Test
@@ -173,7 +173,7 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Changed trait contents of `smithy.api#paginated` at path `/inputToken` from \"token\" to \"otherToken\""));
+                   containsString("Changed trait contents of `smithy.api#paginated` at path `/inputToken` from `token` to `otherToken`"));
     }
 
     @Test
@@ -189,6 +189,6 @@ public class ChangedPaginatedTraitTest {
 
         assertThat(changedTraitEvents.size(), equalTo(1));
         assertThat(changedTraitEvents.get(0).getMessage(),
-                   containsString("Changed trait contents of `smithy.api#paginated` at path `/outputToken` from \"token\" to \"otherToken\""));
+                   containsString("Changed trait contents of `smithy.api#paginated` at path `/outputToken` from `token` to `otherToken`"));
     }
 }
diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ModifiedTraitTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ModifiedTraitTest.java
index e27be2901..5e0aa9f6c 100644
--- a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ModifiedTraitTest.java
+++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ModifiedTraitTest.java
@@ -160,9 +160,9 @@ public class ModifiedTraitTest {
                 equalTo(2L));
 
         assertThat(messages, containsInAnyOrder(
-                "Changed trait `smithy.example#b` from \"hello\" to \"hello!\"",
-                "Removed trait `smithy.example#a`. Previous trait value: {}",
-                "Added trait `smithy.example#c` with value \"foo\""
+                "Changed trait `smithy.example#b` from `hello` to `hello!`",
+                "Removed trait `smithy.example#a`. Previous trait value: `{}`",
+                "Added trait `smithy.example#c` with value `foo`"
         ));
     }
 
@@ -186,8 +186,8 @@ public class ModifiedTraitTest {
                 equalTo(2L));
 
         assertThat(messages, containsInAnyOrder(
-                "Added trait contents to `smithy.example#aTrait` at path `/bar` with value \"no\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/baz/foo` from \"bye\" to \"adios\""
+                "Added trait contents to `smithy.example#aTrait` at path `/bar` with value `no`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/baz/foo` from `bye` to `adios`"
         ));
     }
 
@@ -210,11 +210,17 @@ public class ModifiedTraitTest {
         assertThat(events.stream().filter(e -> !e.getMessage().contains("Removed"))
                 .filter(e -> e.getSourceLocation().getFilename().endsWith("b.smithy")).count(), equalTo(2L));
         assertThat(messages, containsInAnyOrder(
-                "Changed trait contents of `smithy.example#aTrait` at path `/foo/1` from \"b\" to \"B\"",
-                "Added trait contents to `smithy.example#aTrait` at path `/foo/3` with value \"4\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/foo/2`. Removed value: \"3\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/foo`. Removed value: "
-                + "[\n    \"1\",\n    \"2\",\n    \"3\"\n]"
+                "Changed trait contents of `smithy.example#aTrait` at path `/foo/1` from `b` to `B`",
+                "Added trait contents to `smithy.example#aTrait` at path `/foo/3` with value `4`",
+                "Removed trait contents from `smithy.example#aTrait` at path `/foo/2`. Removed value: `3`",
+                String.format("Removed trait contents from `smithy.example#aTrait` at path `/foo`. Removed value: %n"
+                        + "```%n"
+                        + "[%n"
+                        + "    \"1\",%n"
+                        + "    \"2\",%n"
+                        + "    \"3\"%n"
+                        + "]%n"
+                        + "```%n")
         ));
     }
 
@@ -237,10 +243,18 @@ public class ModifiedTraitTest {
         assertThat(events.stream().filter(e -> !e.getMessage().contains("Removed"))
                 .filter(e -> e.getSourceLocation().getFilename().endsWith("b.smithy")).count(), equalTo(2L));
         assertThat(messages, containsInAnyOrder(
-                "Changed trait contents of `smithy.example#aTrait` at path `/foo/1` from \"b\" to \"B\"",
-                "Added trait contents to `smithy.example#aTrait` at path `/foo/3` with value \"4\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/foo/2`. Removed value: \"3\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/foo`. Removed value: [\n    \"1\",\n    \"2\",\n    \"3\"\n]"
+                "Changed trait contents of `smithy.example#aTrait` at path `/foo/1` from `b` to `B`",
+                "Added trait contents to `smithy.example#aTrait` at path `/foo/3` with value `4`",
+                "Removed trait contents from `smithy.example#aTrait` at path `/foo/2`. Removed value: `3`",
+                String.format("Removed trait contents from `smithy.example#aTrait` at path `/foo`. "
+                        + "Removed value: %n"
+                        + "```%n"
+                        + "[%n"
+                        + "    \"1\",%n"
+                        + "    \"2\",%n"
+                        + "    \"3\"%n"
+                        + "]%n"
+                        + "```%n")
         ));
     }
 
@@ -263,10 +277,18 @@ public class ModifiedTraitTest {
         assertThat(events.stream().filter(e -> !e.getMessage().contains("Removed"))
                 .filter(e -> e.getSourceLocation().getFilename().endsWith("b.smithy")).count(), equalTo(2L));
         assertThat(messages, containsInAnyOrder(
-                "Changed trait contents of `smithy.example#aTrait` at path `/foo/bam` from \"b\" to \"B\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/foo`. Removed value: {\n    \"baz\": \"1\",\n    \"bam\": \"2\",\n    \"boo\": \"3\"\n}",
-                "Added trait contents to `smithy.example#aTrait` at path `/foo/qux` with value \"4\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/foo/boo`. Removed value: \"3\""
+                "Changed trait contents of `smithy.example#aTrait` at path `/foo/bam` from `b` to `B`",
+                String.format("Removed trait contents from `smithy.example#aTrait` at path `/foo`. "
+                        + "Removed value: %n"
+                        + "```%n"
+                        + "{%n"
+                        + "    \"baz\": \"1\",%n"
+                        + "    \"bam\": \"2\",%n"
+                        + "    \"boo\": \"3\"%n"
+                        + "}%n"
+                        + "```%n"),
+                "Added trait contents to `smithy.example#aTrait` at path `/foo/qux` with value `4`",
+                "Removed trait contents from `smithy.example#aTrait` at path `/foo/boo`. Removed value: `3`"
         ));
     }
 
@@ -287,8 +309,8 @@ public class ModifiedTraitTest {
         assertThat(events.stream().filter(e -> e.getSourceLocation().getFilename().endsWith("b.smithy")).count(),
                 equalTo(2L));
         assertThat(messages, containsInAnyOrder(
-                "Changed trait contents of `smithy.example#aTrait` at path `/baz/foo` from \"a\" to \"b\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/baz/baz` from \"a\" to \"b\""
+                "Changed trait contents of `smithy.example#aTrait` at path `/baz/foo` from `a` to `b`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/baz/baz` from `a` to `b`"
         ));
     }
 
@@ -311,18 +333,18 @@ public class ModifiedTraitTest {
         assertThat(events.stream().filter(e -> e.getMessage().contains("Changed") || e.getMessage().contains("Added"))
                 .filter(e -> e.getSourceLocation().getFilename().endsWith("b.smithy")).count(), equalTo(9L));
         assertThat(messages, containsInAnyOrder(
-                "Added trait contents to `smithy.example#aTrait` at path `/a` with value \"a\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/b`. Removed value: \"a\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/c` from \"a\" to \"c\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/d` from \"a\" to \"d\"",
-                "Added trait contents to `smithy.example#aTrait` at path `/e` with value \"a\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/f`. Removed value: \"a\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/g` from \"a\" to \"h\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/h` from \"a\" to \"h\"",
-                "Added trait contents to `smithy.example#aTrait` at path `/i` with value \"a\"",
-                "Removed trait contents from `smithy.example#aTrait` at path `/j`. Removed value: \"a\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/k` from \"a\" to \"k\"",
-                "Changed trait contents of `smithy.example#aTrait` at path `/l` from \"a\" to \"l\""
+                "Added trait contents to `smithy.example#aTrait` at path `/a` with value `a`",
+                "Removed trait contents from `smithy.example#aTrait` at path `/b`. Removed value: `a`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/c` from `a` to `c`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/d` from `a` to `d`",
+                "Added trait contents to `smithy.example#aTrait` at path `/e` with value `a`",
+                "Removed trait contents from `smithy.example#aTrait` at path `/f`. Removed value: `a`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/g` from `a` to `h`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/h` from `a` to `h`",
+                "Added trait contents to `smithy.example#aTrait` at path `/i` with value `a`",
+                "Removed trait contents from `smithy.example#aTrait` at path `/j`. Removed value: `a`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/k` from `a` to `k`",
+                "Changed trait contents of `smithy.example#aTrait` at path `/l` from `a` to `l`"
         ));
     }
 
diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/TraitBreakingChangeTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/TraitBreakingChangeTest.java
index 8bcb204bd..4e4659ae8 100644
--- a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/TraitBreakingChangeTest.java
+++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/TraitBreakingChangeTest.java
@@ -131,7 +131,7 @@ public class TraitBreakingChangeTest {
                 assertThat(events.get(0).getId(), equalTo("TraitBreakingChange.Update.smithy.example#exampleTrait"));
                 assertThat(events.get(0).getMessage(),
                            equalTo("Changed trait contents of `smithy.example#exampleTrait` at path `/1` "
-                                   + "from \"b\" to \"B\""));
+                                   + "from `b` to `B`"));
             }
         );
     }
@@ -147,7 +147,7 @@ public class TraitBreakingChangeTest {
                     assertThat(events.get(0).getId(), equalTo("TraitBreakingChange.Remove.smithy.example#exampleTrait"));
                     assertThat(events.get(0).getMessage(),
                                equalTo("Removed trait contents from `smithy.example#exampleTrait` at path `/b`. "
-                                       + "Removed value: \"B\""));
+                                       + "Removed value: `B`"));
                 }
         );
     }
@@ -166,7 +166,7 @@ public class TraitBreakingChangeTest {
                 assertThat(events.get(0).getId(), equalTo("TraitBreakingChange.Update.smithy.example#exampleTrait"));
                 assertThat(events.get(0).getMessage(),
                            equalTo("Changed trait contents of `smithy.example#exampleTrait` at path `/b` "
-                                   + "from \"B\" to \"_B_\""));
+                                   + "from `B` to `_B_`"));
             }
         );
     }
@@ -184,7 +184,7 @@ public class TraitBreakingChangeTest {
                 assertThat(events.get(0).getId(), equalTo("TraitBreakingChange.Remove.smithy.example#exampleTrait"));
                 assertThat(events.get(0).getMessage(),
                            equalTo("Removed trait contents from `smithy.example#exampleTrait` at path "
-                                   + "`/foo/bar`. Removed value: \"hi\""));
+                                   + "`/foo/bar`. Removed value: `hi`"));
             }
         );
     }
@@ -202,7 +202,7 @@ public class TraitBreakingChangeTest {
                 assertThat(events.get(0).getId(), equalTo("TraitBreakingChange.Update.smithy.example#exampleTrait"));
                 assertThat(events.get(0).getMessage(),
                            equalTo("Changed trait contents of `smithy.example#exampleTrait` at path "
-                                   + "`/foo` from \"hi\" to \"bye\""));
+                                   + "`/foo` from `hi` to `bye`"));
             }
         );
     }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java
index 943c4d4e1..70a8e4716 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java
@@ -25,21 +25,30 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.Future;
 import java.util.function.Supplier;
+import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.loader.ModelAssembler;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationUtils;
+import software.amazon.smithy.model.validation.Validator;
 
 /**
  * Runs test cases against a directory of models and error files.
  */
 public final class SmithyTestSuite {
+
+    private static final Logger LOGGER = Logger.getLogger(SmithyTestSuite.class.getName());
     private static final String DEFAULT_TEST_CASE_LOCATION = "errorfiles";
 
     private final List<SmithyTestCase> cases = new ArrayList<>();
@@ -247,13 +256,60 @@ public final class SmithyTestSuite {
     }
 
     private Callable<SmithyTestCase.Result> createTestCaseCallable(SmithyTestCase testCase) {
+        boolean useLegacyValidationMode = isLegacyValidationRequired(testCase);
         return () -> {
             ModelAssembler assembler = modelAssemblerFactory.get();
             assembler.addImport(testCase.getModelLocation());
+            if (useLegacyValidationMode) {
+                assembler.putProperty("LEGACY_VALIDATION_MODE", true);
+            }
             return testCase.createResult(assembler.assemble());
         };
     }
 
+    // We introduced the concept of "critical" validation events after many errorfiles were created that relied
+    // on all validators being run, including validators now considered critical that prevent further validation.
+    // If we didn't account for that here, the addition of "critical" validators would have been a breaking change.
+    // To make it backward compatible, we preemptively detect if the errorfiles contains both critical and
+    // non-critical validation event assertions, and if so, we run validation using an internal-only validation
+    // mode that doesn't fail after critical validators report errors.
+    private boolean isLegacyValidationRequired(SmithyTestCase testCase) {
+        Set<String> criticalEvents = new TreeSet<>();
+        Set<String> nonCriticalEvents = new TreeSet<>();
+
+        for (ValidationEvent event : testCase.getExpectedEvents()) {
+            if (isCriticalValidationEvent(event)) {
+                criticalEvents.add(event.getId());
+            } else {
+                nonCriticalEvents.add(event.getId());
+            }
+        }
+
+        if (!criticalEvents.isEmpty() && !nonCriticalEvents.isEmpty()) {
+            LOGGER.warning(String.format("Test suite `%s` relies on the emission of non-critical validation events "
+                                         + "after critical validation events were emitted. This test case should be "
+                                         + "refactored so that critical validation events are tested using separate "
+                                         + "test cases from non-critical events. Critical events: %s. Non-critical "
+                                         + "events: %s",
+                                         testCase.getModelLocation(),
+                                         criticalEvents,
+                                         nonCriticalEvents));
+            return true;
+        }
+
+        return false;
+    }
+
+    private static boolean isCriticalValidationEvent(ValidationEvent event) {
+        if (ValidationUtils.isCriticalEvent(event.getId())) {
+            return true;
+        }
+
+        // In addition to the method to the check based on Validator classes, MODEL validation event IDs marked as
+        // ERROR that go along with non-critical events requires legacy validation.
+        return event.getId().equals(Validator.MODEL_ERROR) && event.getSeverity() == Severity.ERROR;
+    }
+
     /**
      * Executes the test runner.
      *
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/node/DefaultNodeWriterTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/node/DefaultNodeWriterTest.java
index d03d12971..bafd4e053 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/node/DefaultNodeWriterTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/node/DefaultNodeWriterTest.java
@@ -42,21 +42,21 @@ public class DefaultNodeWriterTest {
                 {new NullNode(SourceLocation.none()), "null"},
                 {new ArrayNode(
                         Arrays.asList(Node.from(1), Node.from(2)),
-                        SourceLocation.none()), String.format("[\n    1,\n    2\n]")},
+                        SourceLocation.none()), String.format("[%n    1,%n    2%n]")},
                 {Node.objectNode()
                          .withMember("foo", Node.from("foo"))
                          .withMember("baz", Node.from(1))
                          .withMember("bar", Node.objectNode()
                              .withMember("qux", Node.arrayNode()
                                      .withValue(Node.from("ipsum")))),
-                 String.format("{\n"
-                               + "    \"foo\": \"foo\",\n"
-                               + "    \"baz\": 1,\n"
-                               + "    \"bar\": {\n"
-                               + "        \"qux\": [\n"
-                               + "            \"ipsum\"\n"
-                               + "        ]\n"
-                               + "    }\n"
+                 String.format("{%n"
+                               + "    \"foo\": \"foo\",%n"
+                               + "    \"baz\": 1,%n"
+                               + "    \"bar\": {%n"
+                               + "        \"qux\": [%n"
+                               + "            \"ipsum\"%n"
+                               + "        ]%n"
+                               + "    }%n"
                                + "}")
                 },
                 {Node.objectNode()
@@ -66,17 +66,17 @@ public class DefaultNodeWriterTest {
                          .withMember("bam", Node.arrayNode()
                                  .withValue(Node.objectNode()
                                          .withMember("abc", Node.from(123)))),
-                        String.format("{\n"
-                                      + "    \"foo\": {\n"
-                                      + "        \"bar\": [\n"
-                                      + "            \"baz\"\n"
-                                      + "        ]\n"
-                                      + "    },\n"
-                                      + "    \"bam\": [\n"
-                                      + "        {\n"
-                                      + "            \"abc\": 123\n"
-                                      + "        }\n"
-                                      + "    ]\n"
+                        String.format("{%n"
+                                      + "    \"foo\": {%n"
+                                      + "        \"bar\": [%n"
+                                      + "            \"baz\"%n"
+                                      + "        ]%n"
+                                      + "    },%n"
+                                      + "    \"bam\": [%n"
+                                      + "        {%n"
+                                      + "            \"abc\": 123%n"
+                                      + "        }%n"
+                                      + "    ]%n"
                                       + "}")
                 }
         });
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeSerializerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeSerializerTest.java
index db7d284bb..eb205331d 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeSerializerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeSerializerTest.java
@@ -11,18 +11,18 @@ public class NodeSerializerTest {
         Node node = Node.parse("{\"foo\": \"bar\", \"baz\": true, \"bam\": false, \"boo\": 10}");
 
         assertThat(Node.printJson(node), equalTo("{\"foo\":\"bar\",\"baz\":true,\"bam\":false,\"boo\":10}"));
-        assertThat(Node.prettyPrintJson(node), equalTo("{\n"
-                                                       + "    \"foo\": \"bar\",\n"
-                                                       + "    \"baz\": true,\n"
-                                                       + "    \"bam\": false,\n"
-                                                       + "    \"boo\": 10\n"
-                                                       + "}"));
-        assertThat(Node.prettyPrintJson(node, "\t"), equalTo("{\n"
-                                                             + "\t\"foo\": \"bar\",\n"
-                                                             + "\t\"baz\": true,\n"
-                                                             + "\t\"bam\": false,\n"
-                                                             + "\t\"boo\": 10\n"
-                                                             + "}"));
+        assertThat(Node.prettyPrintJson(node), equalTo(String.format("{%n"
+                                                       + "    \"foo\": \"bar\",%n"
+                                                       + "    \"baz\": true,%n"
+                                                       + "    \"bam\": false,%n"
+                                                       + "    \"boo\": 10%n"
+                                                       + "}")));
+        assertThat(Node.prettyPrintJson(node, "\t"), equalTo(String.format("{%n"
+                                                             + "\t\"foo\": \"bar\",%n"
+                                                             + "\t\"baz\": true,%n"
+                                                             + "\t\"bam\": false,%n"
+                                                             + "\t\"boo\": 10%n"
+                                                             + "}")));
     }
 
     @Test
@@ -45,24 +45,24 @@ public class NodeSerializerTest {
         assertThat(Node.printJson(node), equalTo(
                 "[{\"foo\":\"\uD83D\uDCA9\",\"baz\":true,\"bam\":false,"
                 + "\"boo\":10},10,true,false,{},[],\"\",\" \",null,-1,-1.0]"));
-        assertThat(Node.prettyPrintJson(node), equalTo(
-                "[\n"
-                + "    {\n"
-                + "        \"foo\": \"\uD83D\uDCA9\",\n"
-                + "        \"baz\": true,\n"
-                + "        \"bam\": false,\n"
-                + "        \"boo\": 10\n"
-                + "    },\n"
-                + "    10,\n"
-                + "    true,\n"
-                + "    false,\n"
-                + "    {},\n" // optimized empty object
-                + "    [],\n" // optimized empty array
-                + "    \"\",\n"
-                + "    \" \",\n"
-                + "    null,\n"
-                + "    -1,\n"
-                + "    -1.0\n"
-                + "]"));
+        assertThat(Node.prettyPrintJson(node), equalTo(String.format(
+                "[%n"
+                + "    {%n"
+                + "        \"foo\": \"\uD83D\uDCA9\",%n"
+                + "        \"baz\": true,%n"
+                + "        \"bam\": false,%n"
+                + "        \"boo\": 10%n"
+                + "    },%n"
+                + "    10,%n"
+                + "    true,%n"
+                + "    false,%n"
+                + "    {},%n" // optimized empty object
+                + "    [],%n" // optimized empty array
+                + "    \"\",%n"
+                + "    \" \",%n"
+                + "    null,%n"
+                + "    -1,%n"
+                + "    -1.0%n"
+                + "]")));
     }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeTest.java
index 91c646e5e..d0a124289 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/node/NodeTest.java
@@ -178,7 +178,7 @@ public class NodeTest {
     @Test
     public void prettyPrintsJson() {
         assertThat(Node.prettyPrintJson(Node.parse("{\"foo\": true}")),
-                   equalTo(String.format("{\n    \"foo\": true\n}")));
+                   equalTo(String.format("{%n    \"foo\": true%n}")));
     }
 
     @Test
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java
index 8a9e951c9..1c6d83b4d 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java
@@ -499,4 +499,62 @@ public class EnumShapeTest {
         assertFalse(EnumShape.canConvertToEnum(string, false));
         assertFalse(EnumShape.canConvertToEnum(string, true));
     }
+
+    @Test
+    public void syntheticEnumSupportsDocumentation() {
+        EnumShape shape = EnumShape.builder()
+                .id("com.example#DocumentedEnum")
+                .addMember("documented", "foo", builder -> {
+                    builder.addTrait(new DocumentationTrait("bar"));
+                })
+                .build();
+
+        SyntheticEnumTrait trait = shape.expectTrait(SyntheticEnumTrait.class);
+        assertEquals("bar", trait.getValues().get(0).getDocumentation().get());
+    }
+
+    @Test
+    public void syntheticEnumSupportsDeprecated() {
+        EnumShape shape = EnumShape.builder()
+                .id("com.example#DeprecatedEnum")
+                .addMember("deprecated", "foo", builder -> {
+                    builder.addTrait(DeprecatedTrait.builder().build());
+                })
+                .build();
+
+        SyntheticEnumTrait trait = shape.expectTrait(SyntheticEnumTrait.class);
+        assertTrue(trait.getValues().get(0).isDeprecated());
+    }
+
+    @Test
+    public void syntheticEnumSupportsTags() {
+        EnumShape shape = EnumShape.builder()
+                .id("com.example#TaggedEnum")
+                .addMember("tagged", "foo", builder -> {
+                    builder.addTrait(TagsTrait.builder().addValue("bar").build());
+                })
+                .build();
+
+        SyntheticEnumTrait trait = shape.expectTrait(SyntheticEnumTrait.class);
+        assertEquals(ListUtils.of("bar"), trait.getValues().get(0).getTags());
+    }
+
+    @Test
+    public void syntheticEnumSupportsInternal() {
+        EnumShape shape = EnumShape.builder()
+                .id("com.example#InternalEnum")
+                .addMember("withoutTag", "foo", builder -> {
+                    builder.addTrait(new InternalTrait());
+                })
+                .addMember("withTag", "bar", builder -> {
+                    builder.addTrait(new InternalTrait());
+                    builder.addTrait(TagsTrait.builder().addValue("internal").build());
+                })
+                .build();
+
+        SyntheticEnumTrait trait = shape.expectTrait(SyntheticEnumTrait.class);
+        for (EnumDefinition definition : trait.getValues()) {
+            assertEquals(ListUtils.of("internal"), definition.getTags());
+        }
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/ContextualValidationEventFormatterTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/ContextualValidationEventFormatterTest.java
index b8e1ec644..5b69b8df8 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/ContextualValidationEventFormatterTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/ContextualValidationEventFormatterTest.java
@@ -31,17 +31,15 @@ public class ContextualValidationEventFormatterTest {
                 .build();
 
         String format = new ContextualValidationEventFormatter().format(event);
-        // Normalize line endings for Windows.
-        format = format.replace("\r\n", "\n");
 
         assertThat(format, startsWith("ERROR: example.smithy#Foo (foo)"));
-        assertThat(format, containsString("\n     @ "));
-        assertThat(format, endsWith(
-                "\n     |"
-                + "\n   3 | structure Foo {"
-                + "\n     | ^"
-                + "\n     = This is the message"
-                + "\n"));
+        assertThat(format, containsString(String.format("%n     @ ")));
+        assertThat(format, endsWith(String.format(
+                "%n     |"
+                + "%n   3 | structure Foo {"
+                + "%n     | ^"
+                + "%n     = This is the message"
+                + "%n")));
     }
 
     @Test
@@ -54,12 +52,11 @@ public class ContextualValidationEventFormatterTest {
                 .build();
 
         String format = new ContextualValidationEventFormatter().format(event);
-        // Normalize line endings for Windows.
-        format = format.replace("\r\n", "\n");
 
-        assertThat(format, equalTo(
+        assertThat(format, equalTo(String.format(
                 "ERROR: - (foo)"
-                + "\n     = This is the message"
-                + "\n"));
+                + "%n     = This is the message"
+                + "%n")
+        ));
     }
 }