Repository: smithy-lang/smithy
Pull request #2146 (2024-02-16 18:35:47)

Add gradle plugin migration guide
#### Background
* Adds a migration guide to help users move from the old version of the Smithy gradle plugin to the newer versions without breaking their existing projects.

#### Testing
Built locally with `make html`

[Built artifacts link](https://github.com/smithy-lang/smithy/actions/runs/7934810756/artifacts/1252081313)

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/docs/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
index 5aec6b216..4139418b2 100644
--- a/docs/source-2.0/guides/gradle-plugin/index.rst
+++ b/docs/source-2.0/guides/gradle-plugin/index.rst
@@ -8,6 +8,12 @@ The `Smithy Gradle plugins`_ integrate Smithy with the `Gradle`_ build system. T
 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
 =======
 
@@ -151,6 +157,8 @@ Runtime dependencies can be added directly to the ``runtimeClasspath`` configura
 or to a configuration that extends ``runtimeClasspath``, such as the ``implementation``
 configuration added by the ``java-library`` plugin.
 
+.. _dependencies:
+
 Build Dependencies
 ^^^^^^^^^^^^^^^^^^
 
@@ -256,8 +264,6 @@ This extension supports the following properties:
       - ``Directory``
       - Defines where Smithy build artifacts are written.
 
-.. _dependencies:
-
 
 Customize output directory
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -382,6 +388,8 @@ process if necessary. To run Smithy CLI commands in a process set the
             fork = true
         }
 
+.. _disable-smithy-formatter:
+
 Disable Smithy Formatter
 ^^^^^^^^^^^^^^^^^^^^^^^^
 
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-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:


Test patch:
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-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