Repository: prettier/prettier (JavaScript)
Date: 2020-11-17 21:37:40+00:00
Task type: Bug Fix

Issue:
Plugin cannot print trailing comments without newline at end of file
<!--

BEFORE SUBMITTING AN ISSUE:

1.  Search for your issue on GitHub: https://github.com/prettier/prettier/issues
    A large number of opened issues are duplicates of existing issues.
    If someone has already opened an issue for what you are experiencing,
    you do not need to open a new issue — please add a 👍 reaction to the
    existing issue instead.

2.  If your issue is with a prettier editor extension or add-on, please open the
    issue in the repo for that extension or add-on, instead of this repo.

-->

**Environments:**

- Prettier Version: 2.1.2
- Running Prettier via: CLI
- Runtime: Node.js v14
- Operating System: Windows/Linux

**Steps to reproduce:**

I am the maintainer for [prettier-plugin-apex](https://github.com/dangmai/prettier-plugin-apex), which has an option to turn off the empty newline at the end of the formatted file (this is due to Salesforce's weird handling of empty lines). Recently a user [reports](https://github.com/dangmai/prettier-plugin-apex/issues/301) that when he uses that option, trailing comments are not being included in the formatted result.

I look into Prettier's core code and I've found that trailing comments are only flushed in the output if there is a new line somewhere after the comment, because a trailing comment uses `line-suffix`, and contents within line-suffix are only flushed after a new line, see [here](https://github.com/prettier/prettier/blob/31e7c1d04e00a5b1f9c20ba6244d3cd7d6f6506b/src/document/doc-printer.js#L505).

I know most Prettier languages do not support removing the trailing empty lines, but I do think some language plugins do need this depending on the language ecosystem they're supporting. Here are a couple of options that can address this issue:

- Prettier core can add a doc type that flushes line-suffix contents, similar to `line`, `softline` or `hardline`.
- Alternatively, Prettier can add a `postprocess` for plugin printers, which allows plugins to manipulate the formatted result. If this is an option, my plugin can always add a trailing empty line to the returned Doc, and remove it as part of post-processing phase based on options.

I know that in my plugin, I can implement `willPrintOwnComments` to work around this issue and make sure my comments are not in a `line-suffix`, however it's a very lengthy hack which IMO should be handled upstream. There may be other options that I haven't thought of as well, and I'm open to suggestions if anyone has any.


Issue discussion:
You can try adding the [`lineSuffixBoundary`](https://github.com/prettier/prettier/blob/master/commands.md#linesuffixboundary) command to the end of your doc.
Hey @thorn0 thanks for chiming in, it is actually one of the things I tried, but as you can see [here](https://github.com/prettier/prettier/blob/31e7c1d04e00a5b1f9c20ba6244d3cd7d6f6506b/src/document/doc-printer.js#L477), lineSuffixBoundary automatically adds a `hardline` to the end of the Doc, which ends up being the trailing empty line to the document (which is what I'm trying to avoid).
I think what's needed is an extra check after the main loop in the doc printer that would flush the `lineSuffix` array if it's not empty. I'll happily review if you do a PR.
That's a good point, I'll open a PR for it :)

Solution patch:
diff --git a/src/document/doc-printer.js b/src/document/doc-printer.js
index 1886571aa30e..541063c4927f 100644
--- a/src/document/doc-printer.js
+++ b/src/document/doc-printer.js
@@ -526,6 +526,13 @@ function printDocToString(doc, options) {
         default:
       }
     }
+
+    // Flush remaining line-suffix contents at the end of the document, in case
+    // there is no new line after the line-suffix.
+    if (cmds.length === 0 && lineSuffix.length) {
+      cmds.push(...lineSuffix.reverse());
+      lineSuffix = [];
+    }
   }
 
   const cursorPlaceholderIndex = out.indexOf(cursor.placeholder);


Test patch:
diff --git a/tests_integration/__tests__/plugin-flush-line-suffix.js b/tests_integration/__tests__/plugin-flush-line-suffix.js
new file mode 100644
index 000000000000..e60ab5adb25c
--- /dev/null
+++ b/tests_integration/__tests__/plugin-flush-line-suffix.js
@@ -0,0 +1,14 @@
+"use strict";
+
+const runPrettier = require("../runPrettier");
+
+describe("flush all line-suffix content", () => {
+  runPrettier("plugins/flushLineSuffix", ["*.foo", "--plugin=./plugin"], {
+    ignoreLineEndings: true,
+  }).test({
+    stdout: "contents",
+    stderr: "",
+    status: 0,
+    write: [],
+  });
+});
diff --git a/tests_integration/plugins/flushLineSuffix/file.foo b/tests_integration/plugins/flushLineSuffix/file.foo
new file mode 100644
index 000000000000..12f00e90b6ef
--- /dev/null
+++ b/tests_integration/plugins/flushLineSuffix/file.foo
@@ -0,0 +1,1 @@
+contents
diff --git a/tests_integration/plugins/flushLineSuffix/plugin.js b/tests_integration/plugins/flushLineSuffix/plugin.js
new file mode 100644
index 000000000000..4690948ab2c4
--- /dev/null
+++ b/tests_integration/plugins/flushLineSuffix/plugin.js
@@ -0,0 +1,25 @@
+"use strict";
+
+const prettier = require("prettier-local");
+const { lineSuffix } = prettier.doc.builders;
+
+module.exports = {
+  languages: [
+    {
+      name: "foo",
+      parsers: ["foo-parser"],
+      extensions: [".foo"]
+    }
+  ],
+  parsers: {
+    "foo-parser": {
+      parse: text => ({ text }),
+      astFormat: "foo-ast"
+    }
+  },
+  printers: {
+    "foo-ast": {
+      print: path => lineSuffix(path.getValue().text.trim())
+    }
+  }
+};