Repository: smithy-lang/smithy
Pull request #2073 (2023-12-13 19:57:15)

Add Annotation processor base class for executing Smithy-Build plugins
### Description of changes:
Adds a base class for annotation processors for Smithy build plugins. This annotation processor executes smithy build for a single plugin and adds the generated artifacts (java code or `META-INF/` metadata files) to the generated sources for the project. Annotation processors are easily supported in all java build systems and this processor will be used for the trait codegen plugin to easily add generated traits.

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/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ab40d21d7..fd0ef6457 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,14 @@
-*Issue #, if available:*
 
-*Description of changes:*
+#### Background
+* What do these changes do? 
+* Why are they important?
 
+#### Testing
+* How did you test these changes?
 
+#### Links
+* Links to additional context, if necessary
+* Issue #, if applicable (see [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for a list of keywords to use for linking issues)
+
+---
 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 96d1e5a61..f88326549 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
 
     steps:
       - uses: actions/checkout@v4
-      - uses: gradle/wrapper-validation-action@v1
+      - uses: gradle/wrapper-validation-action@v2
 
       - name: Set up JDK ${{ matrix.java }}
         uses: actions/setup-java@v4
diff --git a/.github/workflows/make-docs.yml b/.github/workflows/make-docs.yml
new file mode 100644
index 000000000..d79e73b0e
--- /dev/null
+++ b/.github/workflows/make-docs.yml
@@ -0,0 +1,86 @@
+name: make-docs
+on:
+  release:
+    types: [released]
+  workflow_dispatch: # on button click
+    inputs:
+      javadocs:
+        type: boolean
+        description: 'Build JavaDocs?'
+        required: true
+      release:
+        type: boolean
+        description: 'Release the docs?'
+        required: true
+
+env:
+  ARTIFACTS_DIR: /tmp/artifacts
+
+jobs:
+  build-docs:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout repo 
+      uses: actions/checkout@v4
+    - name: Setup
+      run: |
+        mkdir -p $ARTIFACTS_DIR
+
+    - name: Build JavaDocs
+      # run if 'javadocs' is not set to false 
+      if: ${{ !contains(inputs.javadocs, 'false') }}
+      run: |
+        VERSION=$(cat VERSION)
+        mkdir -p $ARTIFACTS_DIR/javadoc
+        ./gradlew javadoc
+        cp -R build/docs/javadoc/latest $ARTIFACTS_DIR/javadoc/$VERSION
+        mkdir $ARTIFACTS_DIR/javadoc/latest
+        cat << EOF > $ARTIFACTS_DIR/javadoc/latest/index.html
+        <!DOCTYPE html>
+        <meta charset="utf-8">
+        <title>Redirecting to latest javadocs</title>
+        <meta http-equiv="refresh" content="0; URL=../${VERSION}/">
+        EOF
+
+    - name: Build Smithy docs
+      run: |
+        cd docs
+        pip3 install -r requirements.txt
+        make clean
+        make html
+        cp -R build/html/* $ARTIFACTS_DIR
+        rm $ARTIFACTS_DIR/.buildinfo || true
+        rm $ARTIFACTS_DIR/objects.inv || true
+
+    - name: Upload artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: smithy-docs
+        path: ${{ env.ARTIFACTS_DIR }}
+        retention-days: 7
+  
+
+  publish-docs:
+    runs-on: ubuntu-latest
+    # run if 'release' is not set to false 
+    if: ${{ !contains(inputs.release, 'false') }}
+    needs: build-docs
+    steps:
+    - name: Checkout GitHub pages branch
+      uses: actions/checkout@v4
+      with:
+        ref: 'gh-pages'
+    - name: Download artifacts
+      id: download
+      uses: actions/download-artifact@v4
+      with:
+        name: smithy-docs
+        path: /tmp/smithy-docs
+    - name: Copy artifacts
+      run: |
+        cp -R /tmp/smithy-docs/* .
+        git config --global user.name "smithy-automation"
+        git config --global user.email "github-smithy-automation@amazon.com"
+        git add -A
+        git commit -m "Update documentation"
+        git push
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79eb4ba8e..59ab6f48c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
 # Smithy Changelog
 
+## 1.45.0 (2024-02-14)
+
+### Features
+
+* Added new option to CLI to configure the format (`text` or `csv`) of validation output. ([#2133](https://github.com/smithy-lang/smithy/pull/2133))
+* Added options to CLI to hide or show validation events for specified validators. ([#2127](https://github.com/smithy-lang/smithy/pull/2127))
+* Added protocol tests for verifying serialization/deserialization behavior for maps with document values. ([#2125](https://github.com/smithy-lang/smithy/pull/2125))
+* Changed `UnreferencedShape` to be an opt-in linter instead of on-by-default to reduce friction when defining common shapes that are not connected to a service shape. Added a validator that allows you to configure what shape to check connectedness (defaults to service shape). ([#2119](https://github.com/smithy-lang/smithy/pull/2119))
+
+### Bug Fixes
+
+* Fixed headers when printing validation event output in csv format. ([#2136](https://github.com/smithy-lang/smithy/pull/2136))
+* Fixed `RestJsonZeroAndFalseQueryValues` protocol test to correctly include a params value for servers. ([#2132](https://github.com/smithy-lang/smithy/pull/2132))
+* Fixed regression in model validation that incorrectly allowed suppression of ERROR events. ([#2130](https://github.com/smithy-lang/smithy/pull/2130))
+
+### Documentation
+
+* Fixed typo in @pattern validation example. ([#2126](https://github.com/smithy-lang/smithy/pull/2126))
+
+
 ## 1.44.0 (2024-01-25)
 
 ### Features
diff --git a/README.md b/README.md
index 637d3ea99..de103e253 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
 # <img alt="Smithy" src="docs/_static/favicon.png" width="28"> Smithy
 [![Build Status](https://github.com/smithy-lang/smithy/workflows/ci/badge.svg)](https://github.com/smithy-lang/smithy/actions/workflows/ci.yml)
 
+<strong> We want your feedback on Smithy.</strong>
+Please consider taking our brief <a href="https://amazonmr.au1.qualtrics.com/jfe/form/SV_9Rfqo2wizSpnZPM">survey</a>
+to share your feedback.
+
+---
+
 Smithy defines and generates clients, services, and documentation for
 any protocol.
 
@@ -42,26 +48,19 @@ guide][quickstart].
 
 
 # Building Smithy models
+> [!IMPORTANT]  
+> Before you proceed, make sure you have the [Smithy CLI installed](https://smithy.io/2.0/guides/smithy-cli/cli_installation.html#cli-installation).
 
-The [Smithy Gradle Plugin] is the best way to get started with building a
-Smithy model. First, create a [`smithy-build.json`] file:
+The [Smithy CLI] is the easiest way to get started with building Smithy models. First, create a [`smithy-build.json`] file:
 
 ```json
 {
-    "version": "1.0"
+    "version": "1.0",
+    "sources": ["model"]
 }
 ```
 
-Then, apply the Smithy Gradle Plugin in your `build.gradle.kts` file and run
-`gradle build`:
-
-```kotlin
-plugins {
-   id("software.amazon.smithy").version("0.6.0")
-}
-```
-
-Finally, create your first model `model/main.smithy`:
+Next, create your first model `model/main.smithy`:
 
 ```smithy
 $version: "2"
@@ -73,10 +72,11 @@ service ExampleService {
 }
 ```
 
+Finally, run `smithy build` to build the model with the [Smithy CLI].
+
 Find out more about building artifacts of your Smithy model in the [Building
 Smithy Models][building] guide. For more examples, see the
-[examples directory](https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples)
-of the Smithy Gradle Plugin repository.
+[examples repository](https://github.com/smithy-lang/smithy-examples)
 
 # License
 
@@ -87,6 +87,7 @@ This library is licensed under the Apache 2.0 License.
 [javadocs]: https://smithy.io/javadoc/latest/
 [quickstart]: https://smithy.io/2.0/quickstart.html
 [Smithy Gradle Plugin]: https://github.com/awslabs/smithy-gradle-plugin/
+[Smithy CLI]: https://smithy.io/2.0/guides/smithy-cli/index.html
 [`smithy-build.json`]: https://smithy.io/2.0/guides/building-models/build-config.html#using-smithy-build-json
 [building]: https://smithy.io/2.0/guides/building-models/index.html
 [awesome-smithy]: https://github.com/smithy-lang/awesome-smithy
diff --git a/VERSION b/VERSION
index 372cf402c..50aceaa7b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.44.0
+1.45.0
diff --git a/build.gradle b/build.gradle
index f246dd12b..bdeb05806 100644
--- a/build.gradle
+++ b/build.gradle
@@ -326,8 +326,8 @@ if (useJreleaser) {
                         active = "ALWAYS"
                         url = "https://aws.oss.sonatype.org/service/local"
                         snapshotUrl = "https://aws.oss.sonatype.org/content/repositories/snapshots"
-                        closeRepository = false
-                        releaseRepository = false
+                        closeRepository = true
+                        releaseRepository = true
                         stagingRepository(stagingDirectory.get().toString())
                     }
                 }
diff --git a/docs/Makefile b/docs/Makefile
index b0a870ca9..4e191c35b 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -22,7 +22,7 @@ merge-versions:
 	mkdir -p build/html
 	cp -R "build/1.0/html" "build/html/1.0"
 	cp -R "build/2.0/html" "build/html/2.0"
-	cp -R "root/" "build/html"
+	cp -R root/* "build/html"
 
 openhtml:
 	open "build/html/index.html"
diff --git a/docs/source-2.0/conf.py b/docs/source-2.0/conf.py
index 9a2c11465..fc6fb0ec3 100644
--- a/docs/source-2.0/conf.py
+++ b/docs/source-2.0/conf.py
@@ -12,3 +12,7 @@ release = u'2.0'
 version = release
 
 html_theme_options['source_directory'] = "docs/source-2.0"
+html_theme_options['announcement'] = '''<strong> We want your feedback on Smithy.</strong>
+Please consider taking our brief <a href="https://amazonmr.au1.qualtrics.com/jfe/form/SV_9Rfqo2wizSpnZPM">survey</a>
+ to share your feedback.
+'''
diff --git a/docs/source-2.0/guides/gradle-plugin.rst b/docs/source-2.0/guides/gradle-plugin.rst
deleted file mode 100644
index 6a8fa7fa6..000000000
--- a/docs/source-2.0/guides/gradle-plugin.rst
+++ /dev/null
@@ -1,345 +0,0 @@
-.. _smithy-gradle-plugin:
-
-====================
-Smithy Gradle Plugin
-====================
-
-The `Smithy Gradle plugin`_ integrates Smithy with `Gradle`_. This plugin can
-build artifacts from Smithy models, generate JARs that contain Smithy models
-found in Java projects, and generate JARs that contain filtered :ref:`projections <projections>`
-of Smithy models.
-
-
-.. _plugin-install:
-
-Installation
-============
-
-The Smithy Gradle plugin is applied using the ``software.amazon.smithy`` plugin.
-The following example configures a project to use the Smithy Gradle plugin:
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-
-.. _smithy-model-sources:
-
-Smithy model sources
-====================
-
-When a JAR is generated for a project, any Smithy models found in the
-following directories will be added to the JAR:
-
-* ``model/``
-* ``src/main/smithy``
-* ``src/main/resources/META-INF/smithy``
-
-Models found in these directories are combined into a flattened directory
-structure and used to validate and build the Smithy model. A Smithy manifest
-file is automatically created for the detected models, and it along with the
-model files, are placed in the ``META-INF/smithy/`` resource of the created
-JAR. Any project that then depends on this created JAR will be able to find
-and use the Smithy models contained in the JAR when using *model discovery*.
-
-
-.. _building-smithy-models:
-
-Building Smithy models
-======================
-
-This plugin operates in two different modes:
-
-1. If no ``projection`` is specified for the ``SmithyExtension``, then the plugin
-   runs a "source" build using the "source" projection.
-2. If a ``projection`` is specified for the ``SmithyExtension``, then the plugin
-   runs in a "projection" mode.
-
-
-.. _smithy-extension-properties:
-
-Smithy extension properties
-===========================
-
-This plugin is configured using the ``software.amazon.smithy.gradle.SmithyExtension``
-extension:
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        projection = "foo"
-    }
-
-
-This extension supports the following properties:
-
-.. list-table::
-    :header-rows: 1
-    :widths: 10 20 70
-
-    * - Property
-      - Type
-      - Description
-    * - projection
-      - ``String``
-      - Sets the :ref:`projection <projections>` name to build. There must
-        be a corresponding projection definition in the :ref:`smithy-build.json <smithy-build-json>`
-        file in the project.
-    * - projectionSourceTags
-      - ``Set<String>``
-      - Get the tags that are searched for in classpaths when determining
-        which models are projected into the created JAR. This plugin will look
-        through the JARs in the buildscript classpath to see if they contain a
-        META-INF/MANIFEST.MF attribute named "Smithy-Tags" that matches any of
-        the given projection source tags. The Smithy models found in each
-        matching JAR are copied into the JAR being projected. This allows a
-        projection JAR to aggregate models into a single JAR.
-    * - tags
-      - ``Set<String>``
-      - Set the tags that are added to the JAR. These tags are placed in the
-        META-INF/MANIFEST.MF attribute named "Smithy-Tags" as a comma
-        separated list. JARs with Smithy-Tags can be queried when building
-        projections so that the Smithy models found in each matching JAR are
-        placed into the projection JAR.
-    * - smithyBuildConfigs
-      - ``FileCollection``
-      - Sets a custom collection of smithy-build.json files to use when
-        building the model.
-    * - allowUnknownTraits
-      - ``boolean``
-      - Sets whether or not unknown traits in the model should be ignored. By
-        default, the build will fail if unknown traits are encountered.
-    * - outputDirectory
-      - ``File``
-      - Defines where Smithy build artifacts are written.
-
-
-.. _building-source-model:
-
-Building a source model
------------------------
-
-A "source" build is run when no ``projection`` is configured in
-``SmithyExtension``. Because no projection was specified, **smithy-build** is
-executed using the ``compileClasspath`` plus the ``buildscript`` classpath. To
-prevent accidentally relying on Smithy models that are only available to
-build scripts, Smithy models are discovered using only the
-``compileClasspath`` and ``runtimeClasspath``.
-
-The following example ``build.gradle.kts`` will build a Smithy model using a
-"source" build:
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-    // The SmithyExtension is used to customize the build. This example
-    // doesn't set any values and can be completely omitted.
-    configure<software.amazon.smithy.gradle.SmithyExtension> {}
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-
-        // These are just examples of dependencies. This model has a dependency on
-        // a "common" model package and uses the external AWS traits.
-        implementation("com.foo.baz:foo-model-internal-common:1.0.0")
-        implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-    }
-
-
-.. _generating-projection:
-
-Generating a projection
------------------------
-
-A "projection" build is run when a :ref:`projection <projections>` is
-specified in the ``SmithyExtension``. You might want to create a projection of
-a model if you need to maintain an internal version of a model that contains
-more information and features than an external version of a model published to
-your customers.
-
-A "projection" build is executed using only the ``buildscript`` classpath, and
-Smithy models are discovered using only the ``buildscript`` classpath. This
-prevents models discovered in the original model from appearing in the
-projected version of the model.
-
-The following example ``build.gradle.kts`` file will run a "projection"
-build that uses the "external" projection.
-
-.. code-block:: kotlin
-
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
-
-    buildscript {
-        repositories {
-            mavenLocal()
-            mavenCentral()
-        }
-        dependencies {
-            classpath("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-
-            // Take a dependency on the internal model package. This
-            // dependency *must* be a buildscript only dependency to ensure
-            // that is does not appear in the generated JAR.
-            classpath("com.foo.baz:foo-internal-model:1.0.0")
-        }
-    }
-
-    // Use the "external" projection. This projection must be found in the
-    // smithy-build.json file. This also ensures that models found in the
-    // foo-internal-package that weren't filtered out are copied into the
-    // projection created by this package.
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        projection = "external"
-        projectionSourceTags = setOf("com.foo.baz:foo-internal-model")
-    }
-
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
-
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-
-        // Any dependencies that the projected model needs must be (re)declared
-        // here. For example, let's assume that the smithy-aws-traits package is
-        // needed in the projected model too.
-        implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
-    }
-
-
-Because the ``projection`` of the ``SmithyExtension`` was set to ``external``, a
-``smithy-build.json`` file **must** be found that defines the ``external``
-projection. For example:
-
-.. code-block:: json
-
-    {
-        "version": "1.0",
-        "projections": {
-            "external": {
-                "transforms": [
-                    {
-                        "name": "excludeShapesByTag",
-                        "args": {
-                            "tags": ["internal"]
-                        }
-                    },
-                    {
-                        "name": "excludeShapesByTrait",
-                        "args": {
-                            "traits": ["internal"]
-                        }
-                    },
-                    {
-                        "name": "excludeMetadata",
-                        "args": {
-                            "keys": ["suppressions", "validators"]
-                        }
-                    },
-                    {
-                        "name": "removeUnusedShapes"
-                    }
-                ]
-            }
-        }
-    }
-
-
-.. _projection-tags:
-
-Projection tags
----------------
-
-Projections are meant to *project* and filter other models into another
-model. You need to specify which dependencies are being projected into your
-JAR by setting the ``projectionSourceTags`` property.
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-            projection = "external"
-            projectionSourceTags = setOf("Foo", "com.baz:bar")
-    }
-
-
-Tags are used to logically group packages to make it easier to build
-projections. The ``tags`` property is used to add ``Smithy-Tags`` to a JAR.
-
-.. code-block:: kotlin
-
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        tags = setOf("X", "foobaz-model")
-    }
-
-
-For example, if your service is made up of 10 packages, you can add the
-"foobaz-model" Smithy tag to each package so that the only value that needs
-to be provided for ``tags`` to correctly project your model is "foobaz-model".
-
-When building a model package, this plugin will automatically add the group
-name of the package being built, the group name + ":" + name of the package,
-and group name + ":" + name + ":" version. This allows models to always
-be queried by group and artifact names in addition to custom tags.
-
-
-.. _artifacts-from-smithy-models:
-
-Building artifacts from Smithy models
--------------------------------------
-
-If a ``smithy-build.json`` file is found at the root of the project, then it
-will be used to generate :ref:`artifacts <projection-artifacts>` from the Smithy model.
-
-The following example generates an OpenAPI model from a Smithy model:
-
-.. code-block:: json
-
-    {
-        "version": "1.0",
-        "plugins": {
-            "openapi": {
-                "service": "foo.baz#MyService"
-            }
-        }
-    }
-
-
-The above Smithy plugin also requires a ``buildscript`` dependency in
-``build.gradle.kts``:
-
-.. code-block:: kotlin
-
-    buildscript {
-        // ...
-        dependencies {
-            // ...
-
-            // This dependency is required in order to apply the "openapi"
-            // plugin in smithy-build.json
-            classpath("software.amazon.smithy:smithy-openapi:__smithy_version__")
-        }
-    }
-
-Complete Examples
-=================
-
-For several complete examples, see the `examples directory`_ of the Smithy
-Gradle plugin repository, or check out the :doc:`Quick start guide </quickstart>` for a tutorial on
-creating a Smithy model and building it with the Smithy Gradle plugin.
-
-.. _examples directory: https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples
-.. _Smithy Gradle plugin: https://github.com/awslabs/smithy-gradle-plugin/
-.. _Gradle: https://gradle.org/
diff --git a/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst b/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst
new file mode 100644
index 000000000..293d7c6f4
--- /dev/null
+++ b/docs/source-2.0/guides/gradle-plugin/gradle-migration-guide.rst
@@ -0,0 +1,186 @@
+.. _gradle_migration_guide:
+
+==========================================
+Migrating to Gradle plugin version 0.10.0+
+==========================================
+
+This guide describes how to migrate your Gradle build files
+to use the `Smithy Gradle plugins`_ v0.10.0+ without breaking
+your existing Gradle projects.
+
+.. warning::
+    Versions 0.10.0+ of the Smithy Gradle plugins will automatically apply
+    the Smithy formatter to your Smithy source files. This can result in a
+    large number of formatting changes the first time you apply these plugins
+    to your project. For instructions on how to disable automatic formatting
+    see :ref:`Disabling the Smithy formatter <disable-smithy-formatter>`.
+
+Update plugin artifact
+======================
+
+The Smithy Gradle plugin has been broken up into separate capability
+(``smithy-jar``) and convention (``smithy-base``) plugins. The
+``smithy-jar`` plugin operates similarly to the previous ``smithy`` plugin,
+but no longer applies the ``java`` plugin automatically. To migrate to the
+new plugin, change the artifact id in the ``plugins`` block and apply the
+``java`` (or ``java-library``) plugin if it is not already applied to
+your project.
+
+.. tab:: Kotlin
+
+    .. code-block:: diff
+        :caption: build.gradle.kts
+
+        plugins {
+        -     id("software.amazon.smithy").version("0.7.0")
+        +     `java`
+        +     id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: diff
+        :caption: build.gradle
+
+        plugins {
+        -      id 'software.amazon.smithy' version '0.7.0'
+        +      id 'java'
+        +      id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+        }
+
+Remove Buildscript Dependencies
+===============================
+
+Pre-0.10.0 versions of the Smithy Gradle plugins use the buildscript block
+for build-only dependencies such as Smithy build plugins. The 0.10.0+
+Gradle plugins create a new Gradle `Configuration`_ ``smithyBuild``
+for these build-only dependencies.
+
+.. tab:: Kotlin
+
+    .. code-block:: diff
+        :caption: build.gradle.kts
+
+        -buildscript {
+        -       repositories {
+        -           mavenLocal()
+        -           mavenCentral()
+        -       }
+        -
+        -       dependencies {
+        -           // Dependency repeated here and below because model discovery previously
+        -           // only ran on buildscript classpath for projections
+        -           classpath("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        -           // Take a dependency on the internal model package. This
+        -           // dependency *must* be a buildscript only dependency to ensure
+        -           // that is does not appear in the generated JAR.
+        -           classpath("com.foo.baz:foo-internal-model:1.0.0")
+        -       }
+        -}
+
+        dependencies {
+            implementation("software.amazon.smithy:smithy-model:__smithy_version__")
+
+            implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        +   smithyBuild("com.foo.baz:foo-internal-model:1.0.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: diff
+        :caption: build.gradle
+
+        -buildscript {
+        -       repositories {
+        -           mavenLocal()
+        -           mavenCentral()
+        -       }
+        -
+        -       dependencies {
+        -           // Dependency repeated here and below because model discovery previously
+        -           // only ran on buildscript classpath for projections
+        -           classpath 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        -           // Take a dependency on the internal model package. This
+        -           // dependency *must* be a buildscript only dependency to ensure
+        -           // that is does not appear in the generated JAR.
+        -           classpath 'com.foo.baz:foo-internal-model:1.0.0'
+        -       }
+        -}
+
+        dependencies {
+            implementation 'software.amazon.smithy:smithy-model:__smithy_version__'
+
+            implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        +   smithyBuild 'com.foo.baz:foo-internal-model:1.0.0'
+        }
+
+Change ``projection`` property name
+===================================
+
+The property ``projection`` has also been updated to ``sourceProjection``.
+
+.. tab:: Kotlin
+
+        .. code-block:: diff
+            :caption: build.gradle.kts
+
+            -configure<software.amazon.smithy.gradle.SmithyExtension> {
+            +smithy {
+            -    projection = "foo"
+            +    sourceProjection.set("foo")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: diff
+            :caption: build.gradle
+
+            -configure<software.amazon.smithy.gradle.SmithyExtension> {
+            +smithy {
+            -    projection = "foo"
+            +    sourceProjection = "foo"
+            }
+
+Change ``smithyBuildJar`` task name
+===================================
+
+The ``smithyBuildJar`` task has been removed. Instead, the plugin now
+executes separate ``smithyBuild`` and ``smithyJarStaging`` tasks. If
+your project previously configured the ``smithyBuildJar``,
+configure the ``smithyBuild`` task instead. Tasks that depended on
+``smithyBuildJar`` should now depend on the ``jar`` task.
+
+.. tab:: Kotlin
+
+        .. code-block:: diff
+            :caption: build.gradle.kts
+
+            tasks {
+            -   smithyBuildJar {
+            +   smithyBuild {
+                    smithyBuildConfigs.set(files("smithy-build.json", other))
+                }
+                // ..
+            }
+
+            -tasks["smithyBuildJar"].dependsOn("otherTask")
+            +tasks["jar"].dependsOn("otherTask")
+
+.. tab:: Groovy
+
+        .. code-block:: diff
+            :caption: build.gradle
+
+            tasks {
+            -   smithyBuildJar {
+            +   smithyBuild {
+                    smithyBuildConfigs = files("smithy-build.json", other)
+                }
+                // ..
+            }
+
+            -tasks["smithyBuildJar"].dependsOn("otherTask")
+            +tasks["jar"].dependsOn("otherTask")
+
+.. _Smithy Gradle plugins: https://github.com/awslabs/smithy-gradle-plugin/
+.. _Configuration: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html
diff --git a/docs/source-2.0/guides/gradle-plugin/index.rst b/docs/source-2.0/guides/gradle-plugin/index.rst
new file mode 100644
index 000000000..4c6db0a8a
--- /dev/null
+++ b/docs/source-2.0/guides/gradle-plugin/index.rst
@@ -0,0 +1,760 @@
+.. _smithy-gradle-plugin:
+
+=====================
+Smithy Gradle Plugins
+=====================
+
+The `Smithy Gradle plugins`_ integrate Smithy with the `Gradle`_ build system. These plugin can
+build artifacts from Smithy models, and generate JARs for Smithy models and model
+:ref:`projections <projections>`.
+
+.. toctree::
+    :maxdepth: 1
+    :caption: Migrate to version 0.8.0+
+
+    gradle-migration-guide
+
+Plugins
+=======
+
+Two official `Gradle`_ plugins are available:
+
+* `smithy-base`_:
+    This plugin configures the `SourceSet`_'s and `Configuration`_'s
+    for a Smithy project. It also creates the base ``smithyBuild`` task for the project that
+    builds the Smithy models and build artifacts for the project.
+* `smithy-jar`_:
+    Adds Smithy files to an existing JAR created by a ``jar`` task (usually created
+    by the `Java plugin`_ or `Kotlin JVM plugin`_). The ``smithy-jar`` plugin also adds build metadata
+    and tags to the JAR's MANIFEST. The ``smithy-jar`` plugin applies the ``smithy-base`` plugin when
+    applied.
+
+.. admonition:: Which plugin should I use?
+    :class: note
+
+    Most users should use the ``smithy-jar`` plugin. If you are building shared model packages,
+    custom traits, custom linters, or codegen integrations you should use the ``smithy-jar`` plugin.
+    If you are writing a code generator you can use the ``smithy-base`` plugin to set up basic
+    `Configuration`_'s and provide access to Smithy gradle tasks.
+
+.. _plugin-apply:
+
+Applying Plugins
+----------------
+
+The Smithy Gradle plugins are applied using the ``plugins`` block of a gradle build script.
+The following example configures a project to use the ``smithy-base`` Gradle plugin:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        plugins {
+            id("software.amazon.smithy.gradle.smithy-base").version("0.10.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        plugins {
+            id 'software.amazon.smithy.gradle.smithy-base' version '0.10.0'
+        }
+
+
+.. _smithy-model-sources:
+
+.. note::
+
+    The ``smithy-jar`` plugin requires that a `jar` task be registered
+    before it is applied. The `Java Plugin`_ and `Kotlin JVM plugin`_
+    are commonly used plugins that both register a `jar` task.
+
+Complete Examples
+=================
+
+For several complete examples, see the `examples directory`_ of the `Smithy
+Gradle plugins`_ repository, or check out the :doc:`Quick start guide </quickstart>` for a tutorial on
+creating a Smithy model and building it with the Smithy Gradle plugin.
+
+The examples in from the `examples directory`_  can be copied into your workspace using
+the :ref:`Smithy CLI <smithy-cli>` ``init`` command as follows:
+
+.. code-block:: text
+
+    smithy init -t <EXAMPLE_NAME> -o <OUTPUT_DIRECTORY> --url https://github.com/smithy-lang/smithy-gradle-plugin
+
+You can list all examples available in the `Smithy Gradle plugins`_ repository with the following command:
+
+.. code-block:: text
+
+    smithy init --list --url https://github.com/smithy-lang/smithy-gradle-plugin
+
+.. _building-smithy-models:
+
+Building Smithy models
+======================
+
+The ``smithyBuild`` task that builds smithy models operates in two different modes:
+
+1. If no ``projection`` is specified for the ``SmithyExtension``, then the task
+   runs a "source" build using the "source" projection.
+2. If a ``projection`` is specified for the ``SmithyExtension``, then the task
+   runs in "projection" mode.
+
+.. _building-source-model:
+
+Building a source model
+-----------------------
+
+A "source" build is run when no ``sourceProjection`` is configured in
+``SmithyExtension``. To prevent accidentally relying on Smithy models
+that are only available as build dependencies, Smithy models are discovered
+using only the :ref:`model sources <model-sources>` and ``runtimeClasspath``.
+
+The following example ``build.gradle.kts`` will build a Smithy model using a
+"source" build:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        plugins {
+            `java-library`
+            id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+        }
+
+        // The SmithyExtension is used to customize the build. This example
+        // doesn't set any values and can be completely omitted.
+        smithy {}
+
+        repositories {
+            mavenLocal()
+            mavenCentral()
+        }
+
+        dependencies {
+            implementation("software.amazon.smithy:smithy-model:__smithy_version__")
+
+            // These are just examples of dependencies. This model has a dependency on
+            // a "common" model package and uses the external AWS traits.
+            implementation("com.foo.baz:foo-model-internal-common:1.0.0")
+            implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        plugins {
+            id 'java-library'
+            'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+        }
+
+        // The SmithyExtension is used to customize the build. This example
+        // doesn't set any values and can be completely omitted.
+        smithy {}
+
+        repositories {
+            mavenLocal()
+            mavenCentral()
+        }
+
+        dependencies {
+            implementation 'software.amazon.smithy:smithy-model:__smithy_version__'
+
+            // These are just examples of dependencies. This model has a dependency on
+            // a "common" model package and uses the external AWS traits.
+            implementation 'com.foo.baz:foo-model-internal-common:1.0.0'
+            implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+        }
+
+.. _generating-projection:
+
+Generating a projection
+-----------------------
+
+A "projection" build is run when a :ref:`projection <projections>` is
+specified in the ``SmithyExtension``. You might create a projection of
+a model if you need to maintain an internal version of a model that contains
+more information and features than an external version of a model published to
+your customers.
+
+Any projected models should be added to the ``smithyBuild`` configuration. This
+prevents packages with projected models from appearing as dependencies of the
+projected version of the model.
+
+The following example gradle build script will run a "projection"
+build using the "external" projection.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            plugins {
+                `java-library`
+                id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+            dependencies {
+                implementation("software.amazon.smithy:smithy-aws-traits:__smithy_version__")
+
+                // Take a dependency on the internal model package. This
+                // dependency *must* be a smithyBuild dependency to ensure
+                // that is does not appear in the generated JAR.
+                smithyBuild("com.foo.baz:foo-internal-model:1.0.0")
+            }
+
+            smithy {
+                // Use the "external" projection. This projection must be found in the
+                // smithy-build.json file. This also ensures that models found in the
+                // foo-internal-package that weren't filtered out are copied into the
+                // projection created by this package.
+                sourceProjection.set("external")
+                projectionSourceTags.addAll("com.foo.baz:foo-internal-model")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            plugins {
+                id 'java-library'
+                id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+            dependencies {
+                implementation 'software.amazon.smithy:smithy-aws-traits:__smithy_version__'
+
+                // Take a dependency on the internal model package. This
+                // dependency *must* be a smithyBuild dependency to ensure
+                // that is does not appear in the generated JAR.
+                smithyBuild 'com.foo.baz:foo-internal-model:1.0.0'
+            }
+
+            smithy {
+                // Use the "external" projection. This projection must be found in the
+                // smithy-build.json file. This also ensures that models found in the
+                // foo-internal-package that weren't filtered out are copied into the
+                // projection created by this package.
+                sourceProjection = "external"
+                projectionSourceTags += ["com.foo.baz:foo-internal-model"]
+            }
+
+Because the ``sourceProjection`` of the ``SmithyExtension`` was set to ``external``, a
+``smithy-build.json`` file **must** be found that defines the ``external``
+projection. For example:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "version": "1.0",
+        "projections": {
+            "external": {
+                "transforms": [
+                    {
+                        "name": "excludeShapesByTag",
+                        "args": {
+                            "tags": ["internal"]
+                        }
+                    },
+                    {
+                        "name": "excludeShapesByTrait",
+                        "args": {
+                            "traits": ["internal"]
+                        }
+                    },
+                    {
+                        "name": "excludeMetadata",
+                        "args": {
+                            "keys": ["suppressions", "validators"]
+                        }
+                    },
+                    {
+                        "name": "removeUnusedShapes"
+                    }
+                ]
+            }
+        }
+    }
+
+.. _projection-tags:
+
+Projection tags
+^^^^^^^^^^^^^^^
+
+Projections can *project* and filter shapes from dependencies into the output model.
+"Projecting" a dependency into your output JAR will include that model in the output
+sources of your project. Downstream consumers of your JAR will then no longer need to
+include the "projected" dependency as a dependency of their project to resolve the
+"projected" shapes. You need to specify which dependencies are being projected into
+your JAR by setting the ``projectionSourceTags`` property.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            smithy {
+                sourceProjection.set("external")
+                projectionSourceTags.addAll("Foo", "com.baz:bar")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            smithy {
+                projection = "external"
+                projectionSourceTags += setOf("Foo", "com.baz:bar")
+            }
+
+
+Tags are used to logically group packages to make it easier to build
+projections. The ``tags`` property is used to add ``Smithy-Tags`` attribute
+to the JAR MANIFEST.
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            smithy {
+                tags.addAll("X", "foobaz-model")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            smithy {
+                tags += ["X", "foobaz-model"]
+            }
+
+
+For example, if your service is made up of 10 packages, you can add the
+"foobaz-model" Smithy tag to each package so that the only value that needs
+to be provided for ``tags`` to correctly project your model is "foobaz-model".
+
+When building a model package, the Smithy Gradle plugin will automatically add
+the group name of the package being built, the group name + ":" + name of the package,
+and group name + ":" + name + ":" version. This allows models to always
+be queried by group and artifact names in addition to custom tags.
+
+.. _artifacts-from-smithy-models:
+
+Building artifacts from Smithy models
+-------------------------------------
+
+If a :ref:`smithy-build.json <smithy-build>` file is found at the root of the project,
+then it will be used to generate :ref:`artifacts <projection-artifacts>` from the Smithy
+model. Smithy uses :ref:`build plugins <plugins>` to generate projection artifacts. To
+use a smithy build plugin with Gradle, first add it as a ``smithyBuild`` dependency in
+the Gradle build script. The plugin will then be discoverable and can be configured
+in the ``smithy-build.json`` file.
+
+For example, to generate a Typescript client from a Smithy model, add the
+``smithy-typescript-codegen`` package as a ``smithyBuild`` dependency:
+
+.. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            dependencies {
+                // ...
+
+                // This dependency is required in order to apply the "typescript-client-codegen"
+                // plugin in smithy-build.json
+                smithyBuild("software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0")
+            }
+
+.. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            dependencies {
+                // ...
+
+                // This dependency is required in order to apply the "typescript-client-codegen"
+                // plugin in smithy-build.json
+                smithyBuild 'software.amazon.smithy.typescript:smithy-typescript-codegen:0.19.0'
+            }
+
+The plugin can then be configured in the ``smithy-build.json`` to generate a typescript client
+from a Smithy model:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "version": "1.0",
+        "plugins": {
+            "typescript-client-codegen": {
+                "package": "@smithy/typescript-client-codegen-example",
+                "packageVersion": "0.0.1",
+                "packageJson": {
+                    "license": "MIT"
+                }
+            }
+        }
+    }
+
+.. _gradle-plugin-configuration:
+
+Configuration
+=============
+
+.. _model-sources:
+
+Smithy model sources
+--------------------
+
+Smithy gradle plugins assume Smithy model files (``*.smithy``) are organized in a similar way as Java source files,
+in sourceSets. The smithy-base plugin adds a new sources block named smithy to every sourceSet.
+By default, this source block will include Smithy models in:
+
+* ``model/``
+* ``src/$sourceSetName/smithy``
+* ``src/$sourceSetName/resources/META-INF/smithy``
+
+New source directories can be added to a smithy sources block as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        sourceSets {
+            main {
+                smithy {
+                    srcDir("includes")
+                }
+            }
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        sourceSets {
+            main {
+                smithy {
+                    srcDir 'includes'
+                }
+            }
+        }
+
+Models found in these directories are combined into a flattened directory
+structure and used to validate and build the Smithy model. A Smithy manifest
+file is automatically created for the detected models, and it, along with the
+model files, are placed in the ``META-INF/smithy/`` resource of the created
+JAR. Any project that depends on this created JAR will be able to find
+and use the Smithy models contained in the JAR when using *model discovery*.
+
+Dependencies
+------------
+
+The Smithy build plugins use two different configurations to search for dependencies
+such as shared models or :ref:`smithy build plugins <plugins>`:
+
+* ``runtimeClasspath``:
+    Runtime dependencies that will be required by any output JARS or publications.
+    For example, a shared model package or Java trait definition.
+* ``smithyBuild``:
+    Build dependencies that are not required by output models.
+
+Runtime dependencies can be added directly to the ``runtimeClasspath`` configuration
+or to a configuration that extends ``runtimeClasspath``, such as the ``implementation``
+configuration added by the ``java-library`` plugin.
+
+.. _dependencies:
+
+Build Dependencies
+^^^^^^^^^^^^^^^^^^
+
+The ``smithy-base`` plugin adds a ``smithyBuild`` `Configuration`_ that can be used to specify
+dependencies used when calling ``smithy build``. These dependencies will not be included in any output JARs or publications. They are akin to `compileOnly`
+dependencies for Java projects. Smithy build plugins and projected dependencies should be
+included in the `smithyBuild` configuration. For example:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        dependencies {
+            smithyBuild("com.example.software:build-only:1.0.0")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        dependencies {
+            smithyBuild 'com.example.software:build-only:1.0.0'
+        }
+
+.. _smithy-extension-properties:
+
+Smithy extension properties
+---------------------------
+
+The Smithy Gradle plugins are configured using the `SmithyExtension`_ extension:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            projection.set("foo")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            projection = "foo"
+        }
+
+
+This extension supports the following properties:
+
+.. list-table::
+    :header-rows: 1
+    :widths: 10 20 70
+
+    * - Property
+      - Type
+      - Description
+    * - format
+      - ``boolean``
+      - Flag indicating whether to format Smithy source files. By default, the :ref:`Smithy CLI <smithy-cli>` ``format``
+        command is executed on all source directories. This opinionated formatter follows the best practices
+        recommended by the Smithy team. It is possible to disable the formatter by setting this flag to ``false``.
+    * - smithyBuildConfigs
+      - ``FileCollection``
+      - Sets a custom collection of smithy-build.json files to use when building the model.
+    * - sourceProjection
+      - ``String``
+      - Sets the :ref:`projection <projections>` name to use as the source (primary) projection.
+        The smithy sources for this projection will be packaged in output JARs by the ``smithy-jar``
+        plugin. There must be a corresponding projection definition in the :ref:`smithy-build.json <smithy-build-json>`
+        file in the project. Defaults to ``"source"``.
+    * - projectionSourceTags
+      - ``Set<String>``
+      - Get the tags that are searched for in classpaths when determining
+        which models are projected into the created JAR. This plugin will look
+        through the JARs in the buildscript classpath to see if they contain a
+        META-INF/MANIFEST.MF attribute named "Smithy-Tags" that matches any of
+        the given projection source tags. The Smithy models found in each
+        matching JAR are copied into the JAR being projected. This allows a
+        projection JAR to aggregate models into a single JAR.
+    * - tags
+      - ``Set<String>``
+      - Set the tags that are added to the JAR. These tags are placed in the
+        META-INF/MANIFEST.MF attribute named "Smithy-Tags" as a comma
+        separated list. JARs with Smithy-Tags can be queried when building
+        projections so that the Smithy models found in each matching JAR are
+        placed into the projection JAR.
+    * - allowUnknownTraits
+      - ``boolean``
+      - Sets whether or not unknown traits in the model should be ignored. By
+        default, the build will fail if unknown traits are encountered.
+    * - fork
+      - ``boolean``
+      - By default, the CLI is run in the same process as Gradle,
+        but inside a thread with a custom class loader. This should
+        work in most cases, but there is an option to run inside a
+        process if necessary.
+    * - outputDirectory
+      - ``Directory``
+      - Defines where Smithy build artifacts are written.
+
+
+Customize output directory
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, Smithy build artifacts will be placed in the project build
+directory in a ``smithyprojections/`` directory. There are two ways to override
+the output directory. The first method is to set the ``outputDirectory`` property
+in the ``smithy-build.json`` config for your Smithy project. For example:
+
+.. code-block:: json
+    :caption: smithy-build.json
+
+    {
+        "outputDirectory": "build/output"
+    }
+
+The second method is to configure the plugin using the ``smithy`` extension:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            outputDirectory.set(file("path/to/output"))
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            outputDirectory = file("path/to/output")
+        }
+
+.. note::
+
+    Setting the output directory on the plugin extension will
+    override any ``outputDirectory`` property set in the
+    smithy-build config.
+
+Set ``smithy-build`` configs to use
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, the plugin will look for a file called ``smithy-build.json``
+at the project's root and will use that as the :ref:`smithy-build config <smithy-build>`
+for your project. If no smithy-build.json file is found, then an empty build config
+is used to build the project.
+
+Alternatively, you can explicitly configure one or more smithy-build configs to use for
+your project as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            smithyBuildConfigs.set(files("smithy-build-config.json"))
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            smithyBuildConfigs = files("smithy-build-config.json")
+        }
+
+Set Smithy Tags to add to a JAR
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When the smithy-jar plugin is applied to a project, it can add a number of Smithy
+tags to the MANIFEST of a generated JAR. These tags can be used by downstream
+consumers to filter which models to include in projections. Tags can be configured
+for the plugin as follows:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            tags.addAll("tag1", "anotherTag", "anotherTag2")
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            tags += ["tag1", "anotherTag", "anotherTag2"]
+        }
+
+Fork a new process when executing Smithy CLI commands
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, :ref:`Smithy CLI <smithy-cli>` commands are run in the
+same process as Gradle, but inside a thread with a custom class loader.
+This should work in most cases, but there is an option to run inside a
+process if necessary. To run Smithy CLI commands in a process set the
+``fork`` configuration option to true:
+
+.. tab:: Kotlin
+
+    .. code-block:: kotlin
+        :caption: build.gradle.kts
+
+        smithy {
+            fork.set(true)
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            fork = true
+        }
+
+.. _disable-smithy-formatter:
+
+Disable Smithy Formatter
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, the :ref:`Smithy CLI <smithy-cli>` ``format`` command is
+executed on all source directories. This opinionated formatter follows
+the best practices recommended by the Smithy team. It is possible to disable
+the formatter by setting the format setting on the plugin extension to false:
+
+.. tab:: Kotlin
+
+    .. code-block::
+        :caption: build.gradle.kts
+
+        smithy {
+            format.set(false)
+        }
+
+.. tab:: Groovy
+
+    .. code-block:: groovy
+        :caption: build.gradle
+
+        smithy {
+            format = false
+        }
+
+.. _examples directory: https://github.com/awslabs/smithy-gradle-plugin/tree/main/examples
+.. _Smithy Gradle plugins: https://github.com/awslabs/smithy-gradle-plugin/
+.. _Gradle: https://gradle.org/
+.. _smithy-base: https://github.com/smithy-lang/smithy-gradle-plugin#smithy-base-plugin
+.. _smithy-jar: https://github.com/smithy-lang/smithy-gradle-plugin#smithy-jar-plugin
+.. _Configuration: https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html
+.. _SourceSet: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.SourceSet.html
+.. _Java plugin: https://docs.gradle.org/current/userguide/java_plugin.html
+.. _Kotlin JVM plugin: https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm
+.. _SmithyExtension: https://github.com/smithy-lang/smithy-gradle-plugin/blob/main/smithy-base/src/main/java/software/amazon/smithy/gradle/SmithyExtension.java
diff --git a/docs/source-2.0/guides/index.rst b/docs/source-2.0/guides/index.rst
index dee6887df..a45214311 100644
--- a/docs/source-2.0/guides/index.rst
+++ b/docs/source-2.0/guides/index.rst
@@ -7,7 +7,7 @@ Guides
 
     smithy-cli/index
     smithy-build-json
-    gradle-plugin
+    gradle-plugin/index
     using-code-generation/index
     model-linters
     model-validation-examples
diff --git a/docs/source-2.0/guides/model-linters.rst b/docs/source-2.0/guides/model-linters.rst
index 50d600e82..dbefc55d5 100644
--- a/docs/source-2.0/guides/model-linters.rst
+++ b/docs/source-2.0/guides/model-linters.rst
@@ -33,7 +33,67 @@ use in Smithy models.
 Linters in ``smithy-linters``
 -----------------------------
 
-The ``smithy-linters`` package defines the following linters.
+For Java developers using Smithy's reference implementation, the following
+linters (except for UnreferencedShape) are defined in the  ``software.amazon.smithy:smithy-linters``
+Maven package.
+
+
+.. _UnreferencedShape:
+
+UnreferencedShape
+=================
+
+Emits when a shape is not connected to the rest of the model. If no
+configuration is provided, the linter will check if a shape is connected to
+the closure of any service shape. A selector can be provided to define a
+custom set of "root" shapes to customize how the linter determines if a shape
+is unreferenced. Shapes that are connected through the :ref:`idref-trait`
+are considered connected.
+
+Rationale
+    Just like unused variables in code, removing unused shapes from a model
+    makes the model easier to maintain.
+
+Default severity
+    ``NOTE``
+
+Configuration
+    .. list-table::
+       :header-rows: 1
+       :widths: 20 20 60
+
+       * - Property
+         - Type
+         - Description
+       * - rootShapeSelector
+         - ``string``
+         - A :ref:`selector <selectors>` that specifies the root shape(s)
+           from which to detect if other shapes are connected. Defaults
+           to "service", meaning any shape connected to any service shape
+           in the model is considered referenced.
+
+Example:
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    metadata validators = [
+        {
+            // Find shapes that aren't connected to service shapes or shapes
+            // marked with the trait, smithy.example#myCustomTrait.
+            name: "UnreferencedShape"
+            configuration: {
+                rootShapeSelector: ":is(service, [trait|smithy.example#myCustomTrait])"
+            }
+        }
+    ]
+
+.. note::
+
+    For backward compatibility reasons, the ``UnreferencedShape`` validator is available
+    in ``software.amazon.smithy:smithy-model`` Maven package and does not require
+    additional dependencies.
 
 
 .. _AbbreviationName:
diff --git a/docs/source-2.0/quickstart.rst b/docs/source-2.0/quickstart.rst
index c99297472..ed0acf26b 100644
--- a/docs/source-2.0/quickstart.rst
+++ b/docs/source-2.0/quickstart.rst
@@ -93,10 +93,31 @@ weather service.
 4. This service closure contains the following operations:
    ``ListCities``, ``GetCity``, ``GetForecast``, ``GetCurrentTime``.
 
-``Weather`` is a :ref:`service` shape that is defined inside of a
-:ref:`namespace <namespaces>`.
+First, create a directory called `smithy-quickstart` with a `model` directory
+and a weather model file such that your `smithy-quickstart` directory has the
+following file structure:
+
+.. code-block:: text
+
+    smithy-quickstart/
+    └── model/
+        └── weather.smithy
+
+.. tip::
+
+    Run the following command to create the quickstart directory
+
+    .. code-block:: text
+
+        mkdir -p smithy-quickstart/model \
+        && touch smithy-quickstart/model/weather.smithy \
+        && cd smithy-quickstart
+
+Next, we will start to model a ``Weather`` service in the `weather.smithy` file.
+``Weather`` is a :ref:`service` shape that is defined inside of a :ref:`namespace <namespaces>`.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     $version: "2"
     namespace example.weather
@@ -129,6 +150,7 @@ A resource is contained within a service or another resource. Resources have
 identifiers, operations, and any number of child resources.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     $version: "2"
     namespace example.weather
@@ -163,6 +185,7 @@ Each ``City`` has a single ``Forecast``. This can be defined by adding the
 ``Forecast`` to the ``resources`` property of the ``City``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     resource City {
         identifiers: { cityId: CityId }
@@ -189,6 +212,7 @@ members of resource operations map to resource properties or identifiers to
 perform updates on or examine the state of a resource.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     resource City {
         identifiers: { cityId: CityId }
@@ -244,6 +268,7 @@ your API.
 Let's define the operation used to "read" a ``City``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     @readonly
     operation GetCity {
@@ -291,6 +316,7 @@ Let's define the operation used to "read" a ``City``.
 And define the operation used to "read" a ``Forecast``.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     @readonly
     operation GetForecast {
@@ -332,6 +358,7 @@ bind the identifier of a ``City`` to its input structure; we are listing
 cities, so there's no way we could provide a ``City`` identifier.
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     /// Provides weather forecasts.
     @paginated(inputToken: "nextToken", outputToken: "nextToken",
@@ -413,6 +440,7 @@ service.
 
 
 .. code-block:: smithy
+    :caption: model/weather.smithy
 
     /// Provides weather forecasts.
     @paginated(inputToken: "nextToken", outputToken: "nextToken",
@@ -442,51 +470,90 @@ service.
 Building the Model
 ==================
 
-Now that you have a model, you'll want to build it and generate things from it.
-Building the model creates projections of the model, applies plugins to
-generate artifacts, runs validation, and creates a JAR that contains the
-filtered projection. The `Smithy Gradle Plugin`_ is the best way to get started
-building a Smithy model. First, create a ``smithy-build.json`` file:
+Now that you have a model, you'll want to build it and generate additional
+artifacts from it. Building the model creates projections of the model, applies plugins to
+generate artifacts, and runs validation.
 
-.. code-block:: json
+.. tab:: Smithy CLI
 
-    {
-        "version": "1.0"
-    }
+    .. admonition:: Install required tools
+        :class: tip
 
-Then create a new ``build.gradle.kts`` file:
+        Before you proceed, make sure you have the :ref:`Smithy CLI installed <cli_installation>`.
 
-.. code-block:: kotlin
+    To build a Smithy model using the :ref:`the Smithy CLI <smithy-cli>`,
+    create a :ref:`smithy-build.json <smithy-build-json>` file in the ``smithy-quickstart`` directory:
 
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
+    .. code-block:: json
+        :caption: smithy-build.json
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
+        {
+            // Version of the smithy-build.json file specification
+            "version": "1.0",
+            // Location to search for Smithy model source files
+            "sources": ["model"]
+        }
 
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-    }
+    Next, run ``smithy build``. That's it! We just created a simple, read-only, ``Weather`` service.
 
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        // Uncomment this to use a custom projection when building the JAR.
-        // projection = "foo"
-    }
+.. tab:: Gradle
+
+    .. admonition:: Install required tools
+        :class: tip
+
+        Before you proceed, make sure you have `gradle installed`_.
+
+
+    To build a Smithy model using the :ref:`Smithy Gradle Plugin <smithy-gradle-plugin>`,
+    first, create a gradle build script file in the ``smithy-quickstart`` directory:
+
+    .. tab:: Kotlin
+
+        .. code-block:: kotlin
+            :caption: build.gradle.kts
+
+            plugins {
+                `java-library`
+                id("software.amazon.smithy.gradle.smithy-jar").version("0.10.0")
+            }
 
-    // Uncomment to disable creating a JAR.
-    //tasks["jar"].enabled = false
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+    .. tab:: Groovy
+
+        .. code-block:: groovy
+            :caption: build.gradle
+
+            plugins {
+                id 'java-library'
+                id 'software.amazon.smithy.gradle.smithy-jar' version '0.10.0'
+            }
+
+            repositories {
+                mavenLocal()
+                mavenCentral()
+            }
+
+    Next, create a :ref:`smithy-build.json <smithy-build-json>` file in the
+    ``smithy-quickstart`` directory:
+
+    .. code-block:: json
+        :caption: smithy-build.json
+
+        {
+            // Version of the smithy-build.json file specification
+            "version": "1.0"
+        }
+
+    Finally, run ``gradle build``. That's it! We just created a simple, read-only, ``Weather`` service.
 
-Finally, copy the weather model to ``model/weather.smithy`` and
-then run ``gradle build`` (make sure you have `gradle installed`_).
 
 Next steps
 ==========
 
-That's it! We just created a simple, read-only, ``Weather`` service.
-
 1. Try adding a "create" lifecycle operation to ``City``.
 2. Try adding a "delete" lifecycle operation to ``City``.
 3. Try adding :ref:`HTTP binding traits <http-traits>` to the API.
@@ -511,49 +578,47 @@ There's plenty more to explore in Smithy.
 Complete example
 ================
 
-If you followed all the steps in this guide, you should have three files, laid
-out like so::
 
-    .
-    ├── build.gradle.kts
-    ├── model
-    │   └── weather.smithy
-    └── smithy-build.json
 
-The ``smithy-build.json`` should have the following contents:
+.. note::
 
-.. code-block:: json
+    You can clone a working version of this quickstart example using the
+    :ref:`Smithy CLI <smithy-cli>` ``init`` command.
 
-    {
-        "version": "1.0"
-    }
+    .. tab:: Quickstart with Smithy CLI
 
-The ``build.gradle.kts`` should have the following contents:
+        .. code-block::
 
-.. code-block:: kotlin
+            smithy init -o <output_directory>
 
-    plugins {
-        id("software.amazon.smithy").version("0.6.0")
-    }
+    .. tab:: Quickstart with Gradle
 
-    repositories {
-        mavenLocal()
-        mavenCentral()
-    }
+        .. code-block::
 
-    dependencies {
-        implementation("software.amazon.smithy:smithy-model:__smithy_version__")
-    }
+            smithy init -t quickstart-gradle -o <output_directory>
 
-    configure<software.amazon.smithy.gradle.SmithyExtension> {
-        // Uncomment this to use a custom projection when building the JAR.
-        // projection = "foo"
-    }
+If you followed all the steps in this guide, your working directory should be laid out like so:
+
+.. tab:: Smithy CLI
+
+    .. code-block:: text
+
+        .
+        ├── smithy-build.json
+        └── model
+            └── weather.smithy
+
+.. tab:: Gradle
+
+    .. code-block:: text
 
-    // Uncomment to disable creating a JAR.
-    //tasks["jar"].enabled = false
+        .
+        ├── build.gradle.kts
+        ├── smithy-build.json
+        └── model
+            └── weather.smithy
 
-Finally, the complete ``weather.smithy`` model should look like:
+The complete ``weather.smithy`` model should look like:
 
 .. code-block:: smithy
 
diff --git a/smithy-build/build.gradle b/smithy-build/build.gradle
index 7b47e4dcc..20a502b25 100644
--- a/smithy-build/build.gradle
+++ b/smithy-build/build.gradle
@@ -24,4 +24,7 @@ ext {
 dependencies {
     api project(":smithy-utils")
     api project(":smithy-model")
+
+    // Allows testing of annotation processor
+    testImplementation "com.google.testing.compile:compile-testing:0.21.0"
 }
diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/processor/SmithyAnnotationProcessor.java b/smithy-build/src/main/java/software/amazon/smithy/build/processor/SmithyAnnotationProcessor.java
new file mode 100644
index 000000000..6eeb05f2c
--- /dev/null
+++ b/smithy-build/src/main/java/software/amazon/smithy/build/processor/SmithyAnnotationProcessor.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.build.processor;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.lang.annotation.Annotation;
+import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.tools.Diagnostic;
+import javax.tools.StandardLocation;
+import software.amazon.smithy.build.SmithyBuild;
+import software.amazon.smithy.build.SmithyBuildResult;
+import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.loader.ModelAssembler;
+import software.amazon.smithy.model.loader.ModelDiscovery;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.utils.IoUtils;
+
+/**
+ * Base implementation of a Smithy annotation processor.
+ *
+ * <p>This implementation can be extended to create an Annotation processor for
+ * a Smithy build plugin that generates Java code. The processor will execute
+ * <strong>a single build plugin</strong> and write any generated Java code created
+ * by the plugin to the correct output path.
+ *
+ * <p>Smithy models on the classpath of extending annotation processors are automatically
+ * discovered and any java artifacts generated by this plugin are written to standard locations.
+ * Generated resource files are written to the class output while all other generated Java
+ * classes are written to the source output. Plugin artifacts that are not java files or
+ * resource files under {@code META-INF/} are ignored.
+ *
+ * <p>Extending classes must specify the name of the build plugin they will execute using
+ * {@link #getPluginName()}. This plugin must be discoverable on the annotation processor classpath.
+ * The plugin is then executed using the data from the annotation as the plugin configuration.
+ *
+ * <p>For example, if your plugin has the following configuration node in a `smithy-build.json` file:
+ * <pre>
+ *     "myPlugin" : {
+ *         "packageName" = "myPackage.namespace",
+ *         "listOfTags" = ["a", "b", "c"]
+ *     }
+ * </pre>
+ * Then you would define an annotation like the following:
+ * <pre>
+ * {@code
+ *  @Target(ElementType.PACKAGE)
+ *  public @interface MyPlugin {
+ *     String[] listOfTags();
+ *  }
+ * }
+ * </pre>
+ * The {@link #createPluginNode} method is used to map this annotation to the object node
+ * expected by the plugin. Once a mapping has been defined for the annotation, it can be
+ * added to the {@code package-info.java} as follows:
+ * <pre>
+ * {@code
+ * @MyPlugin(listOfTags = {"a", "b", "c"})
+ * package com.example.traitcodegen;
+ *
+ * import com.example.annotations.MyPlugin;
+ * }
+ * </pre>
+ * The base processor class will discover the namespace of the package the annotation
+ * is applied to and pass that as a parameter to the {@link #createPluginNode} method.
+ *
+ * @param <A> Annotation to execute the processor for. This annotation should
+ *            target the {@code PACKAGE} {@link java.lang.annotation.ElementType} and
+ *            should be applied in the {@code package-info.java} file of a package.
+ */
+public abstract class SmithyAnnotationProcessor<A extends Annotation> extends AbstractProcessor {
+    private static final String MANIFEST_PATH = "META-INF/smithy/manifest";
+    private Messager messager;
+    private Filer filer;
+
+    @Override
+    public synchronized void init(ProcessingEnvironment processingEnv) {
+        super.init(processingEnv);
+        messager = processingEnv.getMessager();
+        filer = processingEnv.getFiler();
+    }
+
+    @Override
+    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(getAnnotationClass());
+        if (elements.size() > 1) {
+            messager.printMessage(Diagnostic.Kind.ERROR,
+                    "Only one package can have the " + getAnnotationClass() + " annotation.");
+        } else if (elements.size() == 1) {
+            messager.printMessage(Diagnostic.Kind.NOTE,
+                    "Executing processor: " + this.getClass().getSimpleName() + "...");
+            String packageName = getPackageName(elements.iterator().next());
+            SmithyBuildConfig config = createBuildConfig(getAnnotation(elements), packageName);
+            executeSmithyBuild(config).allArtifacts()
+                    .filter(path -> path.toString().contains(getPluginName()) && path.toString().contains("source"))
+                    .forEach(this::writeArtifact);
+        }
+
+        // Always return false to ensure the annotation processor does not claim the annotation.
+        return false;
+    }
+
+    /**
+     * Name of the Smithy build plugin to execute with this annotation processor.
+     *
+     * @return name of plugin to run.
+     */
+    protected abstract String getPluginName();
+
+    /**
+     * Annotation class for the processor.
+     * <p>
+     * Each implementation of {@code SmithyProcessor} should have a specific package-scoped annotation
+     * used for configuration. {@link #createPluginNode(Annotation)} maps this annotation to the
+     * configuration node for the plugin specified by {@link #getPluginName()}.
+     *
+     * @return class of the annotation used by this processor
+     */
+    protected abstract Class<A> getAnnotationClass();
+
+    /**
+     * Maps annotation data to a plugin configuration node.
+     *
+     * @param annotation instance of generator annotation to use to create the build config.
+     * @return ObjectNode to use as plugin configuration node.
+     */
+    protected abstract ObjectNode createPluginNode(A annotation, String packageName);
+
+    private SmithyBuildConfig createBuildConfig(A annotation, String packageName) {
+        Map<String, ObjectNode> pluginMap = new HashMap<>();
+        pluginMap.put(getPluginName(), createPluginNode(annotation, packageName));
+        return SmithyBuildConfig.builder().version("1.0").plugins(pluginMap).build();
+    }
+
+    private SmithyBuildResult executeSmithyBuild(SmithyBuildConfig config) {
+        ModelAssembler assembler = Model.assembler();
+
+        // Discover any models on the annotation processor classpath
+        assembler.discoverModels(getClass().getClassLoader());
+
+        // Load any models found in the resources standard location
+        ModelDiscovery.findModels(getManifestUrl()).forEach(assembler::addImport);
+
+        SmithyBuild smithyBuild = SmithyBuild.create(getClass().getClassLoader());
+        smithyBuild.model(assembler.assemble().unwrap());
+        smithyBuild.config(config);
+
+        return smithyBuild.build();
+    }
+
+    private void writeArtifact(Path path) {
+        String pathStr = path.toString();
+        final String outputPath = pathStr.substring(pathStr.lastIndexOf(getPluginName())
+                + getPluginName().length() + 1);
+        try {
+            // Resources are written to the class output
+            if (outputPath.startsWith("META-INF")) {
+                try (Writer writer = filer
+                        .createResource(StandardLocation.CLASS_OUTPUT, "", convertOutputPath(outputPath))
+                        .openWriter()
+                ) {
+                    writer.write(IoUtils.readUtf8File(path));
+                }
+                // All other Java files are written to the source output
+            } else if (outputPath.endsWith(".java")) {
+                // The filer needs to use a namespace convention of `.` rather than the default separator for paths.
+                String javaPath = outputPath.replace(FileSystems.getDefault().getSeparator(), ".")
+                        .substring(0, outputPath.lastIndexOf(".java"));
+                try (Writer writer = filer.createSourceFile(javaPath).openWriter()) {
+                    writer.write(IoUtils.readUtf8File(path));
+                }
+            } else {
+                // Non-java files generated are ignored.
+                messager.printMessage(Diagnostic.Kind.NOTE, "Ignoring generated file: " + outputPath);
+            }
+        } catch (IOException exc) {
+            throw new UncheckedIOException(exc);
+        }
+    }
+
+    private A getAnnotation(Set<? extends Element> elements) {
+        return elements.stream()
+                .findFirst()
+                .map(element -> element.getAnnotation(getAnnotationClass()))
+                .orElseThrow(() -> new IllegalStateException("No annotation of type "
+                        + getAnnotationClass() + " found on element."));
+    }
+
+    private URL getManifestUrl() {
+        try {
+            return filer.getResource(StandardLocation.CLASS_PATH, "", MANIFEST_PATH).toUri().toURL();
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    // This is necessary to convert windows paths to a valid URI
+    private String convertOutputPath(String outputPath) {
+        return outputPath.replace(FileSystems.getDefault().getSeparator(), "/");
+    }
+
+    private String getPackageName(Element element) {
+        if (element instanceof PackageElement) {
+            PackageElement packageElement = (PackageElement) element;
+            return packageElement.getQualifiedName().toString();
+        } else {
+            throw new IllegalStateException("Expected Annotation to be applied to package element "
+                    + "but found : " + element);
+        }
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java
index c89af9717..25e6fab95 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/AstCommand.java
@@ -86,7 +86,7 @@ final class AstCommand implements Command {
                 .models(arguments.getPositional())
                 .validationPrinter(env.stderr())
                 .validationMode(Validator.Mode.QUIET)
-                .severity(Severity.DANGER)
+                .defaultSeverity(Severity.DANGER)
                 .build();
 
         ModelSerializer serializer = ModelSerializer.builder().build();
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
index 25508cdfd..0ed1e38b0 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
@@ -72,7 +72,7 @@ final class BuildCommand implements Command {
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
         arguments.addReceiver(new DiscoveryOptions());
-        arguments.addReceiver(new SeverityOption());
+        arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
         arguments.addReceiver(new Options());
 
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
index c0a7c10f3..9ff05b87f 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/DiffCommand.java
@@ -65,8 +65,9 @@ final class DiffCommand implements Command {
     @Override
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
-        arguments.addReceiver(new SeverityOption());
+        arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
+        arguments.addReceiver(new ValidationEventFormatOptions());
         arguments.addReceiver(new Options());
         arguments.getReceiver(BuildOptions.class).noPositionalArguments(true);
 
@@ -314,10 +315,10 @@ final class DiffCommand implements Command {
                     .config(config)
                     .arguments(arguments)
                     .env(env)
-                    .validationPrinter(env.stderr())
+                    .validationPrinter(env.stdout())
                     // Only report issues that fail the build.
                     .validationMode(Validator.Mode.QUIET_CORE_ONLY)
-                    .severity(Severity.DANGER);
+                    .defaultSeverity(Severity.DANGER);
         }
 
         // Creating a new model is the same for each diff mode.
@@ -326,6 +327,7 @@ final class DiffCommand implements Command {
                     .models(models)
                     .titleLabel("NEW", ColorTheme.DIFF_EVENT_TITLE)
                     .config(config)
+                    .disableOutputFormatFraming(true) // don't repeat things like CSV headers.
                     .build();
         }
 
@@ -336,7 +338,8 @@ final class DiffCommand implements Command {
             builder
                     .titleLabel("DIFF", ColorTheme.DIFF_TITLE)
                     .validatedResult(new ValidatedResult<>(newModel, events))
-                    .severity(null) // reset so it takes on standard option settings.
+                    .defaultSeverity(null) // reset so it takes on standard option settings.
+                    .disableOutputFormatFraming(true) // don't repeat things like CSV headers.
                     .build();
         }
 
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java
index e081e6c0d..92b70dedc 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/MigrateCommand.java
@@ -152,7 +152,7 @@ final class MigrateCommand implements Command {
                 .env(env)
                 .models(models)
                 .validationPrinter(env.stderr())
-                .severity(Severity.DANGER)
+                .defaultSeverity(Severity.DANGER)
                 .build();
 
         SmithyBuild smithyBuild = SmithyBuild.create(classLoader)
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
index b9020bee8..fc9ef35e7 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ModelBuilder.java
@@ -57,14 +57,35 @@ final class ModelBuilder {
     private List<String> models;
     private Command.Env env;
     private SmithyBuildConfig config;
-    private Severity severity;
+    private Severity defaultSeverity;
     private ValidatedResult<Model> validatedResult;
     private String titleLabel;
     private Style[] titleLabelStyles;
+    private ValidationEventFormatOptions.Format validationOutputFormat;
+    private boolean disableOutputFormatFraming = false;
     private boolean disableConfigModels;
 
     public ModelBuilder arguments(Arguments arguments) {
         this.arguments = arguments;
+
+        // Determine how to format the output, whether it's text (the default) or CSV.
+        // Only some commands (like validate) actually let you customize the output format, so assume a default.
+        if (validationOutputFormat == null) {
+            validationOutputFormat(arguments.hasReceiver(ValidationEventFormatOptions.class)
+                    ? arguments.getReceiver(ValidationEventFormatOptions.class).format()
+                    : ValidationEventFormatOptions.Format.TEXT);
+        }
+
+        return this;
+    }
+
+    public ModelBuilder disableOutputFormatFraming(boolean disableOutputFormatFraming) {
+        this.disableOutputFormatFraming = disableOutputFormatFraming;
+        return this;
+    }
+
+    public ModelBuilder validationOutputFormat(ValidationEventFormatOptions.Format validationOutputFormat) {
+        this.validationOutputFormat = validationOutputFormat;
         return this;
     }
 
@@ -94,8 +115,8 @@ final class ModelBuilder {
         return this;
     }
 
-    public ModelBuilder severity(Severity severity) {
-        this.severity = severity;
+    public ModelBuilder defaultSeverity(Severity defaultSeverity) {
+        this.defaultSeverity = defaultSeverity;
         return this;
     }
 
@@ -123,7 +144,13 @@ final class ModelBuilder {
 
         StandardOptions standardOptions = arguments.getReceiver(StandardOptions.class);
         BuildOptions buildOptions = arguments.getReceiver(BuildOptions.class);
-        Severity minSeverity = resolveMinSeverity(standardOptions);
+
+        // Resolve validator options and severity.
+        ValidatorOptions validatorOptions = arguments.hasReceiver(ValidatorOptions.class)
+                ? arguments.getReceiver(ValidatorOptions.class)
+                : new ValidatorOptions();
+        resolveMinSeverity(standardOptions, validatorOptions);
+
         ClassLoader classLoader = env.classLoader();
         ColorFormatter colors = env.colors();
         CliPrinter stderr = env.stderr();
@@ -174,14 +201,22 @@ final class ModelBuilder {
                 .titleLabel(titleLabel, titleLabelStyles)
                 .build();
 
+        if (!disableOutputFormatFraming) {
+            validationOutputFormat.beginPrinting(validationPrinter);
+        }
+
         for (ValidationEvent event : sortedEvents) {
             // Only log events that are >= --severity. Note that setting --quiet inherently
-            // configures events to need to be >= DANGER.
-            if (event.getSeverity().ordinal() >= minSeverity.ordinal()) {
-                validationPrinter.println(formatter.format(event));
+            // configures events to need to be >= DANGER. Also filter using --show-validators and --hide-validators.
+            if (validatorOptions.isVisible(event)) {
+                validationOutputFormat.print(validationPrinter, formatter, event);
             }
         }
 
+        if (!disableOutputFormatFraming) {
+            validationOutputFormat.endPrinting(validationPrinter);
+        }
+
         env.flush();
         // Note: disabling validation will still show a summary of failures if the model can't be loaded.
         Validator.validate(validationMode != Validator.Mode.ENABLE, colors, stderr, validatedResult);
@@ -274,14 +309,11 @@ final class ModelBuilder {
     }
 
     // Determine a default severity if one wasn't given, by inspecting if there is a --severity option.
-    private Severity resolveMinSeverity(StandardOptions standardOptions) {
-        if (severity != null) {
-            return severity;
-        } else if (arguments.hasReceiver(SeverityOption.class)) {
-            return arguments.getReceiver(SeverityOption.class).severity(standardOptions);
-        } else {
-            return Severity.WARNING;
+    private Severity resolveMinSeverity(StandardOptions standardOptions, ValidatorOptions validatorOption) {
+        if (defaultSeverity != null && validatorOption.severity() == null) {
+            validatorOption.severity(defaultSeverity);
         }
+        return validatorOption.detectAndGetSeverity(standardOptions);
     }
 
     static ModelAssembler createModelAssembler(ClassLoader classLoader) {
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
index 3f2548cb9..b0de900fb 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
@@ -232,7 +232,7 @@ final class SelectCommand implements Command {
                 .models(arguments.getPositional())
                 .validationPrinter(env.stderr())
                 .validationMode(Validator.Mode.QUIET_CORE_ONLY)
-                .severity(Severity.DANGER)
+                .defaultSeverity(Severity.DANGER)
                 .build();
 
         Options options = arguments.getReceiver(Options.class);
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SeverityOption.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SeverityOption.java
deleted file mode 100644
index febca698a..000000000
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SeverityOption.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.smithy.cli.commands;
-
-import java.util.function.Consumer;
-import software.amazon.smithy.cli.ArgumentReceiver;
-import software.amazon.smithy.cli.CliError;
-import software.amazon.smithy.cli.HelpPrinter;
-import software.amazon.smithy.cli.StandardOptions;
-import software.amazon.smithy.model.validation.Severity;
-
-/**
- * Add the --severity option.
- */
-final class SeverityOption implements ArgumentReceiver {
-
-    static final String SEVERITY = "--severity";
-
-    private Severity severity;
-
-    @Override
-    public void registerHelp(HelpPrinter printer) {
-        printer.param(SEVERITY, null, "SEVERITY", "Set the minimum reported validation severity (one of NOTE, "
-                                                  + "WARNING [default setting], DANGER, ERROR).");
-    }
-
-    @Override
-    public Consumer<String> testParameter(String name) {
-        if (SEVERITY.equals(name)) {
-            return value -> {
-                severity(Severity.fromString(value).orElseThrow(() -> {
-                    return new CliError("Invalid severity level: " + value);
-                }));
-            };
-        }
-        return null;
-    }
-
-    /**
-     * Set the severity.
-     *
-     * @param severity Severity to set.
-     */
-    void severity(Severity severity) {
-        this.severity = severity;
-    }
-
-    /**
-     * Get the severity level, taking into account standard options that affect the default.
-     *
-     * @param options Standard options to query if no severity is explicitly set.
-     * @return Returns the resolved severity option.
-     */
-    Severity severity(StandardOptions options) {
-        if (severity != null) {
-            return severity;
-        } else if (options.quiet()) {
-            return Severity.DANGER;
-        } else {
-            return Severity.WARNING;
-        }
-    }
-}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
index 4cc3f2960..aca6d6a49 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidateCommand.java
@@ -46,8 +46,9 @@ final class ValidateCommand implements Command {
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
         arguments.addReceiver(new DiscoveryOptions());
-        arguments.addReceiver(new SeverityOption());
+        arguments.addReceiver(new ValidatorOptions());
         arguments.addReceiver(new BuildOptions());
+        arguments.addReceiver(new ValidationEventFormatOptions());
 
         CommandAction action = HelpActionWrapper.fromCommand(
             this,
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidationEventFormatOptions.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidationEventFormatOptions.java
new file mode 100644
index 000000000..9eb3c6d94
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidationEventFormatOptions.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.util.function.Consumer;
+import software.amazon.smithy.cli.ArgumentReceiver;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.cli.CliPrinter;
+import software.amazon.smithy.cli.HelpPrinter;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidationEventFormatter;
+
+final class ValidationEventFormatOptions implements ArgumentReceiver {
+
+    private static final String VALIDATION_FORMAT = "--format";
+
+    enum Format {
+        TEXT {
+            @Override
+            void print(CliPrinter printer, ValidationEventFormatter formatter, ValidationEvent event) {
+                printer.println(formatter.format(event));
+            }
+        },
+
+        CSV {
+            @Override
+            void beginPrinting(CliPrinter printer) {
+                printer.println("severity,id,shape,file,line,column,message,hint,suppressionReason");
+            }
+
+            @Override
+            void print(CliPrinter printer, ValidationEventFormatter formatter, ValidationEvent event) {
+                printer.println(
+                        String.format("\"%s\",\"%s\",\"%s\",\"%s\",%d,%d,\"%s\",\"%s\",\"%s\"",
+                                      event.getSeverity().toString(),
+                                      formatCsv(event.getId()),
+                                      event.getShapeId().map(ShapeId::toString).orElse(""),
+                                      formatCsv(event.getSourceLocation().getFilename()),
+                                      event.getSourceLocation().getLine(),
+                                      event.getSourceLocation().getColumn(),
+                                      formatCsv(event.getMessage()),
+                                      formatCsv(event.getHint().orElse("")),
+                                      formatCsv(event.getSuppressionReason().orElse(""))));
+            }
+        };
+
+        void beginPrinting(CliPrinter printer) {}
+
+        abstract void print(CliPrinter printer, ValidationEventFormatter formatter, ValidationEvent event);
+
+        void endPrinting(CliPrinter printer) {}
+
+        private static String formatCsv(String value) {
+            // Replace DQUOTE with DQUOTEDQUOTE, escape newlines, and escape carriage returns.
+            return value.replace("\"", "\"\"").replace("\n", "\\n").replace("\r", "\\r");
+        }
+    }
+
+    private Format format = Format.TEXT;
+
+    @Override
+    public void registerHelp(HelpPrinter printer) {
+        printer.param(VALIDATION_FORMAT, null, "text|csv",
+                      "Specifies the format to write validation events (text or csv). Defaults to text.");
+    }
+
+    @Override
+    public Consumer<String> testParameter(String name) {
+        if (name.equals(VALIDATION_FORMAT)) {
+            return s -> {
+                switch (s) {
+                    case "csv":
+                        format(Format.CSV);
+                        break;
+                    case "text":
+                        format(Format.TEXT);
+                        break;
+                    default:
+                        throw new CliError("Unexpected " + VALIDATION_FORMAT + ": `" + s + "`");
+                }
+            };
+        }
+        return null;
+    }
+
+    void format(Format format) {
+        this.format = format;
+    }
+
+    Format format() {
+        return format;
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidatorOptions.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidatorOptions.java
new file mode 100644
index 000000000..98120f9c7
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ValidatorOptions.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import software.amazon.smithy.cli.ArgumentReceiver;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.cli.HelpPrinter;
+import software.amazon.smithy.cli.StandardOptions;
+import software.amazon.smithy.model.validation.Severity;
+import software.amazon.smithy.model.validation.ValidationEvent;
+
+/**
+ * Add validation specific options.
+ */
+final class ValidatorOptions implements ArgumentReceiver {
+
+    static final String SEVERITY = "--severity";
+    static final String SHOW_VALIDATORS = "--show-validators";
+    static final String HIDE_VALIDATORS = "--hide-validators";
+
+    private Severity severity;
+    private List<String> showValidators = Collections.emptyList();
+    private List<String> hideValidators = Collections.emptyList();
+
+    @Override
+    public void registerHelp(HelpPrinter printer) {
+        printer.param(SEVERITY, null, "SEVERITY", "Set the minimum reported validation severity (one of NOTE, "
+                                                  + "WARNING [default setting], DANGER, ERROR).");
+        printer.param(SHOW_VALIDATORS, null, "VALIDATORS", "Comma-separated list of hierarchical validation event "
+                                                           + "IDs to show in the output of the command, "
+                                                           + "hiding the rest.");
+        printer.param(HIDE_VALIDATORS, null, "VALIDATORS", "Comma-separated list of hierarchical validation event "
+                                                           + "IDs to hide in the output of the command, "
+                                                           + "showing the rest.");
+    }
+
+    @Override
+    public Consumer<String> testParameter(String name) {
+        switch (name) {
+            case SEVERITY:
+                return value -> {
+                    severity(Severity.fromString(value).orElseThrow(() -> {
+                        return new CliError("Invalid severity level: " + value);
+                    }));
+                };
+            case SHOW_VALIDATORS:
+                return value -> {
+                    if (!hideValidators.isEmpty()) {
+                        throw new CliError(SHOW_VALIDATORS + " and " + HIDE_VALIDATORS + " are mutually exclusive");
+                    }
+                    showValidators(parseIds(value));
+                };
+            case HIDE_VALIDATORS:
+                return value -> {
+                    if (!showValidators.isEmpty()) {
+                        throw new CliError(SHOW_VALIDATORS + " and " + HIDE_VALIDATORS + " are mutually exclusive");
+                    }
+                    hideValidators(parseIds(value));
+                };
+            default:
+                return null;
+        }
+    }
+
+    private List<String> parseIds(String value) {
+        List<String> result = new ArrayList<>();
+        for (String id : value.split("\\s*,\\s*")) {
+            id = id.trim();
+            if (id.isEmpty()) {
+                throw new CliError("Invalid validation event ID");
+            }
+            result.add(id);
+        }
+        return result;
+    }
+
+    /**
+     * Get the set severity, which may be null.
+     *
+     * @return The nullable severity.
+     */
+    Severity severity() {
+        return severity;
+    }
+
+    /**
+     * Set the severity.
+     *
+     * @param severity Severity to set.
+     */
+    void severity(Severity severity) {
+        this.severity = severity;
+    }
+
+    /**
+     * Set and get the severity level, taking into account standard options that affect the default.
+     *
+     * @param standardOptions  Standard options to query if no severity is explicitly set.
+     * @return Returns the resolved severity option.
+     */
+    Severity detectAndGetSeverity(StandardOptions standardOptions) {
+        if (severity == null) {
+            if (standardOptions.quiet()) {
+                severity = Severity.DANGER;
+            } else {
+                severity = Severity.WARNING;
+            }
+        }
+        return severity;
+    }
+
+    /**
+     * Set the list of validators to show, but hide everything else.
+     *
+     * @param validators Validators to show.
+     */
+    void showValidators(List<String> validators) {
+        this.showValidators = validators;
+    }
+
+    /**
+     * Get the list of validators to show.
+     *
+     * @return Validator event IDs.
+     */
+    List<String> showValidators() {
+        return showValidators;
+    }
+
+    /**
+     * Get the list of validators to hide.
+     *
+     * @return Validator event IDs.
+     */
+    List<String> hideValidators(String csv) {
+        return hideValidators;
+    }
+
+    /**
+     * Set the list of validators to hide.
+     *
+     * @param validators Validators to hide.
+     */
+    void hideValidators(List<String> validators) {
+        this.hideValidators = validators;
+    }
+
+    /**
+     * Check if the given validation event matches the show/hide settings.
+     *
+     * <p>A severity must be set before calling this method.
+     *
+     * @param event Event to check.
+     * @return Return true if the event can be seen.
+     */
+    boolean isVisible(ValidationEvent event) {
+        if (event.getSeverity().ordinal() < severity.ordinal()) {
+            return false;
+        }
+
+        if (!showValidators.isEmpty()) {
+            for (String show : showValidators) {
+                if (event.containsId(show)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        if (!hideValidators.isEmpty()) {
+            for (String hide : hideValidators) {
+                if (event.containsId(hide)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
index d6128d045..bcd30b909 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlStringLexer.java
@@ -237,22 +237,16 @@ final class IdlStringLexer {
     /**
      * Computes the number of leading whitespace characters in a string.
      *
-     * <p>This method returns -1 if the string is empty or if the string
-     * contains only whitespace. When determining the whitespace of the
-     * last line, the length of the line is returned if the entire
-     * line consists of only spaces. This ensures that the whitespace
-     * of the last line can be used to influence the indentation of the
-     * content as a whole (a significant trailing line policy).
+     * <p>This method returns -1 if the string contains only whitespace. When determining the whitespace of the
+     * last line, the length of the line is returned if the entire line consists of only spaces. This ensures that
+     * the whitespace of the last line can be used to influence the indentation of the content as a whole
+     * (a significant trailing line policy).
      *
      * @param line Line to search for whitespace.
      * @param isLastLine Whether or not this is the last line.
      * @return Returns the last whitespace index starting at 0 or -1.
      */
     private static int computeLeadingWhitespace(CharSequence line, boolean isLastLine) {
-        if (line.length() == 0) {
-            return -1;
-        }
-
         for (int offset = 0; offset < line.length(); offset++) {
             if (line.charAt(offset) != ' ') {
                 return offset;
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
index e412f885a..701da61e5 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java
@@ -21,31 +21,49 @@ import java.util.function.Predicate;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
 import software.amazon.smithy.model.loader.Prelude;
+import software.amazon.smithy.model.selector.Selector;
 import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.traits.TraitDefinition;
 import software.amazon.smithy.utils.FunctionalUtils;
 
 /**
- * Finds shapes that are not connected to a service shape, are not trait
- * definitions, are not referenced by trait definitions, and are not
- * referenced in trait values through
+ * Finds shapes that are not connected to a "root" shape, are not trait definitions, are not referenced by trait
+ * definitions, and are not referenced in trait values through
  * {@link software.amazon.smithy.model.traits.IdRefTrait}.
  *
+ * <p>The "root" shapes defaults to all service shapes in the model. You can customize this by providing a selector
+ * that considers every matching shape a root shape. For example, a model might consider all shapes marked with
+ * a trait called "root" to be a root shape.
+ *
  * <p>Prelude shapes are never considered unreferenced.
  */
 public final class UnreferencedShapes {
 
+    private static final Selector SERVICE_SHAPES = Selector.parse("service");
+
+    private final Selector rootShapeSelector;
     private final Predicate<Shape> keepFilter;
 
+    /**
+     * @param keepFilter        Predicate that if matched keeps a shape from being unreferenced.
+     * @param rootShapeSelector Selector that returns the root shapes to traverse from (defaults to all services).
+     */
+    public UnreferencedShapes(Predicate<Shape> keepFilter, Selector rootShapeSelector) {
+        this.keepFilter = keepFilter;
+        this.rootShapeSelector = rootShapeSelector;
+    }
+
     public UnreferencedShapes() {
         this(FunctionalUtils.alwaysTrue());
     }
 
-    /**
-     * @param keepFilter Predicate that if matched keeps a shape from being unreferenced.
-     */
+    public UnreferencedShapes(Selector selector) {
+        this(FunctionalUtils.alwaysTrue(), selector);
+    }
+
     public UnreferencedShapes(Predicate<Shape> keepFilter) {
-        this.keepFilter = keepFilter;
+        this(keepFilter, SERVICE_SHAPES);
     }
 
     /**
@@ -60,21 +78,26 @@ public final class UnreferencedShapes {
         Walker shapeWalker = new Walker(providerWithIdRefRelationships);
 
         // Find all shapes connected to any service shape.
-        Set<Shape> connected = new HashSet<>();
-        for (Shape service : model.getServiceShapes()) {
-            connected.addAll(shapeWalker.walkShapes(service));
+        Set<ShapeId> connected = new HashSet<>();
+
+        // Stop traversing into trees that are already traversed.
+        Predicate<Relationship> traversed = rel -> !connected.contains(rel.getNeighborShapeId());
+
+        Set<Shape> rootShapes = rootShapeSelector.select(model);
+        for (Shape root : rootShapes) {
+            shapeWalker.iterateShapes(root, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
         }
 
         // Don't remove shapes that are traits or connected to traits.
         for (Shape trait : model.getShapesWithTrait(TraitDefinition.class)) {
-            connected.addAll(shapeWalker.walkShapes(trait));
+            shapeWalker.iterateShapes(trait, traversed).forEachRemaining(shape -> connected.add(shape.getId()));
         }
 
-        // Any shape that wasn't identified as connected to a service is considered unreferenced.
+        // Any shape that wasn't identified as connected to a root is considered unreferenced.
         Set<Shape> result = new HashSet<>();
         for (Shape shape : model.toSet()) {
             if (!shape.isMemberShape()
-                    && !connected.contains(shape)
+                    && !connected.contains(shape.getId())
                     && !Prelude.isPreludeShape(shape)
                     && keepFilter.test(shape)) {
                 result.add(shape);
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java
new file mode 100644
index 000000000..fe0c42376
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/linters/UnreferencedShapeValidator.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.model.validation.linters;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.neighbor.UnreferencedShapes;
+import software.amazon.smithy.model.node.ExpectationNotMetException;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.selector.Selector;
+import software.amazon.smithy.model.selector.SelectorSyntaxException;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.model.validation.ValidatorService;
+
+/**
+ * Finds shapes that aren't connected to any shapes considered "roots" (defaults to service shapes).
+ *
+ * <p>This linter is in smithy-model rather than smithy-linters so that it's easier for developers relying on the
+ * deprecated UnreferencedShape validator that was always on to migrate to this one instead without needing another
+ * dependency.
+ */
+public final class UnreferencedShapeValidator extends AbstractValidator {
+
+    public static final class Config {
+
+        private Selector rootShapeSelector = Selector.parse("service");
+
+        /**
+         * The Selector used to find root shapes, and any shape that is not connected to any of the returned root
+         * shapes is considered unreferenced.
+         *
+         * <p>Defaults to "service" if not set.
+         *
+         * @return Returns the root selector;
+         */
+        public Selector getRootShapeSelector() {
+            return rootShapeSelector;
+        }
+
+        public void setRootShapeSelector(Selector rootShapeSelector) {
+            this.rootShapeSelector = Objects.requireNonNull(rootShapeSelector);
+        }
+    }
+
+    public static final class Provider extends ValidatorService.Provider {
+        public Provider() {
+            super(UnreferencedShapeValidator.class, configuration -> {
+                Config config = new Config();
+                ObjectNode node = configuration.expectObjectNode()
+                        .expectNoAdditionalProperties(Collections.singleton("rootShapeSelector"));
+                node.getStringMember("rootShapeSelector").ifPresent(rootShapeNode -> {
+                    try {
+                        config.setRootShapeSelector(Selector.parse(rootShapeNode.getValue()));
+                    } catch (SelectorSyntaxException e) {
+                        throw new ExpectationNotMetException("Error parsing `rootShapeSelector`: " + e.getMessage(),
+                                                             rootShapeNode);
+                    }
+                });
+                return new UnreferencedShapeValidator(config);
+            });
+        }
+    }
+
+    private final Config config;
+
+    private UnreferencedShapeValidator(Config config) {
+        this.config = config;
+    }
+
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        List<ValidationEvent> events = new ArrayList<>();
+
+        for (Shape shape : new UnreferencedShapes(config.rootShapeSelector).compute(model)) {
+            events.add(note(shape, "This shape is unreferenced. It has no modeled connections to shapes "
+                                   + "that match the following selector: `" + config.rootShapeSelector + "`"));
+        }
+
+        return events;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java
index 19bd195af..a400abfa3 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnreferencedShapeValidator.java
@@ -27,7 +27,11 @@ import software.amazon.smithy.model.validation.ValidationEvent;
 /**
  * Adds a validation note event for each shape in the model that is not
  * connected to a service shape.
+ *
+ * <p>This validator is deprecated and no longer applied by default.
+ * Use the UnreferencedShapeValidator from smithy-linters instead.
  */
+@Deprecated
 public final class UnreferencedShapeValidator extends AbstractValidator {
     @Override
     public List<ValidationEvent> validate(Model model) {
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index 1a8691e08..42df78dca 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -52,6 +52,5 @@ software.amazon.smithy.model.validation.validators.TraitTargetValidator
 software.amazon.smithy.model.validation.validators.TraitValueValidator
 software.amazon.smithy.model.validation.validators.UnionValidator
 software.amazon.smithy.model.validation.validators.UnitTypeValidator
-software.amazon.smithy.model.validation.validators.UnreferencedShapeValidator
 software.amazon.smithy.model.validation.validators.UnstableTraitValidator
 software.amazon.smithy.model.validation.validators.XmlNamespaceTraitValidator
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService
index c9fc2570d..05885ebaf 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.ValidatorService
@@ -1,2 +1,3 @@
 software.amazon.smithy.model.validation.linters.EmitEachSelectorValidator$Provider
 software.amazon.smithy.model.validation.linters.EmitNoneSelectorValidator$Provider
+software.amazon.smithy.model.validation.linters.UnreferencedShapeValidator$Provider
diff --git a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
index 2df51058e..a9b1a47f6 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/FormatVisitor.java
@@ -17,14 +17,13 @@ package software.amazon.smithy.syntax;
 
 import com.opencastsoftware.prettier4j.Doc;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.utils.StringUtils;
 
 final class FormatVisitor {
 
@@ -447,11 +446,49 @@ final class FormatVisitor {
             case TEXT_BLOCK: {
                 // Dispersing the lines of the text block preserves any indentation applied from formatting parent
                 // nodes.
-                List<Doc> lines = Arrays.stream(tree.concatTokens().split(System.lineSeparator()))
-                        .map(String::trim)
-                        .map(Doc::text)
-                        .collect(Collectors.toList());
-                return Doc.intersperse(Doc.line(), lines);
+
+                // We need to rebuild the text block to remove any incidental leading whitespace. The easiest way to
+                // do that is to use the already parsed and resolved value from the lexer.
+                String stringValue = cursor.getTree()
+                        .tokens()
+                        .findFirst()
+                        .orElseThrow(() -> new RuntimeException("TEXT_BLOCK cursor does not have an IDL token"))
+                        .getStringContents();
+
+                // If the last character is a newline, then the closing triple quote must be on the next line.
+                boolean endQuoteOnNextLine = stringValue.endsWith("\n") || stringValue.endsWith("\r");
+
+                List<Doc> resultLines = new ArrayList<>();
+                resultLines.add(Doc.text("\"\"\""));
+
+                String[] inputLines = stringValue.split("\\r?\\n", -1);
+                for (int i = 0; i < inputLines.length; i++) {
+                    boolean lastLine = i == inputLines.length - 1;
+
+                    // If this is the last line and the ending quote is on the next line, then skip the extra line.
+                    if (endQuoteOnNextLine && lastLine) {
+                        break;
+                    }
+
+                    String lineValue = inputLines[i];
+
+                    // Trim trailing whitespace.
+                    // TODO: This may need to be configurable.
+                    lineValue = StringUtils.stripEnd(lineValue, null);
+
+                    // Add the closing quote to this line if it needs to be on the last line.
+                    if (lastLine) {
+                        lineValue += "\"\"\"";
+                    }
+
+                    resultLines.add(Doc.text(lineValue));
+                }
+
+                if (endQuoteOnNextLine) {
+                    resultLines.add(Doc.text("\"\"\""));
+                }
+
+                return Doc.intersperse(Doc.line(), resultLines);
             }
 
             case TOKEN:
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
index d5f5ce0d3..df1a00d68 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java
@@ -17,9 +17,12 @@ package software.amazon.smithy.utils;
 
 import java.lang.reflect.Method;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -574,6 +577,12 @@ import java.util.regex.Pattern;
  */
 public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
+    // Valid formatter characters that can be registered. Must be sorted for binary search to work.
+    static final char[] VALID_FORMATTER_CHARS = {
+            '!', '%', '&', '*', '+', ',', '-', '.', ';', '=', '@',
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '`'};
+
     private static final Pattern LINES = Pattern.compile("\\r?\\n");
     private static final Map<Character, BiFunction<Object, String, String>> DEFAULT_FORMATTERS = MapUtils.of(
             'L', (s, i) -> formatLiteral(s),
@@ -603,22 +612,19 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     /**
      * Copies settings from the given AbstractCodeWriter into this AbstractCodeWriter.
      *
-     * <p>The settings of the {@code other} AbstractCodeWriter will overwrite
-     * both global and state-based settings of this AbstractCodeWriter. Formatters of
-     * the {@code other} AbstractCodeWriter will be merged with the formatters of this
-     * AbstractCodeWriter, and in the case of conflicts, the formatters of the
-     * {@code other} will take precedence.
+     * <p>The settings of the {@code other} AbstractCodeWriter will overwrite both global and state-based settings
+     * of this AbstractCodeWriter.
      *
-     * <p>Stateful settings of the {@code other} AbstractCodeWriter are copied into
-     * the <em>current</em> state of this AbstractCodeWriter. Only the settings of
-     * the top-most state is copied. Other states, and the contents of the
-     * top-most state are not copied.
+     * <p>Stateful settings of the {@code other} AbstractCodeWriter like formatters, interceptors, and context are
+     * flattened and then copied into the <em>current</em> state of this AbstractCodeWriter. Any conflicts between
+     * formatters, interceptors, or context of the current writer are overwritten by the other writer. The stack of
+     * states and the contents written to {@code other} are not copied.
      *
      * <pre>{@code
-     * SimpleCodeWritera = new SimpleCodeWriter();
+     * SimpleCodeWriter a = new SimpleCodeWriter();
      * a.setExpressionStart('#');
      *
-     * SimpleCodeWriterb = new SimpleCodeWriter();
+     * SimpleCodeWriter b = new SimpleCodeWriter();
      * b.copySettingsFrom(a);
      *
      * assert(b.getExpressionStart() == '#');
@@ -634,6 +640,16 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
         // Copy the current state settings of other into the current state.
         currentState.copyStateFrom(other.currentState);
+
+        // Flatten containers into the current state. This is done in reverse order to ensure that more recent
+        // state changes supersede earlier changes.
+        Iterator<State> reverseOtherStates = other.states.descendingIterator();
+        while (reverseOtherStates.hasNext()) {
+            State otherState = reverseOtherStates.next();
+            currentState.interceptors.addAll(otherState.interceptors);
+            currentState.formatters.putAll(otherState.formatters);
+            currentState.context.putAll(otherState.context);
+        }
     }
 
     /**
@@ -686,7 +702,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T putFormatter(char identifier, BiFunction<Object, String, String> formatFunction) {
-        this.currentState.formatters.get().putFormatter(identifier, formatFunction);
+        this.currentState.putFormatter(identifier, formatFunction);
         return (T) this;
     }
 
@@ -962,9 +978,15 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
 
             // Don't attempt to intercept anonymous sections.
             if (!(sectionValue instanceof AnonymousCodeSection)) {
-                for (CodeInterceptor<CodeSection, T> interceptor : popped.interceptors.peek().get(sectionValue)) {
-                    result = interceptSection(popped, interceptor, result);
+                // Ensure the remaining parent interceptors are applied in the order they were inserted.
+                // This is the reverse order used when normally iterating over the states deque.
+                Iterator<State> insertionOrderedStates = states.descendingIterator();
+                while (insertionOrderedStates.hasNext()) {
+                    State state = insertionOrderedStates.next();
+                    result = applyPoppedInterceptors(popped, state, sectionValue, result);
                 }
+                // Now ensure the popped state's interceptors are applied.
+                result = applyPoppedInterceptors(popped, popped, sectionValue, result);
             }
 
             if (popped.isInline) {
@@ -986,6 +1008,13 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         return (T) this;
     }
 
+    private String applyPoppedInterceptors(State popped, State state, CodeSection sectionValue, String result) {
+        for (CodeInterceptor<CodeSection, T> interceptor : state.getInterceptors(sectionValue)) {
+            result = interceptSection(popped, interceptor, result);
+        }
+        return result;
+    }
+
     // This method exists because inlining in popSection is impossible due to needing to mutate a result variable.
     @SuppressWarnings("unchecked")
     private String interceptSection(State popped, CodeInterceptor<CodeSection, T> interceptor, String previous) {
@@ -1070,7 +1099,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T onSection(String sectionName, Consumer<Object> interceptor) {
-        currentState.interceptors.get().putInterceptor(CodeInterceptor.forName(sectionName, (w, p) -> {
+        currentState.putInterceptor(CodeInterceptor.forName(sectionName, (w, p) -> {
             String trimmedContent = removeTrailingNewline(p);
             interceptor.accept(trimmedContent);
         }));
@@ -1098,7 +1127,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public <S extends CodeSection> T onSection(CodeInterceptor<S, T> interceptor) {
-        currentState.interceptors.get().putInterceptor(interceptor);
+        currentState.putInterceptor(interceptor);
         return (T) this;
     }
 
@@ -1857,7 +1886,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
      */
     @SuppressWarnings("unchecked")
     public T putContext(String key, Object value) {
-        currentState.context.get().put(key, value);
+        currentState.context.put(key, value);
         return (T) this;
     }
 
@@ -1879,40 +1908,45 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     /**
      * Removes a named key-value pair from the context of the current state.
      *
+     * <p>This method has no effect if the parent state defines the context key value pair.
+     *
      * @param key Key to add to remove from the current context.
      * @return Returns self.
      */
     @SuppressWarnings("unchecked")
     public T removeContext(String key) {
-        if (currentState.context.peek().containsKey(key)) {
-            currentState.context.get().remove(key);
+        if (currentState.context.containsKey(key)) {
+            currentState.context.remove(key);
+        } else {
+            // Parent states might have a value for this context key, so explicitly set it to null in this context.
+            currentState.context.put(key, null);
         }
         return (T) this;
     }
 
     /**
-     * Gets a named contextual key-value pair from the current state.
+     * Gets a named contextual key-value pair from the current state or any parent states.
      *
      * @param key Key to retrieve.
      * @return Returns the associated value or null if not present.
      */
     public Object getContext(String key) {
-        CodeSection section = currentState.sectionValue;
-        Map<String, Object> currentContext = currentState.context.peek();
-        if (currentContext.containsKey(key)) {
-            return currentContext.get(key);
-        } else if (section != null) {
-            Method method = findContextMethod(section, key);
-            if (method != null) {
-                try {
-                    return method.invoke(section);
-                } catch (ReflectiveOperationException e) {
-                    String message = String.format(
-                            "Unable to get context '%s' from a matching method of the current CodeSection: %s %s",
-                            key,
-                            e.getCause() != null ? e.getCause().getMessage() : e.getMessage(),
-                            getDebugInfo());
-                    throw new RuntimeException(message, e);
+        for (State state : states) {
+            if (state.context.containsKey(key)) {
+                return state.context.get(key);
+            } else if (state.sectionValue != null) {
+                Method method = findContextMethod(state.sectionValue, key);
+                if (method != null) {
+                    try {
+                        return method.invoke(state.sectionValue);
+                    } catch (ReflectiveOperationException e) {
+                        String message = String.format(
+                                "Unable to get context '%s' from a matching method of the current CodeSection: %s %s",
+                                key,
+                                e.getCause() != null ? e.getCause().getMessage() : e.getMessage(),
+                                getDebugInfo());
+                        throw new RuntimeException(message, e);
+                    }
                 }
             }
         }
@@ -1984,7 +2018,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
     // Used only by CodeFormatter to apply formatters.
     @SuppressWarnings("unchecked")
     String applyFormatter(char identifier, Object value) {
-        BiFunction<Object, String, String> f = currentState.formatters.peek().getFormatter(identifier);
+        BiFunction<Object, String, String> f = resolveFormatter(identifier);
         if (f != null) {
             return f.apply(value, getIndentText());
         } else if (identifier == 'C') {
@@ -2009,7 +2043,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
                 throw new ClassCastException(String.format(
                         "Expected value for 'C' formatter to be an instance of %s or %s, but found %s %s",
                         Runnable.class.getName(), Consumer.class.getName(),
-                        value.getClass().getName(), getDebugInfo()));
+                        value == null ? "null" : value.getClass().getName(), getDebugInfo()));
             }
         } else {
             // Return null if no formatter was found.
@@ -2017,6 +2051,16 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         }
     }
 
+    BiFunction<Object, String, String> resolveFormatter(char identifier) {
+        for (State state : states) {
+            BiFunction<Object, String, String> result = state.getFormatter(identifier);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
+
     private final class State {
         private final boolean isRoot;
         private String indentText = "    ";
@@ -2030,9 +2074,9 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         private boolean needsIndentation;
 
         private CodeSection sectionValue;
-        private CopyOnWriteRef<Map<String, Object>> context;
-        private CopyOnWriteRef<CodeWriterFormatterContainer> formatters;
-        private CopyOnWriteRef<CodeInterceptorContainer<T>> interceptors;
+        private final Map<String, Object> context = new HashMap<>();
+        private final Map<Character, BiFunction<Object, String, String>> formatters = new HashMap<>();
+        private final List<CodeInterceptor<CodeSection, T>> interceptors = new ArrayList<>();
 
         private StringBuilder builder;
 
@@ -2046,11 +2090,7 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
         State() {
             builder = new StringBuilder();
             isRoot = true;
-            CodeWriterFormatterContainer formatterContainer = new CodeWriterFormatterContainer();
-            DEFAULT_FORMATTERS.forEach(formatterContainer::putFormatter);
-            this.formatters = CopyOnWriteRef.fromOwned(formatterContainer);
-            this.context = CopyOnWriteRef.fromOwned(new HashMap<>());
-            this.interceptors = CopyOnWriteRef.fromOwned(new CodeInterceptorContainer<>());
+            DEFAULT_FORMATTERS.forEach(this::putFormatter);
         }
 
         @SuppressWarnings("CopyConstructorMissesField")
@@ -2060,10 +2100,11 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
             this.builder = copy.builder;
         }
 
+        // This does not copy context, interceptors, or formatters.
+        // State inheritance relies on stacks of States in an AbstractCodeWriter.
         private void copyStateFrom(State copy) {
             this.newline = copy.newline;
             this.expressionStart = copy.expressionStart;
-            this.context = copy.context;
             this.indentText = copy.indentText;
             this.leadingIndentString = copy.leadingIndentString;
             this.indentation = copy.indentation;
@@ -2071,9 +2112,6 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
             this.trimTrailingSpaces = copy.trimTrailingSpaces;
             this.disableNewline = copy.disableNewline;
             this.needsIndentation = copy.needsIndentation;
-            this.context = CopyOnWriteRef.fromBorrowed(copy.context.peek(), HashMap::new);
-            this.formatters = CopyOnWriteRef.fromBorrowed(copy.formatters.peek(), CodeWriterFormatterContainer::new);
-            this.interceptors = CopyOnWriteRef.fromBorrowed(copy.interceptors.peek(), CodeInterceptorContainer::new);
         }
 
         @Override
@@ -2200,6 +2238,48 @@ public abstract class AbstractCodeWriter<T extends AbstractCodeWriter<T>> {
                 return sectionValue.sectionName();
             }
         }
+
+        void putFormatter(Character identifier, BiFunction<Object, String, String> formatFunction) {
+            if (Arrays.binarySearch(VALID_FORMATTER_CHARS, identifier) < 0) {
+                throw new IllegalArgumentException("Invalid formatter identifier: " + identifier);
+            }
+            formatters.put(identifier, formatFunction);
+        }
+
+        BiFunction<Object, String, String> getFormatter(char identifier) {
+            return formatters.get(identifier);
+        }
+
+        @SuppressWarnings("unchecked")
+        void putInterceptor(CodeInterceptor<? extends CodeSection, T> interceptor) {
+            interceptors.add((CodeInterceptor<CodeSection, T>) interceptor);
+        }
+
+        /**
+         * Gets a list of interceptors that match the given type and for which the
+         * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
+         * when given {@code forSection}.
+         *
+         * @param forSection The section that is being intercepted.
+         * @param <S> The type of section being intercepted.
+         * @return Returns the list of matching interceptors.
+         */
+        <S extends CodeSection> List<CodeInterceptor<CodeSection, T>> getInterceptors(S forSection) {
+            // Add in parent interceptors.
+            List<CodeInterceptor<CodeSection, T>> result = new ArrayList<>();
+            // Merge in local interceptors.
+            for (CodeInterceptor<CodeSection, T> interceptor : interceptors) {
+                // Add the interceptor only if it's the right type.
+                if (interceptor.sectionType().isInstance(forSection)) {
+                    // Only add if the filter passes.
+                    if (interceptor.isIntercepted(forSection)) {
+                        result.add(interceptor);
+                    }
+                }
+            }
+
+            return result;
+        }
     }
 
     String removeTrailingNewline(String value) {
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
index 8a7efaf99..04355e05a 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeFormatter.java
@@ -635,7 +635,7 @@ final class CodeFormatter {
             // Parse the formatter and apply it.
             int line = parser.line();
             int column = parser.column();
-            char identifier = parser.expect(CodeWriterFormatterContainer.VALID_FORMATTER_CHARS);
+            char identifier = parser.expect(AbstractCodeWriter.VALID_FORMATTER_CHARS);
 
             // The error message needs to be created here and given to the operation in way that it can
             // throw with an appropriate message.
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java
deleted file mode 100644
index 2a0425ae0..000000000
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptorContainer.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.smithy.utils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A container for CodeSection.Interceptors registered with AbstractCodeWriter.
- */
-@SmithyInternalApi
-final class CodeInterceptorContainer<W extends AbstractCodeWriter<W>> {
-
-    private final List<CodeInterceptor<CodeSection, W>> interceptors = new ArrayList<>();
-    private final CodeInterceptorContainer<W> parent;
-
-    CodeInterceptorContainer() {
-        this(null);
-    }
-
-    CodeInterceptorContainer(CodeInterceptorContainer<W> parent) {
-        this.parent = parent;
-    }
-
-    @SuppressWarnings("unchecked")
-    void putInterceptor(CodeInterceptor<? extends CodeSection, W> interceptor) {
-        interceptors.add((CodeInterceptor<CodeSection, W>) interceptor);
-    }
-
-    /**
-     * Gets a list of interceptors that match the given type and for which the
-     * result of {@link CodeInterceptor#isIntercepted(CodeSection)} returns true
-     * when given {@code forSection}.
-     *
-     * @param forSection The section that is being intercepted.
-     * @param <S> The type of section being intercepted.
-     * @return Returns the list of matching interceptors.
-     */
-    <S extends CodeSection> List<CodeInterceptor<CodeSection, W>> get(S forSection) {
-        // Add in parent interceptors.
-        List<CodeInterceptor<CodeSection, W>> result = parent == null
-                ? new ArrayList<>()
-                : parent.get(forSection);
-        // Merge in local interceptors.
-        for (CodeInterceptor<CodeSection, W> interceptor : interceptors) {
-            // Add the interceptor only if it's the right type.
-            if (interceptor.sectionType().isInstance(forSection)) {
-                // Only add if the filter passes.
-                if (interceptor.isIntercepted(forSection)) {
-                    result.add(interceptor);
-                }
-            }
-        }
-
-        return result;
-    }
-}
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
index e1a4ecbf3..7f25e9981 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterDebugInfo.java
@@ -42,7 +42,7 @@ public final class CodeWriterDebugInfo {
     /**
      * Get the CodeWriter state path from which the debug information was collected.
      *
-     * @return Returns the path as returned by {@link CodeWriter#getStateDebugPath()};
+     * @return Returns the path as returned by {@link AbstractCodeWriter#getDebugInfo()};
      */
     public String getStateDebugPath() {
         return getMetadata("path");
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java
deleted file mode 100644
index e2dfffebb..000000000
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriterFormatterContainer.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-package software.amazon.smithy.utils;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.BiFunction;
-
-/**
- * A container for formatters registered with CodeWriter.
- */
-@SmithyInternalApi
-final class CodeWriterFormatterContainer {
-
-    // Must be sorted for binary search to work.
-    static final char[] VALID_FORMATTER_CHARS = {
-            '!', '%', '&', '*', '+', ',', '-', '.', ';', '=', '@',
-            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
-            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_', '`'};
-
-    private final Map<Character, BiFunction<Object, String, String>> formatters = new HashMap<>();
-    private final CodeWriterFormatterContainer parent;
-
-    CodeWriterFormatterContainer() {
-        this(null);
-    }
-
-    CodeWriterFormatterContainer(CodeWriterFormatterContainer parent) {
-        this.parent = parent;
-    }
-
-    void putFormatter(Character identifier, BiFunction<Object, String, String> formatFunction) {
-        if (Arrays.binarySearch(VALID_FORMATTER_CHARS, identifier) < 0) {
-            throw new IllegalArgumentException("Invalid formatter identifier: " + identifier);
-        }
-        formatters.put(identifier, formatFunction);
-    }
-
-    BiFunction<Object, String, String> getFormatter(char identifier) {
-        BiFunction<Object, String, String> result = formatters.get(identifier);
-
-        if (result == null && parent != null) {
-            result = parent.getFormatter(identifier);
-        }
-
-        return result;
-    }
-}


Test patch:
diff --git a/smithy-aws-protocol-tests/model/restJson1/documents.smithy b/smithy-aws-protocol-tests/model/restJson1/documents.smithy
index 0d8b3318f..9ca1e68fe 100644
--- a/smithy-aws-protocol-tests/model/restJson1/documents.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/documents.smithy
@@ -324,3 +324,73 @@ apply DocumentTypeAsPayload @httpResponseTests([
         }
     }
 ])
+
+/// This example serializes documents as the value of maps.
+@idempotent
+@http(uri: "/DocumentTypeAsMapValue", method: "PUT")
+operation DocumentTypeAsMapValue {
+    input: DocumentTypeAsMapValueInputOutput,
+    output: DocumentTypeAsMapValueInputOutput,
+}
+
+structure DocumentTypeAsMapValueInputOutput {
+    docValuedMap: DocumentValuedMap,
+}
+
+map DocumentValuedMap {
+    key: String,
+    value: Document,
+}
+
+apply DocumentTypeAsMapValue @httpRequestTests([
+    {
+        id: "DocumentTypeAsMapValueInput",
+        documentation: "Serializes a map that uses documents as the value.",
+        protocol: restJson1,
+        method: "PUT",
+        uri: "/DocumentTypeAsMapValue",
+        body: """
+            {
+                "docValuedMap": {
+                    "foo": { "f": 1, "o": 2 },
+                    "bar": [ "b", "a", "r" ],
+                    "baz": "BAZ"
+                }
+            }""",
+        bodyMediaType: "application/json",
+        headers: {"Content-Type": "application/json"},
+        params: {
+            docValuedMap: {
+                "foo": { "f": 1, "o": 2 },
+                "bar": [ "b", "a", "r" ],
+                "baz": "BAZ",
+            },
+        },
+    },
+])
+
+apply DocumentTypeAsMapValue @httpResponseTests([
+    {
+        id: "DocumentTypeAsMapValueOutput",
+        documentation: "Serializes a map that uses documents as the value.",
+        protocol: restJson1,
+        code: 200,
+        body: """
+            {
+                "docValuedMap": {
+                    "foo": { "f": 1, "o": 2 },
+                    "bar": [ "b", "a", "r" ],
+                    "baz": "BAZ"
+                }
+            }""",
+        bodyMediaType: "application/json",
+        headers: {"Content-Type": "application/json"},
+        params: {
+            docValuedMap: {
+                "foo": { "f": 1, "o": 2 },
+                "bar": [ "b", "a", "r" ],
+                "baz": "BAZ",
+            },
+        },
+    },
+])
diff --git a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
index 995b18904..53a45a24c 100644
--- a/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/http-query.smithy
@@ -232,6 +232,10 @@ apply AllQueryStringTypes @httpRequestTests([
         params: {
             queryInteger: 0
             queryBoolean: false
+            queryParamsMapOfStringList: {
+                queryInteger: ["0"]
+                queryBoolean: ["false"]
+            }
         }
     }
 ])
diff --git a/smithy-aws-protocol-tests/model/restJson1/main.smithy b/smithy-aws-protocol-tests/model/restJson1/main.smithy
index 16c779b4f..f0a638594 100644
--- a/smithy-aws-protocol-tests/model/restJson1/main.smithy
+++ b/smithy-aws-protocol-tests/model/restJson1/main.smithy
@@ -87,6 +87,7 @@ service RestJson {
         // Documents
         DocumentType,
         DocumentTypeAsPayload,
+        DocumentTypeAsMapValue,
 
         // Unions
         JsonUnions,
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/AnnotationProcessorTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/AnnotationProcessorTest.java
new file mode 100644
index 000000000..7b85362c8
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/AnnotationProcessorTest.java
@@ -0,0 +1,52 @@
+package software.amazon.smithy.build.processor;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.CompilationSubject;
+import com.google.testing.compile.Compiler;
+import com.google.testing.compile.JavaFileObjects;
+import javax.tools.StandardLocation;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+/**
+ * Test the {@link SmithyAnnotationProcessor}
+ */
+public class AnnotationProcessorTest {
+    private static final Compiler compiler = Compiler.javac()
+            .withProcessors(new TestProcessorImplementation());
+
+    private static Compilation compilation;
+
+    @BeforeAll
+    static void compile() {
+        compilation = compiler.compile(JavaFileObjects.forResource(
+                "software/amazon/smithy/build/processor/package-info.java"));
+        CompilationSubject.assertThat(compilation).succeeded();
+    }
+
+    @Test
+    void generatesBasicJavaFiles() {
+        System.out.println(compilation.diagnostics());
+        CompilationSubject.assertThat(compilation)
+                .generatedFile(StandardLocation.SOURCE_OUTPUT, "com/example/testing/Empty.java")
+                .contentsAsUtf8String()
+                .isEqualTo(TestBuildPlugin.getEmptyClass("com.example.testing"));
+    }
+
+    @Test
+    void generatesMetaInfFiles() {
+        CompilationSubject.assertThat(compilation)
+                .generatedFile(StandardLocation.CLASS_OUTPUT, "META-INF/services/com.example.spi.impl.Example")
+                .contentsAsUtf8String()
+                .isEqualTo("com.example.testing.Empty");
+    }
+
+    @Test
+    void ignoresFile() {
+        CompilationSubject.assertThat(compilation).hadNoteContaining("Executing processor: TestProcessorImplementation...");
+        CompilationSubject.assertThat(compilation).hadNoteContaining("Ignoring generated file: ");
+        CompilationSubject.assertThat(compilation).hadNoteContaining("Ignored.ignored");
+    }
+}
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestBuildPlugin.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestBuildPlugin.java
new file mode 100644
index 000000000..30cbe2c5f
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestBuildPlugin.java
@@ -0,0 +1,65 @@
+package software.amazon.smithy.build.processor;
+
+import java.nio.file.Paths;
+import software.amazon.smithy.build.FileManifest;
+import software.amazon.smithy.build.PluginContext;
+import software.amazon.smithy.build.SmithyBuildPlugin;
+import software.amazon.smithy.model.node.ObjectNode;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+public class TestBuildPlugin implements SmithyBuildPlugin {
+    private static final String META_INF_LOCATION = "META-INF/services/";
+
+    @Override
+    public String getName() {
+        return "test-plugin";
+    }
+
+    @Override
+    public void execute(PluginContext context) {
+        Settings settings = Settings.from(context.getSettings());
+        FileManifest fileManifest = context.getFileManifest();
+
+        String pathifiedNamespace = settings.packageName.replace(".", "/");
+
+        // Write an empty java class
+        fileManifest.writeFile(pathifiedNamespace + "/Empty.java",
+                getEmptyClass(settings.packageName));
+
+        // Write a META-INF metadata file
+        fileManifest.writeFile(
+                Paths.get(META_INF_LOCATION + "com.example.spi.impl.Example").normalize(),
+                settings.packageName + "." + "Empty"
+        );
+
+        // Write a non-java file we expect to be ignored
+        fileManifest.writeFile(pathifiedNamespace + "/Ignored.ignored",
+                "ignore me. I dont compile!");
+
+        // Require that expected smithy shapes were correctly loaded
+        context.getModel().expectShape(ShapeId.from("com.testing.smithy#StructyMcStructFace"));
+    }
+
+    public static String getEmptyClass(String namespace) {
+        StringBuilder builder = new StringBuilder();
+        builder.append("package ").append(namespace).append(";").append(System.lineSeparator())
+                .append(System.lineSeparator())
+                .append("public final class Empty {").append(System.lineSeparator())
+                .append(System.lineSeparator())
+                .append("}").append(System.lineSeparator());
+        return builder.toString();
+    }
+
+
+    private static final class Settings {
+        private final String packageName;
+
+        private Settings(String packageName) {
+            this.packageName = packageName;
+        }
+
+        static Settings from(ObjectNode node) {
+            return new Settings(node.expectStringMember("packageName").getValue());
+        }
+    }
+}
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorAnnotation.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorAnnotation.java
new file mode 100644
index 000000000..c67c4690f
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorAnnotation.java
@@ -0,0 +1,9 @@
+package software.amazon.smithy.build.processor;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target(ElementType.PACKAGE)
+public @interface TestProcessorAnnotation {
+    String NAME = "software.amazon.smithy.build.processor.TestProcessorAnnotation";
+}
diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorImplementation.java b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorImplementation.java
new file mode 100644
index 000000000..229cd8dc1
--- /dev/null
+++ b/smithy-build/src/test/java/software/amazon/smithy/build/processor/TestProcessorImplementation.java
@@ -0,0 +1,28 @@
+package software.amazon.smithy.build.processor;
+
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.node.ObjectNode;
+
+@SupportedAnnotationTypes(TestProcessorAnnotation.NAME)
+@SupportedSourceVersion(SourceVersion.RELEASE_8)
+public class TestProcessorImplementation extends SmithyAnnotationProcessor<TestProcessorAnnotation> {
+    @Override
+    protected String getPluginName() {
+        return "test-plugin";
+    }
+
+    @Override
+    protected Class<TestProcessorAnnotation> getAnnotationClass() {
+        return TestProcessorAnnotation.class;
+    }
+
+    @Override
+    protected ObjectNode createPluginNode(TestProcessorAnnotation annotation, String packageName) {
+        return Node.objectNodeBuilder()
+                .withMember("packageName", packageName)
+                .build();
+    }
+}
diff --git a/smithy-build/src/test/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/smithy-build/src/test/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin
new file mode 100644
index 000000000..e0fc632cc
--- /dev/null
+++ b/smithy-build/src/test/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin
@@ -0,0 +1,1 @@
+software.amazon.smithy.build.processor.TestBuildPlugin
diff --git a/smithy-build/src/test/resources/META-INF/smithy/manifest b/smithy-build/src/test/resources/META-INF/smithy/manifest
new file mode 100644
index 000000000..3f2bc2164
--- /dev/null
+++ b/smithy-build/src/test/resources/META-INF/smithy/manifest
@@ -0,0 +1,1 @@
+structs-n-stuff.smithy
diff --git a/smithy-build/src/test/resources/META-INF/smithy/structs-n-stuff.smithy b/smithy-build/src/test/resources/META-INF/smithy/structs-n-stuff.smithy
new file mode 100644
index 000000000..5066e4a0e
--- /dev/null
+++ b/smithy-build/src/test/resources/META-INF/smithy/structs-n-stuff.smithy
@@ -0,0 +1,7 @@
+$version: "2.0"
+
+namespace com.testing.smithy
+
+structure StructyMcStructFace {
+    fieldA: String
+}
diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/processor/package-info.java b/smithy-build/src/test/resources/software/amazon/smithy/build/processor/package-info.java
new file mode 100644
index 000000000..60bb108c7
--- /dev/null
+++ b/smithy-build/src/test/resources/software/amazon/smithy/build/processor/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Generates Test files
+ */
+@TestProcessorAnnotation
+package com.example.testing;
+
+import software.amazon.smithy.build.processor.TestProcessorAnnotation;
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java
index fb2313380..05ea13753 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/DiffCommandTest.java
@@ -71,6 +71,24 @@ public class DiffCommandTest {
         });
     }
 
+    @Test
+    public void canWriteCsvOutput() {
+        IntegUtils.withTempDir("diff", dir -> {
+            Path a = dir.resolve("a.smithy");
+            writeFile(a, "$version: \"2.0\"\nnamespace example\nstring A\n");
+
+            Path b = dir.resolve("b.smithy");
+            writeFile(b, "$version: \"2.0\"\nnamespace example\n@aaaaaa\nstring A\n");
+
+            RunResult result = IntegUtils.run(dir, ListUtils.of("diff", "--old", a.toString(),
+                                                                "--new", b.toString(),
+                                                                "--format", "csv"));
+            assertThat("Not 1: output [" + result.getOutput() + ']', result.getExitCode(), is(1));
+            assertThat(result.getOutput(), containsString("severity,id,"));
+            assertThat(result.getOutput(), containsString("ERROR"));
+        });
+    }
+
     @Test
     public void showsLabelForDiffEvents() {
         IntegUtils.withTempDir("diff", dir -> {
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/DiffCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/DiffCommandTest.java
new file mode 100644
index 000000000..19a2150ca
--- /dev/null
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/DiffCommandTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.cli.CliUtils;
+
+public class DiffCommandTest {
+    @Test
+    public void canOutputCsv() throws Exception {
+        Path oldModel = Paths.get(getClass().getResource("diff/old.smithy").toURI());
+        Path newModel = Paths.get(getClass().getResource("diff/new.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("diff",
+                                                    "--old", oldModel.toString(),
+                                                    "--new", newModel.toString(),
+                                                    "--format", "csv");
+
+        assertThat(result.code(), not(0));
+
+        // Make sure FAILURE is sent to stderr.
+        assertThat(result.stderr(), containsString("FAILURE"));
+        assertThat(result.stdout(), not(containsString("FAILURE")));
+
+        String[] lines = result.stdout().split("(\\r\\n|\\r|\\n)");
+        assertThat(lines.length, is(2));
+        assertThat(lines[0], containsString("severity,id,shape,file,line,column,message,hint,suppressionReason"));
+        assertThat(lines[1], containsString("\"ERROR\",\"ChangedShapeType\",\"smithy.example#Hello\""));
+    }
+}
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 b3f2fcf32..7461b109b 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
@@ -18,6 +18,7 @@ package software.amazon.smithy.cli.commands;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 
 import java.net.URISyntaxException;
@@ -151,4 +152,107 @@ public class ValidateCommandTest {
         assertThat(result.code(), not(0));
         assertThat(result.stderr(), containsString("Invalid severity"));
     }
+
+    @Test
+    public void showAndHideAreExclusive() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", "X", "--hide-validators", "Y");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("--show-validators and --hide-validators are mutually exclusive"));
+    }
+
+    @Test
+    public void hideAndShowAreExclusive() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--hide-validators", "Y", "--show-validators", "X");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("--show-validators and --hide-validators are mutually exclusive"));
+    }
+
+    @Test
+    public void validatesEventIdsAreNotEmpty() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", "Foo,,Bar");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Invalid validation event ID"));
+    }
+
+    @Test
+    public void validatesEventIdsAreNotBlank() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", "Foo, ,Bar");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Invalid validation event ID"));
+    }
+
+    @Test
+    public void validatesEventIdsAreNotLeadingEmpty() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--show-validators", ",Bar");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Invalid validation event ID"));
+    }
+
+    @Test
+    public void canShowEventsById() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--debug", "--show-validators", "EmitWarnings",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), not(containsString("EmitDangers")));
+        assertThat(result.stdout(), not(containsString("HttpLabelTrait")));
+    }
+
+    @Test
+    public void canHideEventsById() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--debug", "--hide-validators", "EmitDangers",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), not(containsString("EmitDangers")));
+        assertThat(result.stdout(), containsString("HttpLabelTrait"));
+    }
+
+    @Test
+    public void canOutputCsv() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--format", "csv",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), containsString("suppressionReason"));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), containsString("EmitDangers"));
+        assertThat(result.stdout(), containsString("HttpLabelTrait"));
+        assertThat(result.stdout(), not(containsString("FAILURE"))); // stderr
+
+        String[] lines = result.stdout().split("(\\r\\n|\\r|\\n)");
+        assertThat(lines[0], containsString("severity,id,shape,file,line,column,message,hint,suppressionReason"));
+    }
+
+    @Test
+    public void canOutputText() throws Exception {
+        Path validationEventsModel = Paths.get(getClass().getResource("validation-events.smithy").toURI());
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--format", "text",
+                                                    validationEventsModel.toString());
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stdout(), not(containsString("suppressionReason")));
+        assertThat(result.stdout(), containsString("EmitWarnings"));
+        assertThat(result.stdout(), containsString("EmitDangers"));
+        assertThat(result.stdout(), containsString("HttpLabelTrait"));
+        assertThat(result.stdout(), not(containsString("FAILURE"))); // stderr
+    }
+
+    @Test
+    public void outputFormatMustBeValid() {
+        CliUtils.Result result = CliUtils.runSmithy("validate", "--format", "HELLO");
+
+        assertThat(result.code(), not(0));
+        assertThat(result.stderr(), containsString("Unexpected --format: `HELLO`"));
+    }
 }
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/new.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/new.smithy
new file mode 100644
index 000000000..cdae50e01
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/new.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated
+string Hello
+
+structure Foo {
+    hello: Hello
+}
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/old.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/old.smithy
new file mode 100644
index 000000000..9bfda345b
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/diff/old.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated
+integer Hello
+
+structure Foo {
+    hello: Hello
+}
diff --git a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors
index f68673b92..f98c5c12b 100644
--- a/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors
+++ b/smithy-linters/src/test/resources/software/amazon/smithy/linters/errorfiles/camel-case-validator-defaults-test.errors
@@ -38,6 +38,3 @@
 [DANGER] ns.baz#lowerStructureTrait$UpperCamelCase: Member shape member name, `UpperCamelCase`, is not lower camel case; members in the ns.baz namespace must all use lower camel case. | DefaultCamelCase
 [DANGER] ns.foo#Structure$lowerCamelCase: Member shape member name, `lowerCamelCase`, is not upper camel case; members in the MyService must all use upper camel case. | DefaultCamelCase
 [DANGER] ns.foo#Structure$snake_case: Member shape member name, `snake_case`, is not upper camel case; members in the MyService must all use upper camel case. | DefaultCamelCase
-
-[NOTE] ns.foo#Enum: The enum shape is not connected to from any service shape. | UnreferencedShape
-[NOTE] ns.foo#IntEnum: The intEnum shape is not connected to from any service shape. | UnreferencedShape
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
index dfce68688..a0ae485b7 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlInternalTokenizerTest.java
@@ -21,9 +21,13 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.startsWith;
 
+import java.util.stream.Stream;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class IdlInternalTokenizerTest {
 
@@ -181,4 +185,81 @@ public class IdlInternalTokenizerTest {
         assertThat(lines, equalTo("Hi\nThere\n123\n456"));
         MatcherAssert.assertThat(tokenizer.removePendingDocCommentLines(), nullValue());
     }
+
+    public static Stream<Arguments> textBlockTests() {
+        return Stream.of(
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "        - Indented\"\"\"\n",
+                        "Hello\n    - Indented"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "        - Indented\n"
+                        + "    \"\"\"\n",
+                        "Hello\n    - Indented\n"
+                ),
+                Arguments.of(
+                    "\"\"\"\n"
+                    + "    Hello\n"
+                    + "        - Indented\n"
+                    + "\"\"\"\n",
+                    "    Hello\n        - Indented\n"
+                ),
+                Arguments.of(
+                    "\"\"\"\n"
+                    + "    Hello\"\"\"\n",
+                    "Hello"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "    Hello\n"
+                        + "\n"
+                        + "        - Indented\n"
+                        + "\"\"\"\n",
+                        "    Hello\n\n        - Indented\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          Hello\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          \"\"\"",
+                        "\nHello\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\n" // empty lines are incidental whitespace.
+                        + "          Hello\n"
+                        + "                        \n" // only WS doesn't influence line length calculations.
+                        + "          \"\"\"",
+                        "\nHello\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\n" // empty lines are incidental whitespace.
+                        + "Hello\n"
+                        + "\n"
+                        + "\n"
+                        + "\"\"\"",
+                        "\nHello\n\n\n"
+                ),
+                Arguments.of(
+                        "\"\"\"\n"
+                        + "\"\"\"",
+                        ""
+                )
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("textBlockTests")
+    public void parsesTextBlocks(String model, String stringValue) {
+        IdlInternalTokenizer tokenizer = new IdlInternalTokenizer("a.smithy", model);
+        tokenizer.expect(IdlToken.TEXT_BLOCK);
+
+        assertThat(tokenizer.getCurrentTokenStringSlice().toString(), equalTo(stringValue));
+    }
 }
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java
index c7e1279a6..8abf05328 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidationEventDecoratorTest.java
@@ -17,20 +17,16 @@ package software.amazon.smithy.model.loader;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.node.ObjectNode;
 import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.validation.NodeValidationVisitorTest;
 import software.amazon.smithy.model.validation.ValidatedResult;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.model.validation.ValidationEventDecorator;
@@ -40,23 +36,19 @@ import software.amazon.smithy.utils.SetUtils;
 
 public class ValidationEventDecoratorTest {
 
-    static final String HINT = "Consider connecting this structure to a service";
-    static final String UNREFERENCED_SHAPE_EVENT_ID = "UnreferencedShape";
-    static final Set<ShapeId> STRUCT_SHAPE_IDS = SetUtils.of(ShapeId.from("ns.foo#Structure"),
-                                                             ShapeId.from("ns.foo#Structure2"),
-                                                             ShapeId.from("ns.foo#Structure3"),
-                                                             ShapeId.from("ns.foo#Structure4"));
+    static final String HINT = "We had to deprecate this";
+    static final String SHAPE_EVENT_ID = "DeprecatedShape";
+    static final Set<ShapeId> MATCHING_SHAPE_IDS = SetUtils.of(ShapeId.from("smithy.example#Foo$a"));
 
     @Test
     public void canDecorateValidationEvents() {
         ValidatedResult<Model> result = Model.assembler()
-                                             .addImport(NodeValidationVisitorTest.class.getResource("node-validator"
-                                                                                                    + ".json"))
-                                             .validatorFactory(testFactory(new DummyHintValidationEventDecorator()))
-                                             .assemble();
+                .addImport(getClass().getResource("validation-event-decorator-test.smithy"))
+                .validatorFactory(testFactory(new TestValidationEventDecorator()))
+                .assemble();
         for (ValidationEvent event : result.getValidationEvents()) {
             ShapeId eventShapeId = event.getShapeId().orElse(null);
-            if (STRUCT_SHAPE_IDS.contains(eventShapeId)) {
+            if (MATCHING_SHAPE_IDS.contains(eventShapeId)) {
                 assertThat(event.getHint().isPresent(), equalTo(true));
                 assertThat(event.getHint().get(), equalTo(HINT));
             } else {
@@ -69,10 +61,9 @@ public class ValidationEventDecoratorTest {
     public void exceptionsAreNotCaughtWhenDecoratorsThrow() {
         assertThrows(RuntimeException.class, () -> {
             Model.assembler()
-                 .addImport(NodeValidationVisitorTest.class.getResource("node-validator"
-                                                                        + ".json"))
-                 .validatorFactory(testFactory(new ThrowingValidationEventDecorator()))
-                 .assemble();
+                    .addImport(getClass().getResource("validation-event-decorator-test.smithy"))
+                    .validatorFactory(testFactory(new ThrowingValidationEventDecorator()))
+                    .assemble();
         });
     }
 
@@ -96,11 +87,10 @@ public class ValidationEventDecoratorTest {
         };
     }
 
-    static class DummyHintValidationEventDecorator implements ValidationEventDecorator {
-
+    static class TestValidationEventDecorator implements ValidationEventDecorator {
         @Override
         public boolean canDecorate(ValidationEvent ev) {
-            return ev.containsId(UNREFERENCED_SHAPE_EVENT_ID) && ev.getMessage().contains("The structure ");
+            return ev.containsId(SHAPE_EVENT_ID);
         }
 
         @Override
@@ -110,15 +100,14 @@ public class ValidationEventDecoratorTest {
     }
 
     static class ThrowingValidationEventDecorator implements ValidationEventDecorator {
-
         @Override
         public boolean canDecorate(ValidationEvent ev) {
-            return ev.containsId(UNREFERENCED_SHAPE_EVENT_ID) && ev.getMessage().contains("The structure ");
+            return ev.containsId(SHAPE_EVENT_ID);
         }
 
         @Override
         public ValidationEvent decorate(ValidationEvent ev) {
-            throw new RuntimeException("ups");
+            throw new RuntimeException("oops!");
         }
     }
 }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors
index 4b8456e12..ea6d0c6c1 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors
@@ -22,8 +22,6 @@
 [ERROR] ns.foo#NInput$a: This `a` structure member is marked with the `httpLabel` trait, but no corresponding `http` URI label could be found when used as the input of the `ns.foo#N` operation. | HttpLabelTrait
 [ERROR] ns.foo#PInput$a: The `a` structure member corresponds to a greedy label when used as the input of the `ns.foo#P` operation. This member targets (integer: `ns.foo#Integer`), but greedy labels must target string shapes. | HttpLabelTrait
 [ERROR] ns.foo#RInput$b: `httpHeader` binding of `x-foo` conflicts with the `httpPrefixHeaders` binding of `ns.foo#RInput$a` to ``. `httpHeader` bindings must not case-insensitively start with any `httpPrefixHeaders` bindings. | HttpPrefixHeadersTrait
-[NOTE] ns.foo#BadError: The structure shape is not connected to from any service shape. | UnreferencedShape
-[NOTE] ns.foo#BadErrorMultipleBindings: The structure shape is not connected to from any service shape. | UnreferencedShape
 [ERROR] ns.foo#GInput$b: Trait `httpHeader` cannot be applied to `ns.foo#GInput$b`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, list > member > :test(boolean, number, string, timestamp))) | TraitTarget
 [ERROR] ns.foo#GOutput$b: Trait `httpHeader` cannot be applied to `ns.foo#GOutput$b`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, list > member > :test(boolean, number, string, timestamp))) | TraitTarget
 [ERROR] ns.foo#HInput$a: Trait `httpHeader` cannot be applied to `ns.foo#HInput$a`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, number, string, timestamp, list > member > :test(boolean, number, string, timestamp))) | TraitTarget
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors
index eca0fad78..66999efd6 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/linters/emit-each-selector-validator.errors
@@ -12,16 +12,13 @@
 [DANGER] ns.foo#Long: Selector capture matched selector: long | long
 [DANGER] ns.foo#Long: Selector capture matched selector: number | number
 [DANGER] ns.foo#Long: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Long: The long shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#Float: Selector capture matched selector: :is(long, float, boolean) | any
 [DANGER] ns.foo#Float: Selector capture matched selector: float | float
 [DANGER] ns.foo#Float: Selector capture matched selector: number | number
 [DANGER] ns.foo#Float: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Float: The float shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#Boolean: Selector capture matched selector: :is(long, float, boolean) | any
 [DANGER] ns.foo#Boolean: Selector capture matched selector: boolean | boolean
 [DANGER] ns.foo#Boolean: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Boolean: The boolean shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|mediaType$='plain'] | traitEndsWith
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|mediaType*='PLAIN' i] | traitContainsCaseInsensitive
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|mediaType*='plain'] | traitContains
@@ -33,10 +30,8 @@
 [DANGER] ns.foo#Blob: Selector capture matched selector: [trait|smithy.api#mediaType*=plain] | traitContains
 [DANGER] ns.foo#Blob: Selector capture matched selector: blob | blob
 [DANGER] ns.foo#Blob: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#Blob: The blob shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#List: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not
 [DANGER] ns.foo#List: Selector capture matched selector: list | list
-[NOTE] ns.foo#List: The list shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] ns.foo#List$member: Selector capture matched selector: :not(:is([trait|error], simpleType)) | not
 [DANGER] ns.foo#List$member: Selector capture matched selector: :test(member > [id='ns.foo#String']) | memberTargetsString
 [DANGER] ns.foo#List$member: Selector capture matched selector: > | valid-neighbor-only
@@ -90,8 +85,6 @@
 [DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: > | valid-neighbor-only
 [DANGER] ns.foo#OperationBInput$id: Selector capture matched selector: member | member
 [DANGER] ns.foo#UtcTimestamp: Selector capture matched selector: simpleType | simpleType
-[NOTE] ns.foo#UtcTimestamp: The timestamp shape is not connected to from any service shape. | UnreferencedShape
 [DANGER] other.ns#String: Selector capture matched selector: [id|name="String"] | shapeName
 [DANGER] other.ns#String: Selector capture matched selector: [id|name='String'] | shapeName
 [DANGER] other.ns#String: Selector capture matched selector: simpleType | simpleType
-[NOTE] other.ns#String: The string shape is not connected to from any service shape. | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.errors
deleted file mode 100644
index caa3aadb1..000000000
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.errors
+++ /dev/null
@@ -1,2 +0,0 @@
-[NOTE] ns.foo#UnreferencedString: The string shape is not connected to from any service shape. | UnreferencedShape
-[NOTE] ns.foo#UnreferencedStructure: The structure shape is not connected to from any service shape. | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.json
deleted file mode 100644
index a634b841e..000000000
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shape-validator.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
-    "smithy": "2.0",
-    "shapes": {
-        "ns.foo#MyService": {
-            "type": "service",
-            "version": "2017-01-17",
-            "operations": [
-                {
-                    "target": "ns.foo#ReferencedOperation"
-                }
-            ]
-        },
-        "ns.foo#ReferencedOperation": {
-            "type": "operation",
-            "input": {
-                "target": "ns.foo#ReferencedOperationInput"
-            },
-            "output": {
-                "target": "ns.foo#ReferencedOperationOutput"
-            },
-            "errors": [
-                {
-                    "target": "ns.foo#ReferencedOperationError"
-                }
-            ],
-            "traits": {
-                "smithy.api#readonly": {}
-            }
-        },
-        "ns.foo#ReferencedOperationInput": {
-            "type": "structure",
-            "members": {
-                "int": {
-                    "target": "ns.foo#ReferencedInteger"
-                }
-            },
-            "traits": {
-                "smithy.api#input": {}
-            }
-        },
-        "ns.foo#ReferencedOperationOutput": {
-            "type": "structure",
-            "members": {
-                "float": {
-                    "target": "ns.foo#ReferencedFloat"
-                }
-            },
-            "traits": {
-                "smithy.api#output": {}
-            }
-        },
-        "ns.foo#ReferencedInteger": {
-            "type": "integer"
-        },
-        "ns.foo#ReferencedOperationError": {
-            "type": "structure",
-            "traits": {
-                "smithy.api#error": "client"
-            }
-        },
-        "ns.foo#ReferencedFloat": {
-            "type": "float"
-        },
-        "ns.foo#UnreferencedStructure": {
-            "type": "structure",
-            "members": {
-                "foo": {
-                    "target": "ns.foo#UnreferencedString"
-                }
-            }
-        },
-        "ns.foo#UnreferencedString": {
-            "type": "string"
-        }
-    }
-}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.errors
new file mode 100644
index 000000000..a36014120
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.errors
@@ -0,0 +1,2 @@
+[WARNING] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `[trait|smithy.example#root]` | UnreferencedShape
+[WARNING] smithy.example#Bam: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `[trait|smithy.example#root]` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.smithy
new file mode 100644
index 000000000..960b44e40
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/custom-root.smithy
@@ -0,0 +1,37 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        // Elevate the severity so that NOTE events don't slip through.
+        severity: "WARNING"
+        configuration: {
+            rootShapeSelector: "[trait|smithy.example#root]"
+        }
+    }
+]
+
+namespace smithy.example
+
+// Considered referenced because it's a trait.
+@trait
+structure root {}
+
+// Considered referenced because of the root trait.
+@root
+structure MyAbc {
+    MyString: MyString
+}
+
+// Considered referenced because it's referenced by MyAbc$MyString
+string MyString
+
+// Considered referenced because of the root trait.
+@root
+string MyDef
+
+// Unreferenced.
+string Baz
+
+// Unreferenced.
+string Bam
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.errors
new file mode 100644
index 000000000..09f4ca5b1
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.errors
@@ -0,0 +1,1 @@
+[ERROR] -: Error creating `UnreferencedShape` validator: Error parsing | Model
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.smithy
new file mode 100644
index 000000000..84c0132c8
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/invalid-root-selector.smithy
@@ -0,0 +1,10 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        configuration: {
+            rootShapeSelector: "!!!!"
+        }
+    }
+]
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.errors
new file mode 100644
index 000000000..34611ace1
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.errors
@@ -0,0 +1,1 @@
+[NOTE] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.smithy
new file mode 100644
index 000000000..4110b262c
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/selector-defaults-to-services.smithy
@@ -0,0 +1,29 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+    }
+]
+
+namespace smithy.example
+
+// A "root" shape, so does not emit.
+service MyService {
+    operations: [
+        MyOperation
+    ]
+}
+
+// Connected.
+operation MyOperation {
+    input := {
+        foo: MyString
+    }
+}
+
+// Connected.
+string MyString
+
+// Not connected.
+string Baz
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.errors
new file mode 100644
index 000000000..eabbbc6e9
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.errors
@@ -0,0 +1,2 @@
+[NOTE] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
+[NOTE] smithy.example#Bam: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.smithy
new file mode 100644
index 000000000..76bb6cf8a
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-multiple-matches.smithy
@@ -0,0 +1,48 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        configuration: {
+            rootShapeSelector: "service"
+        }
+    }
+]
+
+namespace smithy.example
+
+service MyService1 {
+    operations: [
+        MyOperation1
+    ]
+}
+
+service MyService2 {
+    operations: [
+        MyOperation2
+    ]
+}
+
+operation MyOperation1 {
+    input := {
+        foo: MyAbc
+    }
+}
+
+operation MyOperation2 {
+    output := {
+        bar: MyDef
+    }
+}
+
+structure MyAbc {
+    MyString: MyString
+}
+
+string MyString
+
+string MyDef
+
+string Baz
+
+string Bam
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.errors
new file mode 100644
index 000000000..eabbbc6e9
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.errors
@@ -0,0 +1,2 @@
+[NOTE] smithy.example#Baz: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
+[NOTE] smithy.example#Bam: This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `service` | UnreferencedShape
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.smithy
new file mode 100644
index 000000000..8b6a3f5e1
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unreferenced-shapes/service-root-single-match.smithy
@@ -0,0 +1,39 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "UnreferencedShape"
+        configuration: {
+            rootShapeSelector: "service"
+        }
+    }
+]
+
+namespace smithy.example
+
+service MyService {
+    operations: [
+        MyOperation
+    ]
+}
+
+operation MyOperation {
+    input := {
+        foo: MyAbc
+    }
+    output := {
+        bar: MyDef
+    }
+}
+
+structure MyAbc {
+    MyString: MyString
+}
+
+string MyString
+
+string MyDef
+
+string Baz
+
+string Bam
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/validation-event-decorator-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/validation-event-decorator-test.smithy
new file mode 100644
index 000000000..9230df426
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/validation-event-decorator-test.smithy
@@ -0,0 +1,13 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@deprecated
+string MyString
+
+string OtherString
+
+structure Foo {
+    a: MyString
+    b: OtherString
+}
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
index 9a39712d8..0e4d4b241 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
@@ -9,7 +9,7 @@ metadata validators = [
         configuration: {
             selector: """
                 operation -[input]-> structure > member
-                :test(member > string:not([trait|enum]))
+                    :test(member > string:not([trait|enum]))
                 :test(member > string:not([trait|length]))
                 :test(member > string:not([trait|aws.api#arnReference]))
                 :test(member > string:not([trait|aws.api#providesPassRole]))
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
index 899acc3ee..242e7b96f 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
@@ -2,6 +2,7 @@ $version: "2.0"
 
 namespace smithy.example
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Foo.
@@ -9,6 +10,7 @@ namespace smithy.example
 )
 string Foo
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Bar.
@@ -17,6 +19,7 @@ string Foo
 )
 string Bar
 
+// The quotes and text will be indented inside the parens when formatted.
 @documentation(
     """
     This is the documentation for Baz.
@@ -25,10 +28,61 @@ string Bar
 )
 string Baz
 
+// The opening quotes will be indented and move to the next line, the contents of the block will remain indented
+// because indentation is based on the closing quote, the closing quote will be indented, and the closing paren on
+// the next line.
 @documentation(
     """
-    This is the documentation for Bux.
-    Lorem ipsum dolor.
+        This is the documentation for Bux.
+        Lorem ipsum dolor.
     """
 )
 string Bux
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on last line, and the closing paren will move to the next line.
+@documentation(
+    """
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }"""
+)
+string JsonDocsNoTrailingNewLine
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on its own line, and the closing paren will move to the next line.
+@documentation(
+    """
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }
+    """
+)
+string JsonDocsTrailingNewLine
+
+// Ensure extra newlines at the end are included.
+@documentation(
+    """
+    Hello with extra newlines.
+
+
+    """
+)
+string ExtraTrailingNewlines
+
+// Moves the quotes to the same indentation level and the closing parens to the next line.
+@documentation(
+    """
+    """
+)
+string EmptyTextBlock
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
index 834b3579b..2527d7609 100644
--- a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.smithy
@@ -2,6 +2,7 @@ $version: "2.0"
 
 namespace smithy.example
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Foo.
@@ -9,6 +10,7 @@ namespace smithy.example
 )
 string Foo
 
+// This will look exactly the same when formatted.
 @documentation(
     """
     This is the documentation for Bar.
@@ -17,6 +19,7 @@ string Foo
 )
 string Bar
 
+// The quotes and text will be indented inside the parens when formatted.
 @documentation(
 """
 This is the documentation for Baz.
@@ -25,8 +28,51 @@ Lorem ipsum dolor.
 )
 string Baz
 
+// The opening quotes will be indented and move to the next line, the contents of the block will remain indented
+// because indentation is based on the closing quote, the closing quote will be indented, and the closing paren on
+// the next line.
 @documentation("""
     This is the documentation for Bux.
     Lorem ipsum dolor.
 """)
 string Bux
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on last line, and the closing paren will move to the next line.
+@documentation("""
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }""")
+string JsonDocsNoTrailingNewLine
+
+// The opening quote will move to the next line, indentend, the content will remain the same, the closing quote
+// will stay on its own line, and the closing paren will move to the next line.
+@documentation("""
+    This doc string must not have the leading whitespace altered.
+
+    {
+        "foo": true,
+        bar: [
+            false
+        ]
+    }
+    """)
+string JsonDocsTrailingNewLine
+
+// Ensure extra newlines at the end are included.
+@documentation("""
+    Hello with extra newlines.
+
+
+    """)
+string ExtraTrailingNewlines
+
+// Moves the quotes to the same indentation level and the closing parens to the next line.
+@documentation("""
+    """)
+string EmptyTextBlock
diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
index af0faf970..c87738c88 100644
--- a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
+++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java
@@ -23,8 +23,10 @@ import static org.hamcrest.Matchers.startsWith;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -982,6 +984,26 @@ public class CodeWriterTest {
         assertThat(a.toString(), equalTo("\n"));
     }
 
+    @Test
+    public void flattensStatesOfOtherWhenCopying() {
+        CodeWriter a = new CodeWriter();
+        a.pushState();
+        a.putFormatter('A', (value, indent) -> value.toString().toUpperCase(Locale.ENGLISH));
+        a.putContext("a", 1);
+        a.putContext("b", "B");
+        a.pushState();
+        a.putContext("b", 2); // override "b"
+        a.putFormatter('B', (value, indent) -> value.toString().toLowerCase(Locale.ENGLISH));
+
+        CodeWriter b = new CodeWriter();
+        b.putContext("c", 3);
+        b.copySettingsFrom(a);
+        b.writeInline("${1A} ${2B} ${a:L} ${b:L} ${c:L}", "Hello", "Goodbye");
+
+        assertThat(b.toString(), equalTo("HELLO goodbye 1 2 3\n"));
+        assertThat(a.toString(), equalTo("\n"));
+    }
+
     @Test
     public void canPassRunnableToFormatters() {
         CodeWriter writer = new CodeWriter();
@@ -1451,6 +1473,34 @@ public class CodeWriterTest {
         assertThat(writer.toString(), equalTo("foo: ok actually\n"));
     }
 
+    @Test
+    public void canAccessStateClassesOfInheritedStates() {
+        SimpleCodeWriter writer = new SimpleCodeWriter();
+        writer.putContext("a", "A");
+        writer.pushState();
+        writer.pushState(new CustomSection());
+        writer.putContext("b", "B");
+        writer.pushState();
+        writer.write("${a:L}, ${b:L}, ${hello:L}${partial:C|}");
+        writer.popState();
+
+        assertThat(writer.toString(), equalTo("A, B, Hello, 0T, 1T, 2T\n"));
+    }
+
+    @Test
+    public void canRemoveContextFromTheCurrentState() {
+        SimpleCodeWriter writer = new SimpleCodeWriter();
+        writer.putContext("a", "A");
+        writer.pushState();
+        writer.putContext("b", "B");
+        writer.removeContext("a");
+
+        writer.write("${a:L}, ${b:L}");
+        writer.popState();
+
+        assertThat(writer.toString(), equalTo(", B\n"));
+    }
+
     private static final class MySection implements CodeSection {
         public String getFoo() {
             return "foo";
@@ -1496,4 +1546,25 @@ public class CodeWriterTest {
             return false;
         }
     }
+
+    public static final class CustomSection implements CodeSection {
+        public String hello() {
+            return "Hello";
+        }
+
+        public List<String> values() {
+            return ListUtils.of("a", "b", "c");
+        }
+
+        public String test() {
+            return "T";
+        }
+
+        public Consumer<AbstractCodeWriter<?>> partial() {
+            // This partial ensures that nested contexts can reach up to parent contexts and sections to get values.
+            return writer -> {
+                writer.write("${#values}, ${key:L}${test:L}${/values}");
+            };
+        }
+    }
 }