Repository: apollographql/apollo-rs
Pull request #724 (2023-11-06 11:40:07)

Serialize multi-line strings as block strings 
Fixes https://github.com/apollographql/apollo-rs/issues/664

SimonSapin: Should we also prefer block strings for single-line descriptions? (As opposed to string `Value`s)
SimonSapin: It looks like graphql-js:

* [Always prefers block strings for descriptions](https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/utilities/printSchema.ts#L306)
* [`StringValue` objects can contain a `isBLockString` boolean flag](https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/language/printer.ts#L144-L147)… but [nothing ever sets it](https://github.com/search?q=repo%3Agraphql%2Fgraphql-js%20isBlockString&type=code)?
* Block string printing has [some heuristics](https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/language/blockString.ts#L146-L153) to decide whether to add leading/trailing newlines

@IvanGoncharov did I miss something?
goto-bus-stop: Did the new pushes change something or is it just a rebase?
SimonSapin: Rebase + removing the obsolete comment
IvanGoncharov: > [StringValue objects can contain a isBLockString boolean flag](https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/language/printer.ts#L144-L147)… but [nothing ever sets it](https://github.com/search?q=repo%3Agraphql%2Fgraphql-js%20isBlockString&type=code)?

@SimonSapin The flag called, `block` is `isBLockString` just an alias in this particular destruction.
It is set in parser:
https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/language/parser.ts#L713
This allows graphql-js to do round-trip where block string is parsed and when printed as block string.

> [Always prefers block strings for descriptions](https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/utilities/printSchema.ts#L306)

Yes, note that not every string can be printed as block string.

> Block string printing has [some heuristics](https://github.com/graphql/graphql-js/blob/0b7590f0a2b65e6210da2e49be0d8e6c27781af2/src/language/blockString.ts#L146-L153) to decide whether to add leading/trailing newlines

Yes, in graphql-js we had a bug where by printing string as block string we were loosing some characters so I spend bunch of time figuring out different heuristics that preserved all characters.
SimonSapin: New commit to make stylistic choices closer to graphql-js:
* Prefer block strings for descriptions
* For short single-line values serialize block strings as `"""example"""`, except for trailing characters that would make it incorrect

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index d0be3b1..15e03e5 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -108,10 +108,26 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
   [pull/752]:**
   when mutating isn’t needed after parsing,
   this returns an immutable `Valid<_>` value in one step.
+- **Serialize multi-line strings as block strings- [SimonSapin], [pull/724]**
+  Example before:
+  ```graphql
+  "Example\n\nDescription description description"
+  schema { query: MyQuery }
+  ```
+  After:
+  ```graphql
+  """
+  Example
+  
+  Description description description
+  """
+  schema { query: MyQuery }
+  ```
 
 [SimonSapin]: https://github.com/SimonSapin
 [issue/709]: https://github.com/apollographql/apollo-rs/issues/709
 [issue/751]: https://github.com/apollographql/apollo-rs/issues/751
+[pull/724]: https://github.com/apollographql/apollo-rs/pull/724
 [pull/752]: https://github.com/apollographql/apollo-rs/pull/752
 
 
diff --git a/crates/apollo-compiler/src/ast/serialize.rs b/crates/apollo-compiler/src/ast/serialize.rs
index 6900713..757fd0d 100644
--- a/crates/apollo-compiler/src/ast/serialize.rs
+++ b/crates/apollo-compiler/src/ast/serialize.rs
@@ -12,7 +12,7 @@ pub struct Serialize<'a, T> {
 
 #[derive(Debug, Clone)]
 pub(crate) struct Config<'a> {
-    ident_prefix: Option<&'a str>,
+    indent_prefix: Option<&'a str>,
 }
 
 pub(crate) struct State<'config, 'fmt, 'fmt2> {
@@ -29,13 +29,13 @@ impl<'a, T> Serialize<'a, T> {
     /// `prefix` is repeated at the start of each line by the number of indentation levels.
     /// The default is `"  "`, two spaces.
     pub fn indent_prefix(mut self, prefix: &'a str) -> Self {
-        self.config.ident_prefix = Some(prefix);
+        self.config.indent_prefix = Some(prefix);
         self
     }
 
     /// Disable indentation and line breaks
     pub fn no_indent(mut self) -> Self {
-        self.config.ident_prefix = None;
+        self.config.indent_prefix = None;
         self
     }
 }
@@ -43,7 +43,7 @@ impl<'a, T> Serialize<'a, T> {
 impl Default for Config<'_> {
     fn default() -> Self {
         Self {
-            ident_prefix: Some("  "),
+            indent_prefix: Some("  "),
         }
     }
 }
@@ -89,7 +89,7 @@ impl<'config, 'fmt, 'fmt2> State<'config, 'fmt, 'fmt2> {
     }
 
     fn new_line_common(&mut self, space: bool) -> fmt::Result {
-        if let Some(prefix) = self.config.ident_prefix {
+        if let Some(prefix) = self.config.indent_prefix {
             self.write("\n")?;
             for _ in 0..self.indent_level {
                 self.write(prefix)?;
@@ -100,14 +100,27 @@ impl<'config, 'fmt, 'fmt2> State<'config, 'fmt, 'fmt2> {
         Ok(())
     }
 
+    /// Panics if newlines are disabled
+    fn require_new_line(&mut self) -> fmt::Result {
+        let prefix = self
+            .config
+            .indent_prefix
+            .expect("require_new_line called with newlines disabled");
+        self.write("\n")?;
+        for _ in 0..self.indent_level {
+            self.write(prefix)?;
+        }
+        Ok(())
+    }
+
     pub(crate) fn newlines_enabled(&self) -> bool {
-        self.config.ident_prefix.is_some()
+        self.config.indent_prefix.is_some()
     }
 
     pub(crate) fn on_single_line<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
-        let indent_prefix = self.config.ident_prefix.take();
+        let indent_prefix = self.config.indent_prefix.take();
         let result = f(self);
-        self.config.ident_prefix = indent_prefix;
+        self.config.indent_prefix = indent_prefix;
         result
     }
 }
@@ -702,7 +715,10 @@ impl Value {
             Value::Boolean(true) => state.write("true"),
             Value::Boolean(false) => state.write("false"),
             Value::Enum(name) => state.write(name),
-            Value::String(value) => serialize_string_value(state, value),
+            Value::String(value) => {
+                let is_description = false;
+                serialize_string_value(state, is_description, value)
+            }
             Value::Variable(name) => display!(state, "${}", name),
             Value::Float(value) => display!(state, value),
             Value::Int(value) => display!(state, value),
@@ -800,12 +816,12 @@ pub(crate) fn curly_brackets_space_separated<T>(
     state.write("}")
 }
 
-fn serialize_string_value(state: &mut State, mut str: &str) -> fmt::Result {
-    // TODO: use block string when it would be possible AND would look nicer
-    // The parsed value of a block string cannot have:
-    // * non-\n control characters (not representable)
-    // * leading/trailing empty lines (would be stripped when reparsing)
-    // * common indent (would be stripped when reparsing)
+fn serialize_string_value(state: &mut State, is_description: bool, mut str: &str) -> fmt::Result {
+    let contains_newline = str.contains('\n');
+    let prefer_block_string = is_description || contains_newline;
+    if state.newlines_enabled() && prefer_block_string && can_be_block_string(str) {
+        return serialize_block_string(state, contains_newline, str);
+    }
     state.write("\"")?;
     loop {
         if let Some(i) = str.find(|c| (c < ' ' && c != '\t') || c == '"' || c == '\\') {
@@ -831,9 +847,100 @@ fn serialize_string_value(state: &mut State, mut str: &str) -> fmt::Result {
     state.write("\"")
 }
 
+fn serialize_block_string(state: &mut State, contains_newline: bool, str: &str) -> fmt::Result {
+    const TRIPLE_QUOTE: &str = "\"\"\"";
+    const ESCAPED_TRIPLE_QUOTE: &str = "\\\"\"\"";
+    const _: () = assert!(TRIPLE_QUOTE.len() == 3);
+    const _: () = assert!(ESCAPED_TRIPLE_QUOTE.len() == 4);
+
+    fn serialize_line(state: &mut State, mut line: &str) -> Result<(), fmt::Error> {
+        while let Some((before, after)) = line.split_once(TRIPLE_QUOTE) {
+            state.write(before)?;
+            state.write(ESCAPED_TRIPLE_QUOTE)?;
+            line = after;
+        }
+        state.write(line)
+    }
+
+    let multi_line =
+        contains_newline || str.len() > 70 || str.ends_with('"') || str.ends_with('\\');
+
+    state.write(TRIPLE_QUOTE)?;
+    if !multi_line {
+        // """example""""
+        serialize_line(state, str)?
+    } else {
+        // """
+        // example
+        // """
+
+        // `can_be_block_string` excludes \r, so the only remaining line terminator is \n
+        for line in str.split('\n') {
+            if line.is_empty() {
+                // Skip indentation which would be trailing whitespace
+                state.write("\n")?;
+            } else {
+                state.require_new_line()?;
+                serialize_line(state, line)?;
+            }
+        }
+        state.require_new_line()?;
+    }
+    state.write(TRIPLE_QUOTE)
+}
+
+/// Is it possible to create a serialization that, when fed through
+/// [BlockStringValue](https://spec.graphql.org/October2021/#BlockStringValue()),
+/// returns exactly `value`?
+fn can_be_block_string(value: &str) -> bool {
+    // `BlockStringValue` splits its inputs at any `LineTerminator` (\n, \r\n, or \r)
+    // and eventually joins lines but always with \n. So its output can never contain \r
+    if value.contains('\r') {
+        return false;
+    }
+
+    /// <https://spec.graphql.org/October2021/#WhiteSpace>
+    fn trim_start_graphql_whitespace(value: &str) -> &str {
+        value.trim_start_matches([' ', '\t'])
+    }
+
+    // With the above, \n is the only remaining LineTerminator
+    let mut lines = value.split('\n');
+    if lines
+        .next()
+        .is_some_and(|first| trim_start_graphql_whitespace(first).is_empty())
+        || lines
+            .next_back()
+            .is_some_and(|last| trim_start_graphql_whitespace(last).is_empty())
+    {
+        // Leading or trailing whitespace-only line would be trimmed by `BlockStringValue`
+        return false;
+    }
+
+    let common_indent = {
+        let mut lines = value.split('\n');
+        // Skip the first line, the one following """
+        lines.next();
+
+        let each_line_indent_utf8_len = lines.filter_map(|line| {
+            let after_indent = trim_start_graphql_whitespace(line);
+            if !after_indent.is_empty() {
+                Some(line.len() - after_indent.len())
+            } else {
+                None // skip whitespace-only lines
+            }
+        });
+        each_line_indent_utf8_len.min().unwrap_or(0)
+    };
+    // If there is common indent `BlockStringValue` would remove it
+    // and incorrectly round-trip to a different value.
+    common_indent == 0
+}
+
 fn serialize_description(state: &mut State, description: &Option<NodeStr>) -> fmt::Result {
     if let Some(description) = description {
-        serialize_string_value(state, description)?;
+        let is_description = true;
+        serialize_string_value(state, is_description, description)?;
         state.new_line_or_space()?;
     }
     Ok(())


Test patch:
diff --git a/crates/apollo-compiler/test_data/ok/0039_string_literals.graphql b/crates/apollo-compiler/test_data/ok/0039_string_literals.graphql
new file mode 100644
index 0000000..b468bc7
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0039_string_literals.graphql
@@ -0,0 +1,51 @@
+type Query {
+  "LF: a\nb"
+  f1: Int
+
+  "CRLF: a\r\nb"
+  f2: Int
+
+  """
+    a
+
+    b
+  
+
+  """
+  f3: Int
+
+  "a \"b\" c"
+  f4: Int
+
+  """a \"""b\""" c"""
+  f5: Int
+
+  """
+  regex: \d+
+  """
+  f6: Int
+
+  "\nLeading empty line to preserve"
+  f7: Int
+
+  " \nLeading whitespace-only line to preserve"
+  f8: Int
+
+  "Trailing empty line to preserve\n"
+  f9: Int
+
+  "Trailing whitespace-only line to preserve\n\t"
+  f10: Int
+
+  f11(arg: String = "a\nb"): Int
+
+  f12(arg: String = "a \"b\" c"): Int
+
+  f13(arg: String = "regex: \\d+"): Int
+
+  "Trailing backslash \\"
+  f14: Int
+
+  "Trailing quote\""
+  f15: Int
+}
diff --git a/crates/apollo-compiler/test_data/ok/0039_string_literals.txt b/crates/apollo-compiler/test_data/ok/0039_string_literals.txt
new file mode 100644
index 0000000..5fa3b13
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0039_string_literals.txt
@@ -0,0 +1,316 @@
+Schema {
+    sources: {
+        -1: SourceFile {
+            path: "built_in.graphql",
+            source_text: include_str!("built_in.graphql"),
+        },
+        38: SourceFile {
+            path: "0039_string_literals.graphql",
+            source_text: "type Query {\n  \"LF: a\\nb\"\n  f1: Int\n\n  \"CRLF: a\\r\\nb\"\n  f2: Int\n\n  \"\"\"\n    a\n\n    b\n  \n\n  \"\"\"\n  f3: Int\n\n  \"a \\\"b\\\" c\"\n  f4: Int\n\n  \"\"\"a \\\"\"\"b\\\"\"\" c\"\"\"\n  f5: Int\n\n  \"\"\"\n  regex: \\d+\n  \"\"\"\n  f6: Int\n\n  \"\\nLeading empty line to preserve\"\n  f7: Int\n\n  \" \\nLeading whitespace-only line to preserve\"\n  f8: Int\n\n  \"Trailing empty line to preserve\\n\"\n  f9: Int\n\n  \"Trailing whitespace-only line to preserve\\n\\t\"\n  f10: Int\n\n  f11(arg: String = \"a\\nb\"): Int\n\n  f12(arg: String = \"a \\\"b\\\" c\"): Int\n\n  f13(arg: String = \"regex: \\\\d+\"): Int\n\n  \"Trailing backslash \\\\\"\n  f14: Int\n\n  \"Trailing quote\\\"\"\n  f15: Int\n}\n",
+        },
+    },
+    schema_definition: SchemaDefinition {
+        description: None,
+        directives: [],
+        query: Some(
+            ComponentName {
+                origin: Definition,
+                name: "Query",
+            },
+        ),
+        mutation: None,
+        subscription: None,
+    },
+    directive_definitions: {
+        "skip": built_in_directive!("skip"),
+        "include": built_in_directive!("include"),
+        "deprecated": built_in_directive!("deprecated"),
+        "specifiedBy": built_in_directive!("specifiedBy"),
+    },
+    types: {
+        "__Schema": built_in_type!("__Schema"),
+        "__Type": built_in_type!("__Type"),
+        "__TypeKind": built_in_type!("__TypeKind"),
+        "__Field": built_in_type!("__Field"),
+        "__InputValue": built_in_type!("__InputValue"),
+        "__EnumValue": built_in_type!("__EnumValue"),
+        "__Directive": built_in_type!("__Directive"),
+        "__DirectiveLocation": built_in_type!("__DirectiveLocation"),
+        "Int": built_in_type!("Int"),
+        "Float": built_in_type!("Float"),
+        "String": built_in_type!("String"),
+        "Boolean": built_in_type!("Boolean"),
+        "ID": built_in_type!("ID"),
+        "Query": Object(
+            0..602 @38 ObjectType {
+                description: None,
+                name: "Query",
+                implements_interfaces: {},
+                directives: [],
+                fields: {
+                    "f1": Component {
+                        origin: Definition,
+                        node: 15..35 @38 FieldDefinition {
+                            description: Some(
+                                "LF: a\nb",
+                            ),
+                            name: "f1",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f2": Component {
+                        origin: Definition,
+                        node: 39..63 @38 FieldDefinition {
+                            description: Some(
+                                "CRLF: a\r\nb",
+                            ),
+                            name: "f2",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f3": Component {
+                        origin: Definition,
+                        node: 67..103 @38 FieldDefinition {
+                            description: Some(
+                                "a\n\nb",
+                            ),
+                            name: "f3",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f4": Component {
+                        origin: Definition,
+                        node: 107..128 @38 FieldDefinition {
+                            description: Some(
+                                "a \"b\" c",
+                            ),
+                            name: "f4",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f5": Component {
+                        origin: Definition,
+                        node: 132..161 @38 FieldDefinition {
+                            description: Some(
+                                "a \"\"\"b\"\"\" c",
+                            ),
+                            name: "f5",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f6": Component {
+                        origin: Definition,
+                        node: 165..197 @38 FieldDefinition {
+                            description: Some(
+                                "regex: \\d+",
+                            ),
+                            name: "f6",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f7": Component {
+                        origin: Definition,
+                        node: 201..245 @38 FieldDefinition {
+                            description: Some(
+                                "\nLeading empty line to preserve",
+                            ),
+                            name: "f7",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f8": Component {
+                        origin: Definition,
+                        node: 249..304 @38 FieldDefinition {
+                            description: Some(
+                                " \nLeading whitespace-only line to preserve",
+                            ),
+                            name: "f8",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f9": Component {
+                        origin: Definition,
+                        node: 308..353 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing empty line to preserve\n",
+                            ),
+                            name: "f9",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f10": Component {
+                        origin: Definition,
+                        node: 357..415 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing whitespace-only line to preserve\n\t",
+                            ),
+                            name: "f10",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f11": Component {
+                        origin: Definition,
+                        node: 419..449 @38 FieldDefinition {
+                            description: None,
+                            name: "f11",
+                            arguments: [
+                                423..443 @38 InputValueDefinition {
+                                    description: None,
+                                    name: "arg",
+                                    ty: 428..434 @38 Named(
+                                        "String",
+                                    ),
+                                    default_value: Some(
+                                        437..443 @38 String(
+                                            "a\nb",
+                                        ),
+                                    ),
+                                    directives: [],
+                                },
+                            ],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f12": Component {
+                        origin: Definition,
+                        node: 453..488 @38 FieldDefinition {
+                            description: None,
+                            name: "f12",
+                            arguments: [
+                                457..482 @38 InputValueDefinition {
+                                    description: None,
+                                    name: "arg",
+                                    ty: 462..468 @38 Named(
+                                        "String",
+                                    ),
+                                    default_value: Some(
+                                        471..482 @38 String(
+                                            "a \"b\" c",
+                                        ),
+                                    ),
+                                    directives: [],
+                                },
+                            ],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f13": Component {
+                        origin: Definition,
+                        node: 492..529 @38 FieldDefinition {
+                            description: None,
+                            name: "f13",
+                            arguments: [
+                                496..523 @38 InputValueDefinition {
+                                    description: None,
+                                    name: "arg",
+                                    ty: 501..507 @38 Named(
+                                        "String",
+                                    ),
+                                    default_value: Some(
+                                        510..523 @38 String(
+                                            "regex: \\d+",
+                                        ),
+                                    ),
+                                    directives: [],
+                                },
+                            ],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f14": Component {
+                        origin: Definition,
+                        node: 533..567 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing backslash \\",
+                            ),
+                            name: "f14",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f15": Component {
+                        origin: Definition,
+                        node: 571..600 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing quote\"",
+                            ),
+                            name: "f15",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                },
+            },
+        ),
+    },
+}
+ExecutableDocument {
+    sources: {
+        -1: SourceFile {
+            path: "built_in.graphql",
+            source_text: include_str!("built_in.graphql"),
+        },
+        38: SourceFile {
+            path: "0039_string_literals.graphql",
+            source_text: "type Query {\n  \"LF: a\\nb\"\n  f1: Int\n\n  \"CRLF: a\\r\\nb\"\n  f2: Int\n\n  \"\"\"\n    a\n\n    b\n  \n\n  \"\"\"\n  f3: Int\n\n  \"a \\\"b\\\" c\"\n  f4: Int\n\n  \"\"\"a \\\"\"\"b\\\"\"\" c\"\"\"\n  f5: Int\n\n  \"\"\"\n  regex: \\d+\n  \"\"\"\n  f6: Int\n\n  \"\\nLeading empty line to preserve\"\n  f7: Int\n\n  \" \\nLeading whitespace-only line to preserve\"\n  f8: Int\n\n  \"Trailing empty line to preserve\\n\"\n  f9: Int\n\n  \"Trailing whitespace-only line to preserve\\n\\t\"\n  f10: Int\n\n  f11(arg: String = \"a\\nb\"): Int\n\n  f12(arg: String = \"a \\\"b\\\" c\"): Int\n\n  f13(arg: String = \"regex: \\\\d+\"): Int\n\n  \"Trailing backslash \\\\\"\n  f14: Int\n\n  \"Trailing quote\\\"\"\n  f15: Int\n}\n",
+        },
+    },
+    anonymous_operation: None,
+    named_operations: {},
+    fragments: {},
+}
diff --git a/crates/apollo-compiler/test_data/serializer/ok/0039_string_literals.graphql b/crates/apollo-compiler/test_data/serializer/ok/0039_string_literals.graphql
new file mode 100644
index 0000000..fcfa92b
--- /dev/null
+++ b/crates/apollo-compiler/test_data/serializer/ok/0039_string_literals.graphql
@@ -0,0 +1,40 @@
+type Query {
+  """
+  LF: a
+  b
+  """
+  f1: Int
+  "CRLF: a\r\nb"
+  f2: Int
+  """
+  a
+
+  b
+  """
+  f3: Int
+  """a "b" c"""
+  f4: Int
+  """a \"""b\""" c"""
+  f5: Int
+  """regex: \d+"""
+  f6: Int
+  "\nLeading empty line to preserve"
+  f7: Int
+  " \nLeading whitespace-only line to preserve"
+  f8: Int
+  "Trailing empty line to preserve\n"
+  f9: Int
+  "Trailing whitespace-only line to preserve\n	"
+  f10: Int
+  f11(arg: String = "a\nb"): Int
+  f12(arg: String = "a \"b\" c"): Int
+  f13(arg: String = "regex: \\d+"): Int
+  """
+  Trailing backslash \
+  """
+  f14: Int
+  """
+  Trailing quote"
+  """
+  f15: Int
+}