Repository: smithy-lang/smithy
Pull request #2265 (2024-04-29 22:52:45)

Refactor trait codegen javadoc integration to correctly support member documentation
#### Background
Refactors the trait codegen Javadoc integration to use a new approach to adding javadocs to both clean up the integration and to correctly support documentation on all members and enum variants.

In this updated approach empty Javadoc sections are first injected for all getters, classes, and enum variants. Interceptors can then populate the Javadocs section with text based on the presence of documentation traits. Finally, the core integration formats any javadoc sections with text as Java doc comments and discards any empty Javadoc sections.

#### Testing
Adds new suite of more comprehensive tests for smithy documentation traits.

#### Links
* Incorporates fix to externalDocumentation trait integration noted in PR #2263 
* Closes #2263 

---
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-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
index 53423c4b7..c6d32ca82 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java
@@ -22,7 +22,6 @@ import software.amazon.smithy.model.traits.StringListTrait;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.traitcodegen.SymbolProperties;
 import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
-import software.amazon.smithy.traitcodegen.sections.BuilderClassSection;
 import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
 import software.amazon.smithy.utils.BuilderRef;
 import software.amazon.smithy.utils.SmithyBuilder;
@@ -61,7 +60,7 @@ final class BuilderGenerator implements Runnable {
     }
 
     private void writeBuilderClass() {
-        writer.pushState(new BuilderClassSection(symbol));
+        writer.writeDocString(writer.format("Builder for {@link $T}.", symbol));
         writer.writeInline("public static final class Builder $C", (Runnable) this::writeBuilderInterface);
         writer.indent();
         baseShape.accept(new BuilderPropertyGenerator());
@@ -72,7 +71,6 @@ final class BuilderGenerator implements Runnable {
         writer.openBlock("public $T build() {", "}", symbol,
                 () -> writer.write("return new $C;", (Runnable) this::writeBuilderReturn));
         writer.dedent().write("}");
-        writer.popState();
         writer.newLine();
     }
 
@@ -99,10 +97,7 @@ final class BuilderGenerator implements Runnable {
 
 
     private void writeToBuilderMethod() {
-        writer.openDocstring();
-        writer.writeDocStringContents("Creates a builder used to build a {@link $T}.", symbol);
-        writer.closeDocstring();
-        writer.override();
+        writer.writeDocString(writer.format("Creates a builder used to build a {@link $T}.", symbol));
         writer.openBlock("public $T<$T> toBuilder() {", "}",
                 SmithyBuilder.class, symbol, () -> {
             writer.writeInlineWithNoFormatting("return builder()");
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java
index 58a44e265..41d9153ab 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java
@@ -129,14 +129,10 @@ abstract class EnumShapeGenerator implements Consumer<GenerateTraitDirective> {
     }
 
     private void writeFromMethod(Symbol enumSymbol, TraitCodegenWriter writer) {
-        writer.openDocstring();
-        writer.writeDocStringContents("Create a {@code $B} from a value in a model.", enumSymbol);
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("<p> Any unknown value is returned as {@code UNKNOWN}.");
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("@param value Value to create enum from.");
-        writer.writeDocStringContents("@return Returns the {@link $B} enum value.", enumSymbol);
-        writer.closeDocstring();
+        writer.writeDocString(writer.format("Create a {@code $1B} from a value in a model.\n\n"
+                + "<p>Any unknown value is returned as {@code UNKNOWN}.\n"
+                + "@param value Value to create enum from.\n"
+                + "@return Returns the {@link $1B} enum value.", enumSymbol));
         writer.openBlock("public static $B from($T value) {", "}",
                 enumSymbol, getValueType(), () -> {
                     writer.write("$T.requireNonNull(value, \"Enum value should not be null.\");", Objects.class);
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
index 8d71f05fd..113e850b7 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java
@@ -172,15 +172,11 @@ final class FromNodeGenerator extends TraitVisitor<Void> implements Runnable {
     }
 
     private void writeFromNodeJavaDoc() {
-        // Add docstring for method
-        writer.openDocstring();
-        writer.writeDocStringContents("Creates a {@link $T} from a {@link Node}.", symbol);
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("@param node Node to create the $T from.", symbol);
-        writer.writeDocStringContents("@return Returns the created $T.", symbol);
-        writer.writeDocStringContents("@throws $T if the given Node is invalid.",
-                ExpectationNotMetException.class);
-        writer.closeDocstring();
+        writer.writeDocString(writer.format("Creates a {@link $1T} from a {@link Node}.\n\n"
+                    + "@param node Node to create the $1T from.\n"
+                    + "@return Returns the created $1T.\n"
+                    + "@throws $2T if the given Node is invalid.\n",
+                symbol, ExpectationNotMetException.class));
     }
 
     /**
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
index 946816e67..dca3a7887 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java
@@ -159,11 +159,9 @@ final class GetterGenerator implements Runnable {
         }
 
         private void generateEnumValueGetterDocstring(Symbol symbol) {
-            writer.openDocstring();
-            writer.writeDocStringContents("Gets the {@code $1T} value as a {@code $1B} enum.", symbol);
-            writer.writeDocStringContents("");
-            writer.writeDocStringContents("@return Returns the {@code $B} enum.", symbol);
-            writer.closeDocstring();
+            writer.writeDocString(writer.format("Gets the {@code $1T} value as a {@code $1B} enum.\n\n"
+                    + "@return Returns the {@code $1B} enum.",
+                    symbol));
         }
 
         private void generateValuesGetter(Shape shape) {
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationIntegration.java
new file mode 100644
index 000000000..5225a82b1
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationIntegration.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import java.util.List;
+import software.amazon.smithy.traitcodegen.TraitCodegenContext;
+import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration;
+import software.amazon.smithy.traitcodegen.integrations.javadoc.JavaDocIntegration;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+import software.amazon.smithy.utils.ListUtils;
+
+/**
+ * Adds Java annotations to generated Java classes.
+ */
+public class AnnotationIntegration implements TraitCodegenIntegration {
+    @Override
+    public String name() {
+        return "annotations";
+    }
+
+    @Override
+    public List<String> runBefore() {
+        return ListUtils.of(JavaDocIntegration.NAME);
+    }
+
+    @Override
+    public List<? extends CodeInterceptor<? extends CodeSection, TraitCodegenWriter>> interceptors(
+            TraitCodegenContext codegenContext) {
+        return ListUtils.of(
+            new SmithyGeneratedAnnotationInterceptor(),
+            new DeprecatedAnnotationInterceptor(),
+            new UnstableAnnotationInterceptor()
+        );
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/DeprecatedAnnotationInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/DeprecatedAnnotationInterceptor.java
new file mode 100644
index 000000000..28d087f44
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/DeprecatedAnnotationInterceptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import software.amazon.smithy.model.traits.DeprecatedTrait;
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
+import software.amazon.smithy.traitcodegen.sections.GetterSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+
+/**
+ * Adds the {@link Deprecated} annotation to generated code for shapes with the {@link DeprecatedTrait}.
+ */
+final class DeprecatedAnnotationInterceptor implements CodeInterceptor.Prepender<CodeSection, TraitCodegenWriter> {
+
+    @Override
+    public Class<CodeSection> sectionType() {
+        return CodeSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(CodeSection section) {
+        if (section instanceof ClassSection) {
+            return ((ClassSection) section).shape().hasTrait(DeprecatedTrait.class);
+        } else if (section instanceof GetterSection) {
+            return ((GetterSection) section).shape().hasTrait(DeprecatedTrait.class);
+        } else if (section instanceof EnumVariantSection) {
+            return  ((EnumVariantSection) section).memberShape().hasTrait(DeprecatedTrait.class);
+        }
+        return false;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, CodeSection section) {
+        writer.write("@$T", Deprecated.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/SmithyGeneratedAnnotationInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/SmithyGeneratedAnnotationInterceptor.java
new file mode 100644
index 000000000..3ada672c6
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/SmithyGeneratedAnnotationInterceptor.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.SmithyGenerated;
+
+/**
+ * Adds the {@link software.amazon.smithy.utils.SmithyGenerated} annotation to all generated classes.
+ */
+final class SmithyGeneratedAnnotationInterceptor
+        implements CodeInterceptor.Prepender<ClassSection, TraitCodegenWriter> {
+    @Override
+    public Class<ClassSection> sectionType() {
+        return ClassSection.class;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, ClassSection section) {
+        writer.write("@$T", SmithyGenerated.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/UnstableAnnotationInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/UnstableAnnotationInterceptor.java
new file mode 100644
index 000000000..5f8ba199d
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/annotations/UnstableAnnotationInterceptor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import software.amazon.smithy.model.traits.UnstableTrait;
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
+import software.amazon.smithy.traitcodegen.sections.GetterSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+import software.amazon.smithy.utils.SmithyUnstableApi;
+
+/**
+ * Adds the {@link software.amazon.smithy.utils.SmithyUnstableApi} annotation to generated code for
+ * shapes with the {@link UnstableTrait}.
+ */
+final class UnstableAnnotationInterceptor implements CodeInterceptor.Prepender<CodeSection, TraitCodegenWriter> {
+    @Override
+    public Class<CodeSection> sectionType() {
+        return CodeSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(CodeSection section) {
+        if (section instanceof ClassSection) {
+            return ((ClassSection) section).shape().hasTrait(UnstableTrait.class);
+        } else if (section instanceof GetterSection) {
+            return ((GetterSection) section).shape().hasTrait(UnstableTrait.class);
+        } else if (section instanceof EnumVariantSection) {
+            return  ((EnumVariantSection) section).memberShape().hasTrait(UnstableTrait.class);
+        }
+        return false;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, CodeSection section) {
+        writer.write("@$T", SmithyUnstableApi.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java
index bd590dbec..5795638d7 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java
@@ -5,6 +5,7 @@
 
 package software.amazon.smithy.traitcodegen.integrations.core;
 
+import java.util.List;
 import software.amazon.smithy.codegen.core.Symbol;
 import software.amazon.smithy.codegen.core.SymbolProvider;
 import software.amazon.smithy.model.Model;
@@ -12,9 +13,13 @@ import software.amazon.smithy.model.shapes.MemberShape;
 import software.amazon.smithy.model.shapes.Shape;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.traitcodegen.SymbolProperties;
+import software.amazon.smithy.traitcodegen.TraitCodegenContext;
 import software.amazon.smithy.traitcodegen.TraitCodegenSettings;
 import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
 import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
 import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.SmithyInternalApi;
 
@@ -66,6 +71,13 @@ public final class CoreIntegration implements TraitCodegenIntegration {
         };
     }
 
+    @Override
+    public List<? extends CodeInterceptor<? extends CodeSection, TraitCodegenWriter>> interceptors(
+            TraitCodegenContext codegenContext
+    ) {
+        return ListUtils.of(new JavadocFormatterInterceptor());
+    }
+
     private Symbol getTraitSymbol(TraitCodegenSettings settings, Shape shape, Symbol baseSymbol) {
         String relativeNamespace = TraitCodegenUtils.mapNamespace(settings.smithyNamespace(),
                 shape.getId().getNamespace(), settings.packageName());
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/JavadocFormatterInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/JavadocFormatterInterceptor.java
new file mode 100644
index 000000000..eedc4bf2a
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/JavadocFormatterInterceptor.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.core;
+
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+
+/**
+ * Formats any populated Javadoc comment sections as documentation comments.
+ *
+ * <p>This interceptor will run after all other Javadoc interceptors to ensure it picks up all content
+ * added to Jav doc sections. Javadoc sections with no content are discarded.
+ */
+public class JavadocFormatterInterceptor implements CodeInterceptor<JavaDocSection, TraitCodegenWriter> {
+
+    @Override
+    public Class<JavaDocSection> sectionType() {
+        return JavaDocSection.class;
+    }
+
+    @Override
+    public void write(TraitCodegenWriter writer, String previousText, JavaDocSection section) {
+        if (!previousText.isEmpty()) {
+            writer.writeDocString(previousText);
+        }
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java
deleted file mode 100644
index a18bc95ca..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-
-import software.amazon.smithy.traitcodegen.sections.BuilderClassSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds JavaDocs to static builder classes and is triggered by the {@link BuilderClassSection}.
- */
-final class BuilderClassSectionDocsInterceptor implements CodeInterceptor.Prepender<BuilderClassSection,
-        TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, BuilderClassSection section) {
-        writer.openDocstring();
-        writer.writeDocStringContents("Builder for {@link $T}.", section.symbol());
-        writer.closeDocstring();
-    }
-
-    @Override
-    public Class<BuilderClassSection> sectionType() {
-        return BuilderClassSection.class;
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java
deleted file mode 100644
index 6586ea309..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DocumentationTrait;
-import software.amazon.smithy.traitcodegen.sections.ClassSection;
-import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-import software.amazon.smithy.utils.SmithyGenerated;
-
-/**
- * Adds basic JavaDocs for generated java classes.
- */
-final class ClassJavaDocInterceptor implements CodeInterceptor.Prepender<ClassSection, TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, ClassSection section) {
-        writer.openDocstring();
-        writer.pushState(new JavaDocSection(section.shape()));
-        writer.writeDocStringContents(section.shape().expectTrait(DocumentationTrait.class).getValue());
-        writer.popState();
-        writer.closeDocstring();
-        // Adds smithy generated annotation to indicate class was generated
-        // by a code generator
-        writer.write("@$T", SmithyGenerated.class);
-    }
-
-    @Override
-    public Class<ClassSection> sectionType() {
-        return ClassSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(ClassSection section) {
-        return section.shape().hasTrait(DocumentationTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java
deleted file mode 100644
index 4990a0f8e..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DeprecatedTrait;
-import software.amazon.smithy.traitcodegen.sections.ClassSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds the {@code @Deprecated} annotation to generated classes if the smithy shape the class corresponds to
- * has the {@link DeprecatedTrait} trait applied.
- */
-final class DeprecatedAnnotationClassInterceptor implements CodeInterceptor.Prepender<ClassSection,
-        TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, ClassSection section) {
-        writer.writeWithNoFormatting("@Deprecated");
-    }
-
-    @Override
-    public Class<ClassSection> sectionType() {
-        return ClassSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(ClassSection section) {
-        return section.shape().hasTrait(DeprecatedTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedInterceptor.java
similarity index 66%
rename from smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java
rename to smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedInterceptor.java
index 733b293c1..67f285a2d 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedInterceptor.java
@@ -16,15 +16,18 @@ import software.amazon.smithy.utils.CodeInterceptor;
  *
  * <p>If the {@code DeprecatedTrait} contains a {@code since} field, then "As of " note will be added
  * to the generated tag.
+ * <p>Note: This interceptor should be run after {@link SinceInterceptor} and {@link ExternalDocumentationInterceptor}
+ * to ensure proper tag ordering.
  */
-final class DeprecatedNoteInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
+final class DeprecatedInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
     @Override
     public void append(TraitCodegenWriter writer, JavaDocSection section) {
         DeprecatedTrait trait = section.shape().expectTrait(DeprecatedTrait.class);
-        writer.putContext("since", trait.getSince());
-        // Add spacing
-        writer.writeDocStringContents("");
-        writer.writeDocStringContents("@deprecated ${?since}As of ${since:L}. ${/since}$L", trait.getMessage());
+        // Only add the deprecated java doc tag if message or since message exist.
+        if (trait.getSince().isPresent() || trait.getMessage().isPresent()) {
+            writer.putContext("since", trait.getSince());
+            writer.write("@deprecated ${?since}As of ${since:L}. ${/since}$L", trait.getMessage());
+        }
     }
 
     @Override
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DocumentationTraitInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DocumentationTraitInterceptor.java
new file mode 100644
index 000000000..d15fde7ef
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DocumentationTraitInterceptor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import software.amazon.smithy.model.traits.DocumentationTrait;
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+
+/**
+ * Adds Javadoc documentation for the {@link DocumentationTrait}.
+ *
+ * <p>The documentation trait contents are added as the contents of the Javadoc section.
+ */
+final class DocumentationTraitInterceptor implements CodeInterceptor<JavaDocSection, TraitCodegenWriter> {
+
+    @Override
+    public void write(TraitCodegenWriter writer, String previousText, JavaDocSection section) {
+        writer.write(section.shape().expectTrait(DocumentationTrait.class).getValue());
+
+        if (!previousText.isEmpty()) {
+            // Add spacing if tags have been added to the javadoc
+            writer.newLine();
+            writer.writeInlineWithNoFormatting(previousText);
+        }
+    }
+
+    @Override
+    public Class<JavaDocSection> sectionType() {
+        return JavaDocSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(JavaDocSection section) {
+        return section.shape().hasTrait(DocumentationTrait.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java
deleted file mode 100644
index b3e75dfb3..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DocumentationTrait;
-import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
-import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds a docstring to each java enum variant if the corresponding enum member has a doc comment.
- */
-final class EnumVariantJavaDocInterceptor implements CodeInterceptor.Prepender<EnumVariantSection,
-        TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, EnumVariantSection section) {
-        DocumentationTrait trait = section.memberShape().expectTrait(DocumentationTrait.class);
-        writer.newLine();
-        writer.openDocstring();
-        writer.pushState(new JavaDocSection(section.memberShape()));
-        writer.writeDocStringContents(trait.getValue());
-        writer.popState();
-        writer.closeDocstring();
-    }
-
-    @Override
-    public Class<EnumVariantSection> sectionType() {
-        return EnumVariantSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(EnumVariantSection section) {
-        return section.memberShape().hasTrait(DocumentationTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocumentationInterceptor.java
similarity index 76%
rename from smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java
rename to smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocumentationInterceptor.java
index 6fb08ddac..519421da7 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocumentationInterceptor.java
@@ -14,16 +14,17 @@ import software.amazon.smithy.utils.CodeInterceptor;
 /**
  * Adds the javadoc {@code @see} tag to the generated javadocs if the corresponding smithy shape
  * has the {@link ExternalDocumentationTrait} trait applied.
+ *
+ * <p>Note: This interceptor should run before the {@link DeprecatedInterceptor} and {@link SinceInterceptor}
+ * to ensure proper ordering of Javadoc tags.
  */
-final class ExternalDocsInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
+final class ExternalDocumentationInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
 
     @Override
     public void append(TraitCodegenWriter writer, JavaDocSection section) {
         ExternalDocumentationTrait trait = section.shape().expectTrait(ExternalDocumentationTrait.class);
-        // Add a space to make it easier to read
-        writer.writeDocStringContents("");
         for (Map.Entry<String, String> entry : trait.getUrls().entrySet()) {
-            writer.writeDocStringContents("@see <a href=$S>$L</a>", entry.getKey(), entry.getValue());
+            writer.write("@see <a href=$S>$L</a>", entry.getValue(), entry.getKey());
         }
     }
 
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java
deleted file mode 100644
index c06cb2caa..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.DocumentationTrait;
-import software.amazon.smithy.traitcodegen.sections.GetterSection;
-import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-
-/**
- * Adds Javadocs to the getter for a member if the corresponding member has a {@link DocumentationTrait}
- * trait applied. The Javadoc contents will just be the contents of the {@code DocumentationTrait}.
- */
-final class GetterJavaDocInterceptor implements CodeInterceptor.Prepender<GetterSection, TraitCodegenWriter> {
-    @Override
-    public void prepend(TraitCodegenWriter writer, GetterSection section) {
-        DocumentationTrait trait = section.shape().expectTrait(DocumentationTrait.class);
-        writer.newLine();
-        writer.openDocstring();
-        writer.pushState(new JavaDocSection(section.shape()));
-        writer.writeDocStringContents(trait.getValue());
-        writer.popState();
-        writer.closeDocstring();
-    }
-
-    @Override
-    public Class<GetterSection> sectionType() {
-        return GetterSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(GetterSection section) {
-        return section.shape().hasTrait(DocumentationTrait.class)
-                && section.shape().isMemberShape();
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocInjectorInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocInjectorInterceptor.java
new file mode 100644
index 000000000..6a089e5b5
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocInjectorInterceptor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.traitcodegen.sections.ClassSection;
+import software.amazon.smithy.traitcodegen.sections.EnumVariantSection;
+import software.amazon.smithy.traitcodegen.sections.GetterSection;
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+import software.amazon.smithy.utils.CodeSection;
+
+/**
+ * Adds a javadoc section to classes, getters, and enum variants.
+ *
+ * <p>The injected javadoc section does not contain any javadoc contents when it is first injected. This
+ * section is instead populated by other {@code CodeInterceptors} that trigger off of specific traits.
+ * Note: This interceptor will also add any relevant documentation annotations to classes, getters, or enum variants.
+ */
+final class JavaDocInjectorInterceptor implements CodeInterceptor.Prepender<CodeSection, TraitCodegenWriter> {
+    @Override
+    public Class<CodeSection> sectionType() {
+        return CodeSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(CodeSection section) {
+        return section instanceof ClassSection
+                || section instanceof GetterSection
+                || section instanceof EnumVariantSection;
+    }
+
+    @Override
+    public void prepend(TraitCodegenWriter writer, CodeSection section) {
+        Shape shape;
+        if (section instanceof ClassSection) {
+            shape = ((ClassSection) section).shape();
+        } else if (section instanceof GetterSection) {
+            shape = ((GetterSection) section).shape();
+        } else if (section instanceof EnumVariantSection) {
+            shape = ((EnumVariantSection) section).memberShape();
+        } else {
+            throw new IllegalArgumentException("Javadocs cannot be injected for section: " + section);
+        }
+
+        writer.injectSection(new JavaDocSection(shape));
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java
index 9d3724088..02a8b4c91 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java
@@ -18,29 +18,26 @@ import software.amazon.smithy.utils.SmithyInternalApi;
  * Adds all built-in Javadoc-generating interceptors.
  *
  * <p>This integration adds all the required documentation interceptors that ensure
- * that methods, classes, and properties all have JavaDocs added. This integration also
- * adds Annotations such as {@code @Deprecated} that serve as documentation.
+ * that methods, classes, and properties all have JavaDocs added.
  */
 @SmithyInternalApi
 public final class JavaDocIntegration implements TraitCodegenIntegration  {
+    public static final String NAME = "javadoc";
 
     @Override
     public String name() {
-        return "javadoc";
+        return NAME;
     }
 
     @Override
     public List<? extends CodeInterceptor<? extends CodeSection, TraitCodegenWriter>> interceptors(
             TraitCodegenContext codegenContext) {
         return ListUtils.of(
-                new DeprecatedAnnotationClassInterceptor(),
-                new DeprecatedNoteInterceptor(),
-                new UnstableAnnotationClassInterceptor(),
-                new ClassJavaDocInterceptor(),
-                new ExternalDocsInterceptor(),
-                new BuilderClassSectionDocsInterceptor(),
-                new GetterJavaDocInterceptor(),
-                new EnumVariantJavaDocInterceptor()
+                new JavaDocInjectorInterceptor(),
+                new ExternalDocumentationInterceptor(),
+                new SinceInterceptor(),
+                new DeprecatedInterceptor(),
+                new DocumentationTraitInterceptor()
         );
     }
 }
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/SinceInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/SinceInterceptor.java
new file mode 100644
index 000000000..a1c413b8f
--- /dev/null
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/SinceInterceptor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import software.amazon.smithy.model.traits.SinceTrait;
+import software.amazon.smithy.traitcodegen.sections.JavaDocSection;
+import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
+import software.amazon.smithy.utils.CodeInterceptor;
+
+/**
+ * Adds the {@code @since} javadoc tag to generated Javadoc documentation for a class
+ * if the smithy shape the class corresponds to has the {@link SinceTrait} trait applied.
+ *
+ * <p>Note: This interceptor should be run before {@link DeprecatedInterceptor} and after
+ * {@link ExternalDocumentationInterceptor} to ensure proper tag ordering.
+ */
+final class SinceInterceptor implements CodeInterceptor.Appender<JavaDocSection, TraitCodegenWriter> {
+    @Override
+    public void append(TraitCodegenWriter writer, JavaDocSection section) {
+        SinceTrait trait = section.shape().expectTrait(SinceTrait.class);
+        writer.write("@since $L", trait.getValue());
+    }
+
+    @Override
+    public Class<JavaDocSection> sectionType() {
+        return JavaDocSection.class;
+    }
+
+    @Override
+    public boolean isIntercepted(JavaDocSection section) {
+        return section.shape().hasTrait(SinceTrait.class);
+    }
+}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java
deleted file mode 100644
index bdaaef545..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.integrations.javadoc;
-
-import software.amazon.smithy.model.traits.UnstableTrait;
-import software.amazon.smithy.traitcodegen.sections.ClassSection;
-import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter;
-import software.amazon.smithy.utils.CodeInterceptor;
-import software.amazon.smithy.utils.SmithyUnstableApi;
-
-/**
- * Adds the {@code @SmithyUnstableApi} annotation to generated classes if the smithy shape the class corresponds to
- * has the {@link UnstableTrait} trait applied.
- */
-final class UnstableAnnotationClassInterceptor implements CodeInterceptor.Prepender<ClassSection, TraitCodegenWriter> {
-
-    @Override
-    public void prepend(TraitCodegenWriter writer, ClassSection section) {
-        writer.write("@$T", SmithyUnstableApi.class);
-    }
-
-    @Override
-    public Class<ClassSection> sectionType() {
-        return ClassSection.class;
-    }
-
-    @Override
-    public boolean isIntercepted(ClassSection section) {
-        return section.shape().hasTrait(UnstableTrait.class);
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java
deleted file mode 100644
index de3fc05b9..000000000
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package software.amazon.smithy.traitcodegen.sections;
-
-import software.amazon.smithy.codegen.core.Symbol;
-import software.amazon.smithy.utils.CodeSection;
-
-/**
- * Contains the static builder class for a shape.
- */
-public final class BuilderClassSection implements CodeSection {
-    private final Symbol symbol;
-
-    public BuilderClassSection(Symbol symbol) {
-        this.symbol = symbol;
-    }
-
-    /**
-     * {@link Symbol} representing the enclosing class for this builder.
-     */
-    public Symbol symbol() {
-        return symbol;
-    }
-}
diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java
index 98fa58946..cd350891d 100644
--- a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java
+++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java
@@ -10,6 +10,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Scanner;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.regex.Matcher;
@@ -74,11 +75,14 @@ public class TraitCodegenWriter extends SymbolWriter<TraitCodegenWriter, TraitCo
         addImport(symbol, symbol.getName());
     }
 
-    public void openDocstring() {
-        pushState().writeWithNoFormatting("/**");
-    }
+    /**
+     * Writes the provided text in the format of a Java doc string.
+     *
+     * @param contents text to format as a doc string.
+     */
+    public void writeDocString(String contents) {
+        writeWithNoFormatting("/**");
 
-    public void writeDocStringContents(String contents) {
         // Split out any HTML-tag wrapped sections as we do not want to wrap
         // any customer documentation with tags
         Matcher matcher = PATTERN.matcher(contents);
@@ -86,28 +90,26 @@ public class TraitCodegenWriter extends SymbolWriter<TraitCodegenWriter, TraitCo
         writeInlineWithNoFormatting(" * ");
         while (matcher.find()) {
             // write all contents up to the match.
-            writeInlineWithNoFormatting(StringUtils.wrap(contents.substring(lastMatchPos, matcher.start())
-                            .replace("\n", "\n * "), MAX_LINE_LENGTH - 8,
-                    getNewline() + " * ", false));
+            writeDocStringLine(contents.substring(lastMatchPos, matcher.start()));
+
             // write match contents
             writeInlineWithNoFormatting(contents.substring(matcher.start(), matcher.end()).replace("\n", "\n * "));
             lastMatchPos = matcher.end();
         }
-        // Write out all remaining contents
-        writeWithNoFormatting(StringUtils.wrap(contents.substring(lastMatchPos).replace("\n", "\n * "),
-                MAX_LINE_LENGTH - 8,
-                getNewline() + " * ",
-                false));
-    }
 
-    public void writeDocStringContents(String contents, Object... args) {
-        writeInlineWithNoFormatting(" * ");
-        write(StringUtils.wrap(contents.replace("\n", "\n * "), MAX_LINE_LENGTH - 8,
-                getNewline() + " * ", false), args);
+        // Write out all remaining contents
+        writeDocStringLine(contents.substring(lastMatchPos));
+        writeWithNoFormatting("\n */");
     }
 
-    public void closeDocstring() {
-        writeWithNoFormatting(" */").popState();
+    private void writeDocStringLine(String string) {
+        for (Scanner it = new Scanner(string); it.hasNextLine();) {
+            String s = it.nextLine();
+            writeInlineWithNoFormatting(StringUtils.wrap(s, MAX_LINE_LENGTH, getNewline() + " * ", false));
+            if (it.hasNextLine()) {
+                writeInlineWithNoFormatting(getNewline() + " * ");
+            }
+        }
     }
 
     @Override
diff --git a/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration
index 86df7dfb4..05f33b559 100644
--- a/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration
+++ b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration
@@ -1,3 +1,4 @@
 software.amazon.smithy.traitcodegen.integrations.core.CoreIntegration
 software.amazon.smithy.traitcodegen.integrations.javadoc.JavaDocIntegration
 software.amazon.smithy.traitcodegen.integrations.idref.IdRefDecoratorIntegration
+software.amazon.smithy.traitcodegen.integrations.annotations.AnnotationIntegration


Test patch:
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
index 75820a20e..422f78443 100644
--- a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java
@@ -17,6 +17,7 @@ import java.nio.file.Paths;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.build.MockManifest;
 import software.amazon.smithy.build.PluginContext;
@@ -29,13 +30,20 @@ import software.amazon.smithy.model.node.ObjectNode;
 public class TraitCodegenPluginTest {
     private static final int EXPECTED_NUMBER_OF_FILES = 55;
 
-    @Test
-    public void generatesExpectedTraitFiles() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
+    private MockManifest manifest;
+    private Model model;
+
+    @BeforeEach
+    void setup() {
+        manifest = new MockManifest();
+        model = Model.assembler()
                 .discoverModels(getClass().getClassLoader())
                 .assemble()
                 .unwrap();
+    }
+
+    @Test
+    public void generatesExpectedTraitFiles() {
         PluginContext context = PluginContext.builder()
                 .fileManifest(manifest)
                 .settings(ObjectNode.builder()
@@ -63,11 +71,6 @@ public class TraitCodegenPluginTest {
 
     @Test
     public void filtersTags() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
-                .discoverModels(getClass().getClassLoader())
-                .assemble()
-                .unwrap();
         PluginContext context = PluginContext.builder()
                 .fileManifest(manifest)
                 .settings(ObjectNode.builder()
@@ -89,11 +92,6 @@ public class TraitCodegenPluginTest {
 
     @Test
     public void addsHeaderLines() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
-                .discoverModels(getClass().getClassLoader())
-                .assemble()
-                .unwrap();
         PluginContext context = PluginContext.builder()
                 .fileManifest(manifest)
                 .settings(ObjectNode.builder()
@@ -118,49 +116,4 @@ public class TraitCodegenPluginTest {
                 " * Header line two\n" +
                 " */"));
     }
-
-    @Test
-    public void doesNotFormatContentInsideHtmlTags() {
-        MockManifest manifest = new MockManifest();
-        Model model = Model.assembler()
-                .discoverModels(getClass().getClassLoader())
-                .assemble()
-                .unwrap();
-        PluginContext context = PluginContext.builder()
-                .fileManifest(manifest)
-                .settings(ObjectNode.builder()
-                        .withMember("package", "com.example.traits")
-                        .withMember("namespace", "test.smithy.traitcodegen")
-                        .withMember("header", ArrayNode.fromStrings("Header line one", "Header line two"))
-                        .build()
-                )
-                .model(model)
-                .build();
-
-        SmithyBuildPlugin plugin = new TraitCodegenPlugin();
-        plugin.execute(context);
-
-        assertFalse(manifest.getFiles().isEmpty());
-        assertEquals(EXPECTED_NUMBER_OF_FILES, manifest.getFiles().size());
-        Optional<String> fileStringOptional = manifest.getFileString(
-                Paths.get("com/example/traits/structures/StructureTrait.java").toString());
-        assertTrue(fileStringOptional.isPresent());
-        String expected = "    /**\n"
-                          + "     * Documentation includes preformatted text that should not be messed with. This sentence should still be partially\n"
-                          + "     * wrapped.\n"
-                          + "     * For example:\n"
-                          + "     * <pre>\n"
-                          + "     * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n"
-                          + "     * </pre>\n"
-                          + "     *\n"
-                          + "     * <ul>\n"
-                          + "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit </li>\n"
-                          + "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit </li>\n"
-                          + "     * </ul>\n"
-                          + "     */\n"
-                          + "    public Optional<List<String>> getFieldD() {\n"
-                          + "        return Optional.ofNullable(fieldD);\n"
-                          + "    }\n";
-        assertTrue(fileStringOptional.get().contains(expected));
-    }
 }
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationsTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationsTest.java
new file mode 100644
index 000000000..b49af3c5c
--- /dev/null
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/annotations/AnnotationsTest.java
@@ -0,0 +1,136 @@
+package software.amazon.smithy.traitcodegen.integrations.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.MockManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.traitcodegen.TraitCodegenPlugin;
+
+public class AnnotationsTest {
+
+    private static final URL TEST_FILE = Objects.requireNonNull(AnnotationsTest.class.getResource("annotations-test.smithy"));
+    private final MockManifest manifest = new MockManifest();
+
+    @BeforeEach
+    void setup() {
+        Model model = Model.assembler()
+                .addImport(TEST_FILE)
+                .assemble()
+                .unwrap();
+        PluginContext context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(
+                        ObjectNode.builder()
+                                .withMember("package", "com.example.traits")
+                                .withMember("namespace", "com.example.annotations")
+                                .withMember("header", ArrayNode.fromStrings("Header line One"))
+                                .build()
+                )
+                .model(model)
+                .build();
+        SmithyBuildPlugin plugin = new TraitCodegenPlugin();
+        plugin.execute(context);
+
+        assertFalse(manifest.getFiles().isEmpty());
+    }
+
+    @Test
+    void hasSmithyGeneratedAnnotationEvenIfNoDocstring() {
+        String fileContents = getFileContentsFromShapeName("HasSmithyGeneratedClass", true);
+        String fileContentsNested = getFileContentsFromShapeName("HasSmithyGeneratedNested", false);
+
+        assertTrue(fileContents.contains("@SmithyGenerated"));
+        assertTrue(fileContentsNested.contains("@SmithyGenerated"));
+    }
+
+    @Test
+    void deprecatedAnnotationOnClass() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "@Deprecated\n" +
+                "@SmithyGenerated\n" +
+                "public final class DeprecatedStructureTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationWithDocsOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    /**\n" +
+                "     * Has docs in addition to deprecated\n" +
+                "     */\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedWithDocs() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationOnClass() {
+        String fileContents = getFileContentsFromShapeName("UnstableStructure", true);
+        String expected = "@SmithyUnstableApi\n" +
+                "@SmithyGenerated\n" +
+                "public final class UnstableStructureTrait extends AbstractTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationOnMember() {
+        String fileContents = getFileContentsFromShapeName("UnstableStructure", true);
+        String expected = "    @SmithyUnstableApi\n" +
+                "    public Optional<String> getUnstableMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationWithDocsOnMember() {
+        String fileContents = getFileContentsFromShapeName("UnstableStructure", true);
+        String expected = "    /**\n" +
+                "     * Has docs in addition to unstable\n" +
+                "     */\n" +
+                "    @SmithyUnstableApi\n" +
+                "    public Optional<String> getUnstableWithDocs() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationOnEnumVariant() {
+        String fileContents = getFileContentsFromShapeName("EnumWithAnnotations", true);
+        String expected = "        @Deprecated\n" +
+                "        DEPRECATED(\"DEPRECATED\"),";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void unstableAnnotationOnEnumVariant() {
+        String fileContents = getFileContentsFromShapeName("EnumWithAnnotations", true);
+        String expected = "        @SmithyUnstableApi\n" +
+                "        UNSTABLE(\"UNSTABLE\"),";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    private String getFileContentsFromShapeName(String className, boolean isTrait) {
+        String suffix = isTrait ? "Trait" : "";
+        String path = String.format("com/example/traits/%s%s.java", className, suffix);
+        Optional<String> fileStringOptional = manifest.getFileString(Paths.get(path).toString());
+        assertTrue(fileStringOptional.isPresent());
+        return fileStringOptional.get();
+    }
+}
diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavadocTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavadocTest.java
new file mode 100644
index 000000000..c0f641300
--- /dev/null
+++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavadocTest.java
@@ -0,0 +1,229 @@
+package software.amazon.smithy.traitcodegen.integrations.javadoc;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.MockManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.traitcodegen.TraitCodegenPlugin;
+
+public class JavadocTest {
+
+    private static final URL TEST_FILE = Objects.requireNonNull(JavadocTest.class.getResource("javadoc-test.smithy"));
+    private final MockManifest manifest = new MockManifest();
+
+    @BeforeEach
+    void setup() {
+        Model model = Model.assembler()
+                .addImport(TEST_FILE)
+                .assemble()
+                .unwrap();
+        PluginContext context = PluginContext.builder()
+                .fileManifest(manifest)
+                .settings(
+                        ObjectNode.builder()
+                                .withMember("package", "com.example.traits")
+                                .withMember("namespace", "com.example.javadoc")
+                                .withMember("header", ArrayNode.fromStrings("Header line One"))
+                                .build()
+                )
+                .model(model)
+                .build();
+        SmithyBuildPlugin plugin = new TraitCodegenPlugin();
+        plugin.execute(context);
+
+        assertFalse(manifest.getFiles().isEmpty());
+    }
+
+    @Test
+    void hasClassLevelDocstring() {
+        String fileContents = getFileContentsFromShapeName("DocumentationWrapping", true);
+        String expected = "/**\n" +
+                " * Basic class-level documentation\n" +
+                " */\n" +
+                "@SmithyGenerated\n" +
+                "public final class DocumentationWrappingTrait extends AbstractTrait implements ToSmithyBuilder<DocumentationWrappingTrait> {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void wrapsBasicTextString() {
+        String fileContents = getFileContentsFromShapeName("DocumentationWrapping", true);
+        String expected = "    /**\n" +
+                "     * This is a long long docstring that should be wrapped. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n" +
+                "     * eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
+                "     */\n" +
+                "    public Optional<String> getShouldBeWrapped() {\n" +
+                "        return Optional.ofNullable(shouldBeWrapped);\n" +
+                "    }";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void doesNotFormatContentInsideHtmlTags() {
+        String fileContents = getFileContentsFromShapeName("DocumentationWrapping", true);
+        String expected = "    /**\n" +
+                "     * Documentation includes preformatted text that should not be messed with. This sentence should still be partially\n" +
+                "     * wrapped.\n" +
+                "     * For example:\n" +
+                "     * <pre>\n" +
+                "     * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
+                "     * </pre>\n" +
+                "     * <ul>\n" +
+                "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>\n" +
+                "     *     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>\n" +
+                "     * </ul>\n" +
+                "     */\n" +
+                "    public Optional<String> getPreformattedText() {\n" +
+                "        return Optional.ofNullable(preformattedText);\n" +
+                "    }";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationAndNoteOnClass() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "/**\n" +
+                " * @deprecated As of yesterday. A message\n" +
+                " */\n" +
+                "@Deprecated\n" +
+                "@SmithyGenerated\n" +
+                "public final class DeprecatedStructureTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationAndNoteOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    /**\n" +
+                "     * @deprecated As of yesterday. A message\n" +
+                "     */\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void deprecatedAnnotationAndNoteWithDocsOnMember() {
+        String fileContents = getFileContentsFromShapeName("DeprecatedStructure", true);
+        String expected = "    /**\n" +
+                "     * Has docs in addition to deprecated\n" +
+                "     *\n" +
+                "     * @deprecated As of yesterday. A message\n" +
+                "     */\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getDeprecatedWithDocs() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void sinceJavadocTagAddedToClass() {
+        String fileContents = getFileContentsFromShapeName("SinceStructure", true);
+        String expected = "/**\n" +
+                " * @since 1.2\n" +
+                " */\n" +
+                "@SmithyGenerated\n" +
+                "public final class SinceStructureTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void sinceJavaDocOnMember() {
+        String fileContents = getFileContentsFromShapeName("SinceStructure", true);
+        String expected = "    /**\n" +
+                "     * @since 1.2\n" +
+                "     */\n" +
+                "    public Optional<String> getSinceMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void externalDocumentationOnClass() {
+        String fileContents = getFileContentsFromShapeName("ExternalDocumentation", true);
+        String expected = "/**\n" +
+                " * @see <a href=\"https://example.com\">Example</a>\n" +
+                " * @see <a href=\"https://example.com\">Example2</a>\n" +
+                " */\n" +
+                "@SmithyGenerated\n" +
+                "public final class ExternalDocumentationTrait extends AbstractTrait";
+        assertTrue(fileContents.contains(expected));
+
+    }
+
+    @Test
+    void externalDocumentationOnMember() {
+        String fileContents = getFileContentsFromShapeName("ExternalDocumentation", true);
+        String expected = "    /**\n" +
+                "     * @see <a href=\"https://example.com\">Example</a>\n" +
+                "     */\n" +
+                "    public Optional<String> getMemberWithExternalDocumentation() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void allDocumentationIncludedTogetherOnClass() {
+        String fileContents = getFileContentsFromShapeName("Rollup", true);
+        String expected = "/**\n" +
+                " * This structure applies all documentation traits\n" +
+                " *\n" +
+                " * @see <a href=\"https://example.com\">Example</a>\n" +
+                " * @since 4.5\n" +
+                " * @deprecated As of sometime.\n" +
+                " */\n" +
+                "@SmithyUnstableApi\n" +
+                "@Deprecated\n" +
+                "@SmithyGenerated\n" +
+                "public final class RollupTrait extends AbstractTrait";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void allDocumentationIncludedTogetherOnMember() {
+        String fileContents = getFileContentsFromShapeName("Rollup", true);
+        String expected = "    /**\n" +
+                "     * This member applies all documentation traits\n" +
+                "     *\n" +
+                "     * @see <a href=\"https://example.com\">Example</a>\n" +
+                "     * @since 4.5\n" +
+                "     * @deprecated As of sometime.\n" +
+                "     */\n" +
+                "    @SmithyUnstableApi\n" +
+                "    @Deprecated\n" +
+                "    public Optional<String> getRollupMember() {";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    @Test
+    void allDocumentationIncludedTogetherEnumVariant() {
+        String fileContents = getFileContentsFromShapeName("EnumVariantsTest", true);
+        String expected = "        /**\n" +
+                "         * Just a documented variant\n" +
+                "         *\n" +
+                "         * @see <a href=\"https://example.com\">Example</a>\n" +
+                "         * @since 4.5\n" +
+                "         * @deprecated Really. Dont use this.\n" +
+                "         */\n" +
+                "        @SmithyUnstableApi\n" +
+                "        @Deprecated\n" +
+                "        A(\"A\"),";
+        assertTrue(fileContents.contains(expected));
+    }
+
+    private String getFileContentsFromShapeName(String className, boolean isTrait) {
+        String suffix = isTrait ? "Trait" : "";
+        String path = String.format("com/example/traits/%s%s.java", className, suffix);
+        Optional<String> fileStringOptional = manifest.getFileString(Paths.get(path).toString());
+        assertTrue(fileStringOptional.isPresent());
+        return fileStringOptional.get();
+    }
+}
diff --git a/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/annotations/annotations-test.smithy b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/annotations/annotations-test.smithy
new file mode 100644
index 000000000..8ec589625
--- /dev/null
+++ b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/annotations/annotations-test.smithy
@@ -0,0 +1,44 @@
+$version: "2"
+
+namespace com.example.annotations
+
+@trait
+structure HasSmithyGeneratedClass {
+    nested: HasSmithyGeneratedNested
+}
+
+structure HasSmithyGeneratedNested {
+    field: String
+}
+
+@trait
+@deprecated
+structure DeprecatedStructure {
+    @deprecated
+    deprecatedMember: String,
+
+    /// Has docs in addition to deprecated
+    @deprecated
+    deprecatedWithDocs: String
+}
+
+@trait
+@unstable
+structure UnstableStructure {
+    @unstable
+    unstableMember: String,
+
+    /// Has docs in addition to unstable
+    @unstable
+    unstableWithDocs: String
+}
+
+@trait
+enum EnumWithAnnotations {
+    @deprecated
+    DEPRECATED,
+    @unstable
+    UNSTABLE
+}
+
+
diff --git a/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/javadoc/javadoc-test.smithy b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/javadoc/javadoc-test.smithy
new file mode 100644
index 000000000..d9f154cc0
--- /dev/null
+++ b/smithy-trait-codegen/src/test/resources/software/amazon/smithy/traitcodegen/integrations/javadoc/javadoc-test.smithy
@@ -0,0 +1,80 @@
+$version: "2"
+
+namespace com.example.javadoc
+
+/// Basic class-level documentation
+@trait
+structure DocumentationWrapping {
+    /// This is a long long docstring that should be wrapped. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+    shouldBeWrapped: String,
+
+    /// Documentation includes preformatted text that should not be messed with. This sentence should still be partially wrapped.
+    /// For example:
+    ///
+    /// <pre>
+    /// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+    /// </pre>
+    ///
+    /// <ul>
+    ///     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>
+    ///     <li> Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit consectetur adipiscing </li>
+    /// </ul>
+    preformattedText: String
+}
+
+@trait
+@deprecated(message: "A message", since: "yesterday")
+structure DeprecatedStructure {
+    @deprecated(message: "A message", since: "yesterday")
+    deprecatedMember: String,
+
+    /// Has docs in addition to deprecated
+    @deprecated(message: "A message", since: "yesterday")
+    deprecatedWithDocs: String
+}
+
+@trait
+@since("1.2")
+structure SinceStructure {
+    @since("1.2")
+    sinceMember: String
+}
+
+@trait
+@externalDocumentation(
+    Example: "https://example.com",
+    Example2: "https://example.com"
+)
+structure ExternalDocumentation {
+    @externalDocumentation(Example: "https://example.com")
+    memberWithExternalDocumentation: String
+}
+
+
+/// This structure applies all documentation traits
+@trait
+@unstable
+@deprecated(since: "sometime")
+@externalDocumentation(Example: "https://example.com")
+@since("4.5")
+structure Rollup {
+    /// This member applies all documentation traits
+    @unstable
+    @deprecated(since: "sometime")
+    @externalDocumentation(Example: "https://example.com")
+    @since("4.5")
+    rollupMember: String
+}
+
+/// This enum applies all documentation traits to its variants
+@trait
+enum EnumVariantsTest {
+    /// Just a documented variant
+    @unstable
+    @deprecated(message: "Really. Dont use this.")
+    @externalDocumentation(Example: "https://example.com")
+    @since("4.5")
+    A,
+    B
+}
+