Repository: prettier/plugin-ruby (javascript)

Issue:
Prettier doesn't format multiline %s(...) strings properly
## Metadata

Note that this issue doesn't happen with `%(...)`; only with `%s(...)`.

- Ruby version: 3..0.0
- `@prettier/plugin-ruby`: 1.5.5
- Options:
  - [x] `rubyHashLabel`
  - [x] `rubyModifier`
  - [x] `rubySingleQuote`
  - [ ] `rubyToProc`
  - [x] `trailingComma`

## Input

```ruby      
logger.warn %s(
  This is a multiline string
  that I want to print out to the console 
)

logger.warn(%s[
  This is a multiline string
  that I want to print out to the console 
])
```

## Current output

```ruby
logger.warn :s
  This is a multiline string
  that I want to print out to the console
s
logger.warn(
  :s
  This is a multiline string
  that I want to print out to the console
s,
)
# Syntax error on everything below this
```

## Expected output

```ruby
logger.warn "
  This is a multiline string
  that I want to print out to the console
"
logger.warn("
  This is a multiline string 
  that I want to print out to the console
")
```



Discussion:
Ah, it looks like this is our codebase's mistake--`%s(...)` creates a symbol, not a string, and it's not really standard to create a multi-line symbol. I'll close this out for now. 

https://ruby-doc.org/core-2.3.0/doc/syntax/literals_rdoc.html#label-Percent+Strings
Thanks for reporting! This is actually a bug in prettier though, even if your codebase has a bug. I'll get this fixed.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
 ### Added
 
 - [#859](https://github.com/prettier/plugin-ruby/issues/859) - azz, kddeisz - Support the `--insert-pragma` option for the incremental adoption workflow.
+- [#904](https://github.com/prettier/plugin-ruby/issues/904) - Hansenq, kddeisz - Support the `%s` symbol literal syntax.
 
 ### Changed
 
diff --git a/src/ruby/nodes/strings.js b/src/ruby/nodes/strings.js
--- a/src/ruby/nodes/strings.js
+++ b/src/ruby/nodes/strings.js
@@ -82,12 +82,97 @@ function printChar(path, { rubySingleQuote }, _print) {
   return concat([quote, body.slice(1), quote]);
 }
 
+function printPercentSDynaSymbol(path, opts, print) {
+  const node = path.getValue();
+  const parts = [];
+
+  // Push on the quote, which includes the opening character.
+  parts.push(node.quote);
+
+  path.each((childPath) => {
+    const childNode = childPath.getValue();
+
+    if (childNode.type !== "@tstring_content") {
+      // Here we are printing an embedded variable or expression.
+      parts.push(print(childPath));
+    } else {
+      // Here we are printing plain string content.
+      parts.push(join(literalline, childNode.body.split("\n")));
+    }
+  }, "body");
+
+  // Push on the closing character, which is the opposite of the third
+  // character from the opening.
+  parts.push(quotePairs[node.quote[2]]);
+
+  return concat(parts);
+}
+
+// We don't actually want to print %s symbols, as they're much more rarely seen
+// in the wild. But we're going to be forced into it if it's a multi-line symbol
+// or if the quoting would get super complicated.
+function shouldPrintPercentSDynaSymbol(node) {
+  // We shouldn't print a %s dyna symbol if it was not already that way in the
+  // original source.
+  if (node.quote[0] !== "%") {
+    return false;
+  }
+
+  // Here we're going to check if there is a closing character, a new line, or a
+  // quote in the content of the dyna symbol. If there is, then quoting could
+  // get weird, so just bail out and stick to the original bounds in the source.
+  const closing = quotePairs[node.quote[2]];
+
+  return node.body.some(
+    (child) =>
+      child.type === "@tstring_content" &&
+      (child.body.includes("\n") ||
+        child.body.includes(closing) ||
+        child.body.includes("'") ||
+        child.body.includes('"'))
+  );
+}
+
 // Prints a dynamic symbol. Assumes there's a quote property attached to the
 // node that will tell us which quote to use when printing. We're just going to
 // use whatever quote was provided.
+//
+// In the case of a plain dyna symbol, node.quote will be either :" or :'
+// For %s dyna symbols, node.quote will be %s[, %s(, %s{, or %s<
 function printDynaSymbol(path, opts, print) {
   const node = path.getValue();
-  const parts = [node.quote].concat(path.map(print, "body")).concat(node.quote);
+
+  if (shouldPrintPercentSDynaSymbol(node)) {
+    return printPercentSDynaSymbol(path, opts, print);
+  }
+
+  const parts = [];
+  let quote;
+
+  if (isQuoteLocked(node)) {
+    if (node.quote.startsWith("%")) {
+      quote = opts.rubySingleQuote ? "'" : '"';
+    } else {
+      quote = node.quote.slice(1);
+    }
+  } else {
+    quote = opts.rubySingleQuote && isSingleQuotable(node) ? "'" : '"';
+  }
+
+  parts.push(quote);
+  path.each((childPath) => {
+    const child = childPath.getValue();
+
+    if (child.type !== "@tstring_content") {
+      parts.push(print(childPath));
+    } else {
+      parts.push(
+        join(literalline, normalizeQuotes(child.body, quote).split("\n"))
+      );
+    }
+  }, "body");
+
+  parts.push(quote);
 
   // If we're inside of an assoc_new node as the key, then it will handle
   // printing the : on its own since it could change sides.
diff --git a/src/ruby/parser.rb b/src/ruby/parser.rb
--- a/src/ruby/parser.rb
+++ b/src/ruby/parser.rb
@@ -1092,7 +1092,7 @@ def on_dyna_symbol(string)
 
       beging.merge(
         type: :dyna_symbol,
-        quote: beging[:body][1],
+        quote: beging[:body],
         body: string[:body],
         el: ending[:el],
         ec: ending[:ec]
diff --git a/src/utils/skipAssignIndent.js b/src/utils/skipAssignIndent.js
--- a/src/utils/skipAssignIndent.js
+++ b/src/utils/skipAssignIndent.js
@@ -1,4 +1,11 @@
-const skippable = ["array", "hash", "heredoc", "lambda", "regexp_literal"];
+const skippable = [
+  "array",
+  "dyna_symbol",
+  "hash",
+  "heredoc",
+  "lambda",
+  "regexp_literal"
+];
 
 function skipAssignIndent(node) {
   return (


Test patch:
diff --git a/test/js/ruby/nodes/strings.test.js b/test/js/ruby/nodes/strings.test.js
--- a/test/js/ruby/nodes/strings.test.js
+++ b/test/js/ruby/nodes/strings.test.js
@@ -169,14 +169,43 @@ describe("strings", () => {
 
     test("with single quotes", () => expect(":'abc'").toMatchFormat());
 
-    test("with double quotes", () => expect(`:"abc"`).toMatchFormat());
+    test("with double quotes", () => expect(`:"abc"`).toChangeFormat(":'abc'"));
 
-    test("with false interpolation and single quotes", () =>
-      expect(":'abc#{foo}abc'").toMatchFormat());
+    test("with double quotes with double quotes desired", () =>
+      expect(`:"abc"`).toMatchFormat({ rubySingleQuote: false }));
+
+    test("with false interpolation and single quotes, no rubySingleQuote", () =>
+      expect(":'abc#{foo}abc'").toMatchFormat({ rubySingleQuote: false }));
 
     test("with real interpolation and double quotes", () =>
       expect(`:"abc#{foo}abc"`).toMatchFormat());
 
+    test("with real interpolation and double quotes, rubySingleQuote", () =>
+      expect(`:"abc#{foo}abc"`).toMatchFormat({ rubySingleQuote: true }));
+
+    test("%s literal", () => expect("%s[abc]").toChangeFormat(":'abc'"));
+
+    test("%s literal with false interpolation", () =>
+      expect("%s[abc#{d}]").toChangeFormat(":'abc#{d}'"));
+
+    test("%s literal as hash key", () =>
+      expect("{ %s[abc] => d }").toChangeFormat("{ 'abc': d }"));
+
+    test("%s literal as hash key, no rubySingleQuote", () =>
+      expect("{ %s[abc] => d }").toChangeFormat(`{ "abc": d }`, {
+        rubySingleQuote: false
+      }));
+
+    test("%s literal with newlines", () => {
+      const content = ruby(`
+        a = %s[
+          a
+        ]
+      `);
+
+      return expect(content).toMatchFormat();
+    });
+
     test("gets correct quotes", () => {
       const content = "where('lint_tool_configs.plugin': plugins + %w[core])";