Repository: prettier/prettier (JavaScript)
Date: 2018-05-23 16:23:13+00:00
Task type: Feature

Issue:
Consider removing default parser / Skip processing unsupported file types
Currently when

* Prettier is invoked from the CLI, and
* The file being formatted doesn't match any of the extensions supported, and
* No `--parser` option is passed, and
* No `parser` is in a prettier config file,

then it will fall back to the babylon parser and treat the file as JavaScript. This made sense when prettier only supported JS (and then TypeScript), but now that it supports vastly different languages perhaps it is time to challenge that default.

In #2882, we discovered this can have negative repercussions when used with `--write **/*`, modifying files inside of a `.git` directory.

If we were to change the default from JavaScript to a no-op, these kind of issues would go away, and we'd also have less issues reported along the lines of "My CSS doesn't parse", when they're accidentally using the babylon parser.

It is already possible to associate other extensions with a parser in `.prettierrc`.

```json
{
  "overrides": [{
     "files": "*.mjs",
     "options": { "parser": "babylon" }
  }]
}
```

Thoughts @lydell @vjeux?


Issue discussion:
I like the idea! (It might be worth thinking about #2846 at the same time, or even re-thinking the CLI overall. It is quite messy. At the same time we shouldn't cause unnecessary backwards incompatibility. So it might also be better to not mix anything else into the thought process of this issue. I don't know.)
Couldn't this be done without a breaking change? Prettier's parsing of incompatible files generally errors anyway, I don't think a noop would hurt anyone's workflow as those files would already not be formatted properly by Prettier. I do like the idea of #2846 but it seems like it could be done after.
I consider this behavior a bug, not a feature, and by that logic, I think we should make this change without waiting for 2.0.
I think it's really important that we figure out a non-breaking way that fixes 80% of the cases as soon as possible, because _lots_ of users are confused when Prettier tries to format HTML files, which sometimes succeeds and sometimes doesn't, depending on if the HTML is valid JSX or not.

Could this work out, maybe?

If Prettier is told to format a file with an extension it does not recognize and the `parser` option has not been set (either in the CLI or in a config file), print a warning for that file (`Skipping file with unknown extension .html: /home/lydell/foo/src/index.html`).

We have to be really careful so we don't break editor integrations that rely on the CLI such as [vim-prettier](https://github.com/prettier/vim-prettier) (ping @mitermayer) though.

I also think we can consider this a bug and not wait for 2.0.
@lydell could this be implemented as built-in an HTML plugin, which simply does not do anything? I.e. it'll parse the entire text into a syntax tree with a single node and simply print it back as is.
@kachkaev That's a really clever solution if my proposal doesn't work out :+1: 
Making `.html` a special case behaviour sounds a bit convoluted from the implementation perspective and raises questions like _Can I also add `.py` or `.php` as other special cases when corresponding plugins are not loaded?_ Prettier attempts to apply babel parser to these now too, which makes it reasonable to suggest that if we say `Skipping file with unknown extension .html`, the same should apply to other unknown extensions. And that's v2.

A dummy plugin for HTML files will require minimal changes to the architecture of Prettier and will be a right step towards properly implementing it one day. It will also save people from the confusion you are mentioning.
I didn't mean that we should special-case HTML. This would be the warning message template: `` `Skipping file with unknown extension .${extension}: ${filepath}.` ``
Where would that message go? Stderr?

Will `.py` and `.php` files be skipped with the same message (unless corresponding plugins are installed)? If so, that sounds like a v2 behaviour to me, which is right, but breaking.
> Stderr?

Most likely, yes.

> Will .py and .php files be skipped with the same message (unless corresponding plugins are installed)?

That was my idea, yes.If so, that sounds like a v2 behaviour to me, which is right, but breaking.

> If so, that sounds like a v2 behaviour to me, which is right, but breaking.

Why?
> Why?

Because currently Prettier swallows files with all extensions and if a parser cannot be determined, it uses a fallback babel parser. That's not ideal, yes, but that's how things are at the moment. Downstream projects like editor extensions can be using this 'feature' in ways we don't know, so if we want to change the behaviour, it can be only done in Prettier 2.0 (which would allow for _breaking_ changes). Because Prettier adheres semantic versioning and this gives certain promises to the community, there are limitations to what can be changed within a single major version (currently v1).
> Downstream projects like editor extensions can be using this 'feature' in ways we don't know

So let's ask them.

> if we want to change the behaviour, it can be only done in Prettier 2.0

Then we're doomed :) Or we have to make a really boring 2.0 with only this change.
Fwiw, prettier doesn’t really adhere to semantic versioning, at every release, including patch ones, the output of some code will change, so it’s a breaking change as you’ll need to run it through your codebase to get you unblocked.
@vjeux if so, I'm totally happy with getting rid of the default parser skip formatting unless `inferParser()` returned a string for a file name or `--parser` is explicitly given.

> So let's ask them.

@lydell not sure there exists a list of all developers who are using Prettier as part of their tools 😅 
> not sure there exists a list of all developers who are using Prettier as part of their tools

Let's just ask the most popular ones.

- vim-prettier: @mitermayer 
- vim neoformat and ale: unclear who to ask (https://github.com/sbdchd/neoformat/blob/d74189db83ba6b9d50e219586e7235b7efb1d1ee/autoload/neoformat/formatters/javascript.vim#L39-L45, https://github.com/w0rp/ale/blob/3331f6c8f4a1a8ffff90ec1a2faea36eff55fe7c/autoload/ale/fixers/prettier.vim)
- prettier-emacs: @rcoedo
- WebStorm: @prigara (made several setup docs PRs)
- Visual Studio: @madskristensen

The question is: If `prettier file/with/unknown.extension` printed a warning to stderr instead of trying to format `file/with/unknown.extension` as JS, would that cause problems for editor integrations?
It wouldn't be a problem for _prettier-emacs_. The users could conditionally add specific `--parser` options for custom or unsupported extensions.
Hi! I'm the developer who wrote most of the code for the [WebStorm integration](https://github.com/JetBrains/intellij-plugins/tree/master/prettierJS).
It would not be a problem for WebStorm, we use getSupportInfo to determine if the file is acceptable.
Thank you so much for giving the heads up! In the future, feel free to @ - mention me also.
@undeadcat thanks so much for building it, it’s been a highly requested from people using prettier!
Hi @lydell,

This is not a problem for `vim-prettier` as we always include the `--parser` option. I am currently on the process of rewritting most of it for 1.0 release so changes like that should be ok since we will do a major release with other breaking changes.

cc @docwhat
FTR I wrote https://github.com/duailibe/atom-miniprettier and it doesn't try to infer a parser at all, defers that completely to Prettier. It's been working great so far! :)

With this behavior, I won't have to change anything since that's exactly the behavior I want 😄 
@duailibe so will your plugin try to prettify any file extension using babel parser as a fallback now? We try to avoid this with @robwise in https://github.com/prettier/prettier/pull/4341 to make prettier-atom lighter (https://github.com/prettier/prettier-atom/pull/404).
@kachkaev for now, yes. But since I don't use autosave, that's not an actual problem for me.

I'm just commenting on that thread right now. :)
I opened a PR that implements this change, I need some help implementing the error message that @lydell mentioned in https://github.com/prettier/prettier/issues/2884#issuecomment-385642962, but otherwise it is functional
In the same vein, @mitermayer @rocedo @undeadcat @madskristensen we are thinking about changing the JavaScript API to require passing either `parser` or `filepath`, otherwise the text will be passed through as-is.
```js
> prettier.format("function bla      () {}")
'function bla      () {}'
> prettier.format("function bla      () {}", { filepath: "foo.js" })
'function bla() {}\n'
> prettier.format("function bla      () {}", { parser: "babylon" })
'function bla() {}\n'
```
This will affect `prettier.format`, `prettier.formatWithCursor`, and `prettier.check`.

Would this adversely affect you? Are you always specifying either parser or filepath?

Context: https://github.com/prettier/prettier/pull/4426#discussion_r186271892
cc @rcoedo since your username wasn’t highlighted in the above comment.
Thanks @j-f1 
We don't use the JS API in the emacs package 👍 
The WebStorm plugin always passes `filepath`.
cc @robwise
prettier-atom does not always pass filepath (in cases where the user has chosen to manually format text in a draft document that has not yet been saved as a file). Currently, we specify the parser, but that's about to change with #4341 where we will rely on Prettier to tell us what parser to use.
In `prettier-eslint` we let prettier decide what parser to use, however consumers of `prettier-eslint`'s API is free to provide `parser` if they want.
@robwise Do you specify the parser for unsaved draft documents also? You wouldn't be able to pass a filepath for those even after #4341, so I'm wondering how you do it now. Do you use the editor scope? Maybe prettier needs to support editor scope for parser inference in addition to filepath?
@suchipi sorry for late response. Currently, we always specify a parser based on Atom scope, but after #4341 we just don't pass a parser or filepath at all if the file is not saved and prettier reverts to a JS parser and attempts to format with that.

I would stay away from prettier supporting editor scope, that would quickly turn into a nightmare to support. IMO it really should be the editor integration's responsibility if we wanted to go that route, but we're purposely deleting all of the code doing this in #4341 because that paradigm makes it basically impossible to support the new prettier plugins feature.
@suchipi I think we're safe to go forward with this
Ok, cool

Solution patch:
diff --git a/index.js b/index.js
index a7c361d04f3b..4b3f4608b94e 100644
--- a/index.js
+++ b/index.js
@@ -42,12 +42,8 @@ module.exports = {
   },
 
   check: function(text, opts) {
-    try {
-      const formatted = formatWithCursor(text, opts).formatted;
-      return formatted === text;
-    } catch (e) {
-      return false;
-    }
+    const formatted = formatWithCursor(text, opts).formatted;
+    return formatted === text;
   },
 
   doc,
diff --git a/src/cli/util.js b/src/cli/util.js
index 36f9d0f23c31..4a3c2e4274b3 100644
--- a/src/cli/util.js
+++ b/src/cli/util.js
@@ -52,6 +52,23 @@ function diff(a, b) {
 }
 
 function handleError(context, filename, error) {
+  if (error instanceof errors.UndefinedParserError) {
+    if (context.argv["write"] && process.stdout.isTTY) {
+      readline.clearLine(process.stdout, 0);
+      readline.cursorTo(process.stdout, 0, null);
+    }
+    if (!context.argv["list-different"]) {
+      process.exitCode = 2;
+    }
+    context.logger.error(error.message);
+    return;
+  }
+
+  if (context.argv["write"]) {
+    // Add newline to split errors from filename line.
+    process.stdout.write("\n");
+  }
+
   const isParseError = Boolean(error && error.loc);
   const isValidationError = /Validation Error/.test(error && error.message);
 
@@ -119,13 +136,15 @@ function listDifferent(context, input, options, filename) {
     return;
   }
 
-  options = Object.assign({}, options, { filepath: filename });
-
-  if (!prettier.check(input, options)) {
-    if (!context.argv["write"]) {
-      context.logger.log(filename);
+  try {
+    if (!prettier.check(input, options)) {
+      if (!context.argv["write"]) {
+        context.logger.log(filename);
+      }
+      process.exitCode = 1;
     }
-    process.exitCode = 1;
+  } catch (error) {
+    context.logger.error(error.message);
   }
 
   return true;
@@ -286,11 +305,11 @@ function formatStdin(context) {
 
     const options = getOptionsForFile(context, filepath);
 
-    if (listDifferent(context, input, options, "(stdin)")) {
-      return;
-    }
-
     try {
+      if (listDifferent(context, input, options, "(stdin)")) {
+        return;
+      }
+
       writeOutput(format(context, input, options), options);
     } catch (error) {
       handleError(context, "stdin", error);
@@ -329,7 +348,12 @@ function eachFilename(context, patterns, callback) {
       return;
     }
     filePaths.forEach(filePath =>
-      callback(filePath, getOptionsForFile(context, filePath))
+      callback(
+        filePath,
+        Object.assign(getOptionsForFile(context, filePath), {
+          filepath: filePath
+        })
+      )
     );
   } catch (error) {
     context.logger.error(
@@ -381,8 +405,6 @@ function formatFiles(context) {
       return;
     }
 
-    listDifferent(context, input, options, filename);
-
     const start = Date.now();
 
     let result;
@@ -396,13 +418,17 @@ function formatFiles(context) {
       );
       output = result.formatted;
     } catch (error) {
-      // Add newline to split errors from filename line.
-      process.stdout.write("\n");
-
       handleError(context, filename, error);
       return;
     }
 
+    const isDifferent = output !== input;
+
+    if (context.argv["list-different"] && isDifferent) {
+      context.logger.log(filename);
+      process.exitCode = 1;
+    }
+
     if (context.argv["write"]) {
       if (process.stdout.isTTY) {
         // Remove previously printed filename to log it with duration.
@@ -412,14 +438,8 @@ function formatFiles(context) {
 
       // Don't write the file if it won't change in order not to invalidate
       // mtime based caches.
-      if (output === input) {
+      if (isDifferent) {
         if (!context.argv["list-different"]) {
-          context.logger.log(`${chalk.grey(filename)} ${Date.now() - start}ms`);
-        }
-      } else {
-        if (context.argv["list-different"]) {
-          context.logger.log(filename);
-        } else {
           context.logger.log(`${filename} ${Date.now() - start}ms`);
         }
 
@@ -432,6 +452,8 @@ function formatFiles(context) {
           // Don't exit the process if one file failed
           process.exitCode = 2;
         }
+      } else if (!context.argv["list-different"]) {
+        context.logger.log(`${chalk.grey(filename)} ${Date.now() - start}ms`);
       }
     } else if (context.argv["debug-check"]) {
       if (output) {
diff --git a/src/common/errors.js b/src/common/errors.js
index a9f9fd1c1970..056b790b8ba8 100644
--- a/src/common/errors.js
+++ b/src/common/errors.js
@@ -2,8 +2,10 @@
 
 class ConfigError extends Error {}
 class DebugError extends Error {}
+class UndefinedParserError extends Error {}
 
 module.exports = {
   ConfigError,
-  DebugError
+  DebugError,
+  UndefinedParserError
 };
diff --git a/src/main/core-options.js b/src/main/core-options.js
index 4715f2b19e79..37bd07108cda 100644
--- a/src/main/core-options.js
+++ b/src/main/core-options.js
@@ -85,7 +85,10 @@ const options = {
     since: "0.0.10",
     category: CATEGORY_GLOBAL,
     type: "choice",
-    default: "babylon",
+    default: [
+      { since: "0.0.10", value: "babylon" },
+      { since: "1.13.0", value: undefined }
+    ],
     description: "Which parser to use.",
     exception: value =>
       typeof value === "string" || typeof value === "function",
diff --git a/src/main/core.js b/src/main/core.js
index 096cab85f0c7..0e2dad6c83d9 100644
--- a/src/main/core.js
+++ b/src/main/core.js
@@ -255,6 +255,7 @@ function format(text, opts) {
 
 module.exports = {
   formatWithCursor(text, opts) {
+    opts = normalizeOptions(opts);
     return format(text, normalizeOptions(opts));
   },
 
@@ -275,8 +276,8 @@ module.exports = {
 
   // Doesn't handle shebang for now
   formatDoc(doc, opts) {
-    opts = normalizeOptions(opts);
     const debug = printDocToDebug(doc);
+    opts = normalizeOptions(Object.assign({}, opts, { parser: "babylon" }));
     return format(debug, opts).formatted;
   },
 
diff --git a/src/main/multiparser.js b/src/main/multiparser.js
index 9ddcf8e8cc34..3ae96d2ae727 100644
--- a/src/main/multiparser.js
+++ b/src/main/multiparser.js
@@ -21,7 +21,7 @@ function textToDoc(text, partialNextOptions, parentOptions) {
       parentParser: parentOptions.parser,
       originalText: text
     }),
-    { passThrough: true, inferParser: false }
+    { passThrough: true }
   );
 
   const result = require("./parser").parse(text, nextOptions);
diff --git a/src/main/options.js b/src/main/options.js
index 5a7d57525e48..71cfc19862db 100644
--- a/src/main/options.js
+++ b/src/main/options.js
@@ -1,6 +1,7 @@
 "use strict";
 
 const path = require("path");
+const UndefinedParserError = require("../common/errors").UndefinedParserError;
 const getSupportInfo = require("../main/support").getSupportInfo;
 const normalizer = require("./options-normalizer");
 const resolveParser = require("./parser").resolveParser;
@@ -29,30 +30,27 @@ function normalize(options, opts) {
     Object.assign({}, hiddenDefaults)
   );
 
-  if (opts.inferParser !== false) {
-    if (
-      rawOptions.filepath &&
-      (!rawOptions.parser || rawOptions.parser === defaults.parser)
-    ) {
-      const inferredParser = inferParser(
-        rawOptions.filepath,
-        rawOptions.plugins
+  if (!rawOptions.parser) {
+    if (!rawOptions.filepath) {
+      throw new UndefinedParserError(
+        "No parser and no file path given, couldn't infer a parser."
+      );
+    }
+
+    rawOptions.parser = inferParser(rawOptions.filepath, rawOptions.plugins);
+    if (!rawOptions.parser) {
+      throw new UndefinedParserError(
+        `No parser could be inferred for file: ${rawOptions.filepath}`
       );
-      if (inferredParser) {
-        rawOptions.parser = inferredParser;
-      }
     }
   }
 
   const parser = resolveParser(
-    !rawOptions.parser
-      ? rawOptions
-      : // handle deprecated parsers
-        normalizer.normalizeApiOptions(
-          rawOptions,
-          [supportOptions.find(x => x.name === "parser")],
-          { passThrough: true, logger: false }
-        )
+    normalizer.normalizeApiOptions(
+      rawOptions,
+      [supportOptions.find(x => x.name === "parser")],
+      { passThrough: true, logger: false }
+    )
   );
   rawOptions.astFormat = parser.astFormat;
   rawOptions.locEnd = parser.locEnd;
diff --git a/src/main/parser.js b/src/main/parser.js
index f77a6e45d951..7c07fee74eff 100644
--- a/src/main/parser.js
+++ b/src/main/parser.js
@@ -43,8 +43,6 @@ function resolveParser(opts, parsers) {
       throw new ConfigError(`Couldn't resolve parser "${opts.parser}"`);
     }
   }
-  /* istanbul ignore next */
-  return parsers.babylon;
 }
 
 function parse(text, opts) {


Test patch:
diff --git a/tests/cursor/jsfmt.spec.js b/tests/cursor/jsfmt.spec.js
index 1eb4f87bc12b..7f06df1daf17 100644
--- a/tests/cursor/jsfmt.spec.js
+++ b/tests/cursor/jsfmt.spec.js
@@ -3,7 +3,9 @@ run_spec(__dirname, ["babylon", "typescript", "flow"]);
 const prettier = require("../../tests_config/require_prettier");
 
 test("translates cursor correctly in basic case", () => {
-  expect(prettier.formatWithCursor(" 1", { cursorOffset: 2 })).toEqual({
+  expect(
+    prettier.formatWithCursor(" 1", { parser: "babylon", cursorOffset: 2 })
+  ).toEqual({
     formatted: "1;\n",
     cursorOffset: 1
   });
@@ -11,7 +13,9 @@ test("translates cursor correctly in basic case", () => {
 
 test("positions cursor relative to closest node, not SourceElement", () => {
   const code = "return         15";
-  expect(prettier.formatWithCursor(code, { cursorOffset: 15 })).toEqual({
+  expect(
+    prettier.formatWithCursor(code, { parser: "babylon", cursorOffset: 15 })
+  ).toEqual({
     formatted: "return 15;\n",
     cursorOffset: 7
   });
@@ -19,7 +23,9 @@ test("positions cursor relative to closest node, not SourceElement", () => {
 
 test("keeps cursor inside formatted node", () => {
   const code = "return         15";
-  expect(prettier.formatWithCursor(code, { cursorOffset: 14 })).toEqual({
+  expect(
+    prettier.formatWithCursor(code, { parser: "babylon", cursorOffset: 14 })
+  ).toEqual({
     formatted: "return 15;\n",
     cursorOffset: 7
   });
@@ -30,7 +36,9 @@ test("doesn't insert second placeholder for nonexistent TypeAnnotation", () => {
 foo('bar', cb => {
   console.log('stuff')
 })`;
-  expect(prettier.formatWithCursor(code, { cursorOffset: 24 })).toEqual({
+  expect(
+    prettier.formatWithCursor(code, { parser: "babylon", cursorOffset: 24 })
+  ).toEqual({
     formatted: `foo("bar", cb => {
   console.log("stuff");
 });
diff --git a/tests_integration/__tests__/__snapshots__/debug-check.js.snap b/tests_integration/__tests__/__snapshots__/debug-check.js.snap
index 721cff9e3bf3..0f63c4825fee 100644
--- a/tests_integration/__tests__/__snapshots__/debug-check.js.snap
+++ b/tests_integration/__tests__/__snapshots__/debug-check.js.snap
@@ -30,10 +30,6 @@ exports[`show diff for 2+ error files with --debug-check (stderr) 1`] = `
 "
 `;
 
-exports[`show diff for 2+ error files with --debug-check (stdout) 1`] = `
-"
-
-"
-`;
+exports[`show diff for 2+ error files with --debug-check (stdout) 1`] = `""`;
 
 exports[`show diff for 2+ error files with --debug-check (write) 1`] = `Array []`;
diff --git a/tests_integration/__tests__/__snapshots__/early-exit.js.snap b/tests_integration/__tests__/__snapshots__/early-exit.js.snap
index 20de82ea31c6..4a35410b2fd8 100644
--- a/tests_integration/__tests__/__snapshots__/early-exit.js.snap
+++ b/tests_integration/__tests__/__snapshots__/early-exit.js.snap
@@ -296,8 +296,6 @@ Valid options:
   graphql          GraphQL
   markdown         Markdown
   vue              Vue
-
-Default: babylon
 "
 `;
 
@@ -597,7 +595,6 @@ Format options:
                            Defaults to false.
   --parser <flow|babylon|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|vue>
                            Which parser to use.
-                           Defaults to babylon.
   --print-width <int>      The line length where Prettier will try wrap.
                            Defaults to 80.
   --prose-wrap <always|never|preserve>
@@ -717,8 +714,6 @@ Valid options:
   graphql          GraphQL
   markdown         Markdown
   vue              Vue
-
-Default: babylon
 "
 `;
 
@@ -749,7 +744,6 @@ Format options:
                            Defaults to false.
   --parser <flow|babylon|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|vue>
                            Which parser to use.
-                           Defaults to babylon.
   --print-width <int>      The line length where Prettier will try wrap.
                            Defaults to 80.
   --prose-wrap <always|never|preserve>
diff --git a/tests_integration/__tests__/__snapshots__/infer-parser.js.snap b/tests_integration/__tests__/__snapshots__/infer-parser.js.snap
new file mode 100644
index 000000000000..4466cb819538
--- /dev/null
+++ b/tests_integration/__tests__/__snapshots__/infer-parser.js.snap
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` 1`] = `"No parser and no file path given, couldn't infer a parser."`;
+
+exports[`--list-different with unknown path and no parser multiple files (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`--list-different with unknown path and no parser specific file (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`--write and --list-different with unknown path and no parser multiple files (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`--write and --list-different with unknown path and no parser multiple files (stdout) 1`] = `
+"foo.js
+"
+`;
+
+exports[`--write and --list-different with unknown path and no parser multiple files (write) 1`] = `
+Array [
+  Object {
+    "content": "foo();
+",
+    "filename": "foo.js",
+  },
+]
+`;
+
+exports[`--write and --list-different with unknown path and no parser specific file (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`--write with unknown path and no parser multiple files (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`--write with unknown path and no parser multiple files (stdout) 1`] = `
+"foo.js 0ms
+"
+`;
+
+exports[`--write with unknown path and no parser multiple files (write) 1`] = `
+Array [
+  Object {
+    "content": "foo();
+",
+    "filename": "foo.js",
+  },
+]
+`;
+
+exports[`--write with unknown path and no parser specific file (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`stdin no path and no parser --list-different logs error but exits with 0 (stderr) 1`] = `
+"[error] No parser and no file path given, couldn't infer a parser.
+"
+`;
+
+exports[`stdin no path and no parser logs error and exits with 2 (stderr) 1`] = `
+"[error] No parser and no file path given, couldn't infer a parser.
+"
+`;
+
+exports[`stdin with unknown path and no parser --list-different logs error but exits with 0 (stderr) 1`] = `
+"[error] No parser could be inferred for file: foo
+"
+`;
+
+exports[`stdin with unknown path and no parser logs error and exits with 2 (stderr) 1`] = `
+"[error] No parser could be inferred for file: foo
+"
+`;
+
+exports[`unknown path and no parser multiple files (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
+
+exports[`unknown path and no parser multiple files (stdout) 1`] = `
+"foo();
+"
+`;
+
+exports[`unknown path and no parser specific file (stderr) 1`] = `
+"[error] No parser could be inferred for file: FOO
+"
+`;
diff --git a/tests_integration/__tests__/__snapshots__/plugin-options.js.snap b/tests_integration/__tests__/__snapshots__/plugin-options.js.snap
index f95858860b66..2714d56b1775 100644
--- a/tests_integration/__tests__/__snapshots__/plugin-options.js.snap
+++ b/tests_integration/__tests__/__snapshots__/plugin-options.js.snap
@@ -17,7 +17,7 @@ exports[` 1`] = `
                              Defaults to false.
     --parser <flow|babylon|typescript|css|less|scss|json|json5|json-stringify|graphql|markdown|vue>
                              Which parser to use.
-                             Defaults to babylon."
+    --print-width <int>      The line length where Prettier will try wrap."
 `;
 
 exports[`show detailed external option with \`--help foo-option\` (stderr) 1`] = `""`;
diff --git a/tests_integration/__tests__/config-invalid.js b/tests_integration/__tests__/config-invalid.js
index 159dc03ed9d0..dd8236f87605 100644
--- a/tests_integration/__tests__/config-invalid.js
+++ b/tests_integration/__tests__/config-invalid.js
@@ -40,14 +40,26 @@ describe("throw error with invalid config precedence option (configPrecedence)",
   });
 });
 
+// Tests below require --parser to prevent an error (no parser/filepath specified)
+
 describe("show warning with unknown option", () => {
-  runPrettier("cli/config/invalid", ["--config", "option/unknown"]).test({
+  runPrettier("cli/config/invalid", [
+    "--config",
+    "option/unknown",
+    "--parser",
+    "babylon"
+  ]).test({
     status: 0
   });
 });
 
 describe("show warning with kebab-case option key", () => {
-  runPrettier("cli/config/invalid", ["--config", "option/kebab-case"]).test({
+  runPrettier("cli/config/invalid", [
+    "--config",
+    "option/kebab-case",
+    "--parser",
+    "babylon"
+  ]).test({
     status: 0
   });
 });
diff --git a/tests_integration/__tests__/cursor-offset.js b/tests_integration/__tests__/cursor-offset.js
index d6e6cce2f07f..072aefc62606 100644
--- a/tests_integration/__tests__/cursor-offset.js
+++ b/tests_integration/__tests__/cursor-offset.js
@@ -3,13 +3,15 @@
 const runPrettier = require("../runPrettier");
 
 describe("write cursorOffset to stderr with --cursor-offset <int>", () => {
-  runPrettier("cli", ["--cursor-offset", "2"], { input: " 1" }).test({
+  runPrettier("cli", ["--cursor-offset", "2", "--parser", "babylon"], {
+    input: " 1"
+  }).test({
     status: 0
   });
 });
 
 describe("cursorOffset should not be affected by full-width character", () => {
-  runPrettier("cli", ["--cursor-offset", "21"], {
+  runPrettier("cli", ["--cursor-offset", "21", "--parser", "babylon"], {
     input: `const x = ["中文", "中文", "中文", "中文", "中文", "中文", "中文", "中文", "中文", "中文", "中文"];`
     //                              ^ offset = 21                              ^ width = 80
   }).test({
diff --git a/tests_integration/__tests__/debug-check.js b/tests_integration/__tests__/debug-check.js
index 1c9cb5bcfb4d..9e02eb208806 100644
--- a/tests_integration/__tests__/debug-check.js
+++ b/tests_integration/__tests__/debug-check.js
@@ -11,7 +11,7 @@ describe("doesn't crash when --debug-check is passed", () => {
 });
 
 describe("checks stdin with --debug-check", () => {
-  runPrettier("cli/with-shebang", ["--debug-check"], {
+  runPrettier("cli/with-shebang", ["--debug-check", "--parser", "babylon"], {
     input: "0"
   }).test({
     stdout: "(stdin)\n",
diff --git a/tests_integration/__tests__/debug-print-doc.js b/tests_integration/__tests__/debug-print-doc.js
index 954b72843dec..2078fa59ff56 100644
--- a/tests_integration/__tests__/debug-print-doc.js
+++ b/tests_integration/__tests__/debug-print-doc.js
@@ -3,9 +3,11 @@
 const runPrettier = require("../runPrettier");
 
 describe("prints doc with --debug-print-doc", () => {
-  runPrettier("cli/with-shebang", ["--debug-print-doc"], {
-    input: "0"
-  }).test({
+  runPrettier(
+    "cli/with-shebang",
+    ["--debug-print-doc", "--parser", "babylon"],
+    { input: "0" }
+  ).test({
     stdout: '["0", ";", hardline, breakParent];\n',
     stderr: "",
     status: 0
diff --git a/tests_integration/__tests__/infer-parser.js b/tests_integration/__tests__/infer-parser.js
new file mode 100644
index 000000000000..686da50261e8
--- /dev/null
+++ b/tests_integration/__tests__/infer-parser.js
@@ -0,0 +1,123 @@
+"use strict";
+
+const runPrettier = require("../runPrettier");
+const prettier = require("../../tests_config/require_prettier");
+
+describe("stdin no path and no parser", () => {
+  describe("logs error and exits with 2", () => {
+    runPrettier("cli/infer-parser/", ["--stdin"], { input: "foo" }).test({
+      status: 2,
+      stdout: "",
+      write: []
+    });
+  });
+
+  describe("--list-different logs error but exits with 0", () => {
+    runPrettier("cli/infer-parser/", ["--list-different", "--stdin"], {
+      input: "foo"
+    }).test({
+      status: 0,
+      stdout: "",
+      write: []
+    });
+  });
+});
+
+describe("stdin with unknown path and no parser", () => {
+  describe("logs error and exits with 2", () => {
+    runPrettier("cli/infer-parser/", ["--stdin", "--stdin-filepath", "foo"], {
+      input: "foo"
+    }).test({
+      status: 2,
+      stdout: "",
+      write: []
+    });
+  });
+
+  describe("--list-different logs error but exits with 0", () => {
+    runPrettier(
+      "cli/infer-parser/",
+      ["--list-different", "--stdin", "--stdin-filepath", "foo"],
+      { input: "foo" }
+    ).test({
+      status: 0,
+      stdout: "",
+      write: []
+    });
+  });
+});
+
+describe("unknown path and no parser", () => {
+  describe("specific file", () => {
+    runPrettier("cli/infer-parser/", ["FOO"]).test({
+      status: 2,
+      stdout: "",
+      write: []
+    });
+  });
+
+  describe("multiple files", () => {
+    runPrettier("cli/infer-parser/", ["*"]).test({
+      status: 2,
+      write: []
+    });
+  });
+});
+
+describe("--list-different with unknown path and no parser", () => {
+  describe("specific file", () => {
+    runPrettier("cli/infer-parser/", ["--list-different", "FOO"]).test({
+      status: 0,
+      stdout: "",
+      write: []
+    });
+  });
+
+  describe("multiple files", () => {
+    runPrettier("cli/infer-parser/", ["--list-different", "*"]).test({
+      status: 1,
+      stdout: "foo.js\n",
+      write: []
+    });
+  });
+});
+
+describe("--write with unknown path and no parser", () => {
+  describe("specific file", () => {
+    runPrettier("cli/infer-parser/", ["--write", "FOO"]).test({
+      status: 2,
+      stdout: "",
+      write: []
+    });
+  });
+
+  describe("multiple files", () => {
+    runPrettier("cli/infer-parser/", ["--write", "*"]).test({
+      status: 2
+    });
+  });
+});
+
+describe("--write and --list-different with unknown path and no parser", () => {
+  describe("specific file", () => {
+    runPrettier("cli/infer-parser/", [
+      "--list-different",
+      "--write",
+      "FOO"
+    ]).test({
+      status: 0,
+      stdout: "",
+      write: []
+    });
+  });
+
+  describe("multiple files", () => {
+    runPrettier("cli/infer-parser/", ["--list-different", "--write", "*"]).test(
+      { status: 1 }
+    );
+  });
+});
+
+describe("API with no path and no parser", () => {
+  expect(() => prettier.format("foo")).toThrowErrorMatchingSnapshot();
+});
diff --git a/tests_integration/__tests__/list-different.js b/tests_integration/__tests__/list-different.js
index 48b4d03f316d..9ad02dd86f4f 100644
--- a/tests_integration/__tests__/list-different.js
+++ b/tests_integration/__tests__/list-different.js
@@ -3,7 +3,7 @@
 const runPrettier = require("../runPrettier");
 
 describe("checks stdin with --list-different", () => {
-  runPrettier("cli/with-shebang", ["--list-different"], {
+  runPrettier("cli/with-shebang", ["--list-different", "--parser", "babylon"], {
     input: "0"
   }).test({
     stdout: "(stdin)\n",
diff --git a/tests_integration/__tests__/multiple-patterns.js b/tests_integration/__tests__/multiple-patterns.js
index 0fabcd8c5af6..aba8439a09ce 100644
--- a/tests_integration/__tests__/multiple-patterns.js
+++ b/tests_integration/__tests__/multiple-patterns.js
@@ -64,7 +64,8 @@ describe("multiple patterns by with ignore pattern, doesn't ignore node_modules
 });
 
 describe("no errors on empty patterns", () => {
-  runPrettier("cli/multiple-patterns").test({
+  // --parser is mandatory if no filepath is passed
+  runPrettier("cli/multiple-patterns", ["--parser", "babylon"]).test({
     status: 0
   });
 });
diff --git a/tests_integration/__tests__/syntax-error.js b/tests_integration/__tests__/syntax-error.js
index 3d8bf712a4c5..ee256ca8a662 100644
--- a/tests_integration/__tests__/syntax-error.js
+++ b/tests_integration/__tests__/syntax-error.js
@@ -3,7 +3,7 @@
 const runPrettier = require("../runPrettier");
 
 describe("exits with non-zero code when input has a syntax error", () => {
-  runPrettier("cli/with-shebang", ["--stdin"], {
+  runPrettier("cli/with-shebang", ["--stdin", "--parser", "babylon"], {
     input: "a.2"
   }).test({
     status: 2
diff --git a/tests_integration/cli/infer-parser/FOO b/tests_integration/cli/infer-parser/FOO
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests_integration/cli/infer-parser/foo.js b/tests_integration/cli/infer-parser/foo.js
new file mode 100644
index 000000000000..9da4d436fe16
--- /dev/null
+++ b/tests_integration/cli/infer-parser/foo.js
@@ -0,0 +1,1 @@
+foo  (    )