Repository: smithy-lang/smithy
Pull request #1875 (2023-07-21 15:43:43)

Fix indentation when formatting text blocks
Updates the formatter to indent the node value of a node kvp when they are text blocks.

A test case was added for a text block as the node value of a node kvp, and a test case was updated for when text blocks are in 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/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java
index caeef6d5b..6a13df5b6 100644
--- a/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java
+++ b/smithy-syntax/src/main/java/software/amazon/smithy/syntax/Formatter.java
@@ -17,10 +17,12 @@ 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.loader.ModelSyntaxException;
 import software.amazon.smithy.model.shapes.ShapeId;
@@ -178,6 +180,7 @@ public final class Formatter {
                 case APPLY_STATEMENT:
                 case NODE_VALUE:
                 case NODE_KEYWORD:
+                case NODE_STRING_VALUE:
                 case SIMPLE_TYPE_NAME:
                 case ENUM_TYPE_NAME:
                 case AGGREGATE_TYPE_NAME:
@@ -408,12 +411,25 @@ public final class Formatter {
                 }
 
                 case NODE_OBJECT_KVP: {
+                    // Since text blocks span multiple lines, when they are the NODE_VALUE for NODE_OBJECT_KVP,
+                    // they have to be indented. Since we only format valid models, NODE_OBJECT_KVP is guaranteed to
+                    // have a NODE_VALUE child.
+                    TreeCursor nodeValue = cursor.getFirstChild(TreeType.NODE_VALUE);
+                    boolean isTextBlock = Optional.ofNullable(nodeValue.getFirstChild(TreeType.NODE_STRING_VALUE))
+                            .map(nodeString -> nodeString.getFirstChild(TreeType.TEXT_BLOCK))
+                            .isPresent();
+                    Doc nodeValueDoc = visit(nodeValue);
+                    if (isTextBlock) {
+                        nodeValueDoc = nodeValueDoc.indent(4);
+                    }
+
+
                     // Hoist awkward comments in the KVP *before* the KVP rather than between the values and colon.
                     // If there is an awkward comment before the TRAIT value, hoist it above the statement.
                     return skippedComments(cursor, false)
                             .append(visit(cursor.getFirstChild(TreeType.NODE_OBJECT_KEY)))
                             .append(Doc.text(": "))
-                            .append(visit(cursor.getFirstChild(TreeType.NODE_VALUE)));
+                            .append(nodeValueDoc);
                 }
 
                 case NODE_OBJECT_KEY: {
@@ -427,9 +443,17 @@ public final class Formatter {
                            : Doc.text(tree.concatTokens());
                 }
 
+                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);
+                }
+
                 case TOKEN:
-                case TEXT_BLOCK:
-                case NODE_STRING_VALUE:
                 case QUOTED_TEXT:
                 case NUMBER:
                 case SHAPE_ID:


Test patch:
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
new file mode 100644
index 000000000..9a39712d8
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.formatted.smithy
@@ -0,0 +1,21 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "EmitEachSelector"
+        id: "MissingStringInputLengthValidation"
+        severity: "DANGER"
+        message: "This string is missing required length trait"
+        configuration: {
+            selector: """
+                operation -[input]-> structure > member
+                :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]))
+                """
+        }
+    }
+]
+
+namespace smithy.example
diff --git a/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.smithy b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.smithy
new file mode 100644
index 000000000..45e1b5648
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/node-text-blocks.smithy
@@ -0,0 +1,21 @@
+$version: "2.0"
+
+metadata validators = [
+    {
+        name: "EmitEachSelector"
+        id: "MissingStringInputLengthValidation"
+        severity: "DANGER"
+        message: "This string is missing required length trait"
+        configuration: {
+            selector: """
+operation -[input]-> structure > member
+    :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]))
+"""
+        }
+    }
+]
+
+namespace smithy.example
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
new file mode 100644
index 000000000..8fa2be8e6
--- /dev/null
+++ b/smithy-syntax/src/test/resources/software/amazon/smithy/syntax/formatter/trait-textblock.formatted.smithy
@@ -0,0 +1,26 @@
+$version: "2.0"
+
+namespace smithy.example
+
+@documentation(
+    """
+    This is the documentation for Foo.
+    Lorem ipsum dolor."""
+)
+string Foo
+
+@documentation(
+    """
+    This is the documentation for Bar.
+    Lorem ipsum dolor.
+    """
+)
+string Bar
+
+@documentation(
+    """
+    This is the documentation for Baz.
+    Lorem ipsum dolor.
+    """
+)
+string Baz
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 f586af1aa..99efba0d8 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
@@ -16,3 +16,11 @@ string Foo
     """
 )
 string Bar
+
+@documentation(
+"""
+This is the documentation for Baz.
+Lorem ipsum dolor.
+"""
+)
+string Baz