Repository: prettier/prettier (JavaScript)
Date: 2020-05-24 10:27:05+00:00
Task type: Feature

Issue:
Plugins don't add choices to the `parser` option
Plugins typically add parsers to be specified via the `parser` option, however there is no way for them to have their parsers listed as valid values of this option (in `prettier.getSupportedInfo().options`), which makes it difficult for editor integrations and other code to discover available parsers. It's still possible to find them in `prettier.getSupportedInfo().languages`, but it would make sense if Prettier did that itself and added choices to the option automatically.

**Steps to reproduce:**

1. `yarn add prettier @prettier/plugin-php`
2. `./node_modules/.bin/prettier --help` or `./node_modules/.bin/prettier --support-info`

**Expected behavior:**

The `parser` option should list `php` as a valid value.

**Actual behavior:**

It doesn't.


Issue discussion:

Solution patch:
diff --git a/changelog_unreleased/api/pr-8390.md b/changelog_unreleased/api/pr-8390.md
new file mode 100644
index 000000000000..fead47a132a9
--- /dev/null
+++ b/changelog_unreleased/api/pr-8390.md
@@ -0,0 +1,16 @@
+#### Add plugins' parsers to the `parser` option ([#8390](https://github.com/prettier/prettier/pull/8390) by [@thorn0](https://github.com/thorn0))
+
+When a plugin defines a language, the parsers specified for this language are now automatically added to the list of valid values of the `parser` option.
+This might be useful for editor integrations or other applications that need a list of available parsers.
+
+```console
+> yarn add prettier @prettier/plugin-php
+```
+
+```js
+const hasPhpParser = prettier
+  .getSupportInfo()
+  .options.find((option) => option.name === "parser")
+  .choices.map((choice) => choice.value)
+  .includes("php"); // false in Prettier stable, true in Prettier master
+```
diff --git a/src/main/support.js b/src/main/support.js
index f1d0e54d26a7..8bff0cf03d69 100644
--- a/src/main/support.js
+++ b/src/main/support.js
@@ -29,6 +29,10 @@ function getSupportInfo({
   // we need to treat it as the normal one so as to test new features.
   const version = currentVersion.split("-", 1)[0];
 
+  const languages = plugins
+    .reduce((all, plugin) => all.concat(plugin.languages || []), [])
+    .filter(filterSince);
+
   const options = arrayify(
     Object.assign({}, ...plugins.map(({ options }) => options), coreOptions),
     "name"
@@ -54,26 +58,26 @@ function getSupportInfo({
         option.choices = option.choices.filter(
           (option) => filterSince(option) && filterDeprecated(option)
         );
-      }
 
-      const filteredPlugins = plugins.filter(
-        (plugin) =>
-          plugin.defaultOptions &&
-          plugin.defaultOptions[option.name] !== undefined
-      );
+        if (option.name === "parser") {
+          collectParsersFromLanguages(option, languages, plugins);
+        }
+      }
 
-      const pluginDefaults = filteredPlugins.reduce((reduced, plugin) => {
-        reduced[plugin.name] = plugin.defaultOptions[option.name];
-        return reduced;
-      }, {});
+      const pluginDefaults = plugins
+        .filter(
+          (plugin) =>
+            plugin.defaultOptions &&
+            plugin.defaultOptions[option.name] !== undefined
+        )
+        .reduce((reduced, plugin) => {
+          reduced[plugin.name] = plugin.defaultOptions[option.name];
+          return reduced;
+        }, {});
 
       return { ...option, pluginDefaults };
     });
 
-  const languages = plugins
-    .reduce((all, plugin) => all.concat(plugin.languages || []), [])
-    .filter(filterSince);
-
   return { languages, options };
 
   function filterSince(object) {
@@ -101,6 +105,27 @@ function getSupportInfo({
   }
 }
 
+function collectParsersFromLanguages(option, languages, plugins) {
+  const existingValues = new Set(option.choices.map((choice) => choice.value));
+  for (const language of languages) {
+    if (language.parsers) {
+      for (const value of language.parsers) {
+        if (!existingValues.has(value)) {
+          existingValues.add(value);
+          const plugin = plugins.find(
+            (plugin) => plugin.parsers && plugin.parsers[value]
+          );
+          let description = language.name;
+          if (plugin && plugin.name) {
+            description += ` (plugin: ${plugin.name})`;
+          }
+          option.choices.push({ value, description });
+        }
+      }
+    }
+  }
+}
+
 module.exports = {
   getSupportInfo,
 };


Test patch:
diff --git a/tests_integration/__tests__/__snapshots__/plugin-options-string.js.snap b/tests_integration/__tests__/__snapshots__/plugin-options-string.js.snap
index cf0d60aa19a5..656238849fa9 100644
--- a/tests_integration/__tests__/__snapshots__/plugin-options-string.js.snap
+++ b/tests_integration/__tests__/__snapshots__/plugin-options-string.js.snap
@@ -5,7 +5,7 @@ exports[` 1`] = `
 - First value
 + Second value
 
-@@ -20,10 +20,12 @@
+@@ -20,18 +20,20 @@
                              Control how Prettier formats quoted code embedded in the file.
                              Defaults to auto.
     --end-of-line <lf|crlf|cr|auto>
@@ -17,7 +17,16 @@ exports[` 1`] = `
                              How to handle whitespaces in HTML.
                              Defaults to css.
     --jsx-bracket-same-line  Put > on the last line instead of at a new line.
-                             Defaults to false."
+                             Defaults to false.
+    --jsx-single-quote       Use single quotes in JSX.
+                             Defaults to false.
+-   --parser <flow|babel|babel-flow|babel-ts|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|html|angular|lwc>
++   --parser <flow|babel|babel-flow|babel-ts|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|html|angular|lwc|foo-parser>
+                             Which parser to use.
+    --print-width <int>      The line length where Prettier will try wrap.
+                             Defaults to 80.
+    --prose-wrap <always|never|preserve>
+                             How to wrap prose."
 `;
 
 exports[`show detailed external option with \`--help foo-string\` (stderr) 1`] = `""`;
diff --git a/tests_integration/__tests__/__snapshots__/plugin-options.js.snap b/tests_integration/__tests__/__snapshots__/plugin-options.js.snap
index 9176e067a23f..4160962f199a 100644
--- a/tests_integration/__tests__/__snapshots__/plugin-options.js.snap
+++ b/tests_integration/__tests__/__snapshots__/plugin-options.js.snap
@@ -5,7 +5,7 @@ exports[` 1`] = `
 - First value
 + Second value
 
-@@ -20,10 +20,12 @@
+@@ -20,18 +20,20 @@
                              Control how Prettier formats quoted code embedded in the file.
                              Defaults to auto.
     --end-of-line <lf|crlf|cr|auto>
@@ -17,9 +17,52 @@ exports[` 1`] = `
                              How to handle whitespaces in HTML.
                              Defaults to css.
     --jsx-bracket-same-line  Put > on the last line instead of at a new line.
-                             Defaults to false."
+                             Defaults to false.
+    --jsx-single-quote       Use single quotes in JSX.
+                             Defaults to false.
+-   --parser <flow|babel|babel-flow|babel-ts|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|html|angular|lwc>
++   --parser <flow|babel|babel-flow|babel-ts|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|html|angular|lwc|foo-parser>
+                             Which parser to use.
+    --print-width <int>      The line length where Prettier will try wrap.
+                             Defaults to 80.
+    --prose-wrap <always|never|preserve>
+                             How to wrap prose."
 `;
 
+exports[`include plugin's parsers to the values of the \`parser\` option\` (stderr) 1`] = `""`;
+
+exports[`include plugin's parsers to the values of the \`parser\` option\` (stdout) 1`] = `
+"--parser <flow|babel|babel-flow|babel-ts|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|mdx|vue|yaml|html|angular|lwc|foo-parser>
+
+  Which parser to use.
+
+Valid options:
+
+  flow             Flow
+  babel            JavaScript
+  babel-flow       Flow
+  babel-ts         TypeScript
+  typescript       TypeScript
+  css              CSS
+  less             Less
+  scss             SCSS
+  json             JSON
+  json5            JSON5
+  json-stringify   JSON.stringify
+  graphql          GraphQL
+  markdown         Markdown
+  mdx              MDX
+  vue              Vue
+  yaml             YAML
+  html             HTML
+  angular          Angular
+  lwc              Lightning Web Components
+  foo-parser       foo (plugin: ./plugin)
+"
+`;
+
+exports[`include plugin's parsers to the values of the \`parser\` option\` (write) 1`] = `Array []`;
+
 exports[`show detailed external option with \`--help foo-option\` (stderr) 1`] = `""`;
 
 exports[`show detailed external option with \`--help foo-option\` (stdout) 1`] = `
diff --git a/tests_integration/__tests__/plugin-options.js b/tests_integration/__tests__/plugin-options.js
index bb6ddfe9c6c4..1d47987c272d 100644
--- a/tests_integration/__tests__/plugin-options.js
+++ b/tests_integration/__tests__/plugin-options.js
@@ -22,6 +22,16 @@ describe("show detailed external option with `--help foo-option`", () => {
   });
 });
 
+describe("include plugin's parsers to the values of the `parser` option`", () => {
+  runPrettier("plugins/options", [
+    "--plugin=./plugin",
+    "--help",
+    "parser",
+  ]).test({
+    status: 0,
+  });
+});
+
 describe("external options from CLI should work", () => {
   runPrettier(
     "plugins/options",