Repository: prettier/prettier (JavaScript)
Date: 2021-12-01 04:41:31+00:00
Task type: Bug Fix

Issue:
"TypeError: Cannot set property 'originalText' of undefined" from TypeScript Jest inline snapshot
After upgrading from Prettier 2.4.x to 2.5.0, a Jest inline snapshot test in a TypeScript file throws the following error when Jest tries to update the snapshot:

```
    TypeError: Cannot set property 'originalText' of undefined

      at Object.<anonymous> (node_modules/prettier/parser-typescript.js:13:3304772)
      at Object.parse$d [as parse] (node_modules/prettier/index.js:12975:19)
      at coreFormat (node_modules/prettier/index.js:14525:16)
      at formatWithCursor$1 (node_modules/prettier/index.js:14765:14)
      at node_modules/prettier/index.js:60955:12
      at Object.format (node_modules/prettier/index.js:60975:12)
```

I'm not sure if this is a bug within Prettier or within how Jest is using Prettier; if I should file this with the Jest project, please let me know.

**Environments:**

- Prettier Version: 2.5.0
- Running Prettier via: CLI (via Jest)
- Runtime: Node.js 14.17.1
- Operating System: Windows and Linux
- Prettier plugins (if any): None

**Steps to reproduce:**

Clone https://github.com/joshkel/prettier-test-case and run `npm i; npm test`

**Expected behavior:**

The snapshot is correctly written and Prettier formats the file.

**Actual behavior:**

Abort with a stack trace.


Issue discussion:
Hi @joshkel, thanks for the bug report! Looks like it's a combination of two things.

First one is the fact that Jest calls our parsers directly, but never passed any options.

https://github.com/facebook/jest/blob/26abf9a2933f73ef0ae1f10e824c0f0dc0f93394/packages/jest-snapshot/src/InlineSnapshots.ts#L313

The other thing is that https://github.com/prettier/prettier/pull/11602 didn't account for uses outside of Prettier (which is the case for Jest, that used to pass both `parsers` and `opts` as `undefined`)

cc @sosukesuzuki 

Solution patch:
diff --git a/src/language-css/parser-postcss.js b/src/language-css/parser-postcss.js
index 38e96a495df6..69f687d2fd67 100644
--- a/src/language-css/parser-postcss.js
+++ b/src/language-css/parser-postcss.js
@@ -619,7 +619,7 @@ function parseWithParser(parse, text, options) {
 }
 
 // TODO: make this only work on css
-function parseCss(text, parsers, options) {
+function parseCss(text, parsers, options = {}) {
   const isSCSSParser = isSCSS(options.parser, text);
   const parseFunctions = isSCSSParser
     ? [parseScss, parseLess]
@@ -640,7 +640,7 @@ function parseCss(text, parsers, options) {
   }
 }
 
-function parseLess(text, parsers, options) {
+function parseLess(text, parsers, options = {}) {
   const lessParser = require("postcss-less");
   return parseWithParser(
     // Workaround for https://github.com/shellscape/postcss-less/issues/145
@@ -651,7 +651,7 @@ function parseLess(text, parsers, options) {
   );
 }
 
-function parseScss(text, parsers, options) {
+function parseScss(text, parsers, options = {}) {
   const { parse } = require("postcss-scss");
   return parseWithParser(parse, text, options);
 }
diff --git a/src/language-html/parser-html.js b/src/language-html/parser-html.js
index 119eff789b23..a7494dc13167 100644
--- a/src/language-html/parser-html.js
+++ b/src/language-html/parser-html.js
@@ -25,6 +25,7 @@ const { locStart, locEnd } = require("./loc.js");
  * @typedef {import('angular-html-parser/lib/compiler/src/ml_parser/ast').Element} Element
  * @typedef {import('angular-html-parser/lib/compiler/src/ml_parser/parser').ParseTreeResult} ParserTreeResult
  * @typedef {Omit<import('angular-html-parser').ParseOptions, 'canSelfClose'> & {
+ *   name?: 'html' | 'angular' | 'vue' | 'lwc';
  *   recognizeSelfClosing?: boolean;
  *   normalizeTagName?: boolean;
  *   normalizeAttributeName?: boolean;
@@ -365,6 +366,7 @@ function _parse(text, options, parserOptions, shouldParseFrontMatter = true) {
  * @param {ParserOptions} parserOptions
  */
 function createParser({
+  name,
   recognizeSelfClosing = false,
   normalizeTagName = false,
   normalizeAttributeName = false,
@@ -374,14 +376,18 @@ function createParser({
 } = {}) {
   return {
     parse: (text, parsers, options) =>
-      _parse(text, options, {
-        recognizeSelfClosing,
-        normalizeTagName,
-        normalizeAttributeName,
-        allowHtmComponentClosingTags,
-        isTagNameCaseSensitive,
-        getTagContentType,
-      }),
+      _parse(
+        text,
+        { parser: name, ...options },
+        {
+          recognizeSelfClosing,
+          normalizeTagName,
+          normalizeAttributeName,
+          allowHtmComponentClosingTags,
+          isTagNameCaseSensitive,
+          getTagContentType,
+        }
+      ),
     hasPragma,
     astFormat: "html",
     locStart,
@@ -392,13 +398,15 @@ function createParser({
 module.exports = {
   parsers: {
     html: createParser({
+      name: "html",
       recognizeSelfClosing: true,
       normalizeTagName: true,
       normalizeAttributeName: true,
       allowHtmComponentClosingTags: true,
     }),
-    angular: createParser(),
+    angular: createParser({ name: "angular" }),
     vue: createParser({
+      name: "vue",
       recognizeSelfClosing: true,
       isTagNameCaseSensitive: true,
       getTagContentType: (tagName, prefix, hasParent, attrs) => {
@@ -414,6 +422,6 @@ module.exports = {
         }
       },
     }),
-    lwc: createParser(),
+    lwc: createParser({ name: "lwc" }),
   },
 };
diff --git a/src/language-js/parse/espree.js b/src/language-js/parse/espree.js
index 2d3a558d7a34..d3b95793c3bb 100644
--- a/src/language-js/parse/espree.js
+++ b/src/language-js/parse/espree.js
@@ -32,7 +32,7 @@ function createParseError(error) {
   return createError(message, { start: { line: lineNumber, column } });
 }
 
-function parse(originalText, parsers, options) {
+function parse(originalText, parsers, options = {}) {
   const { parse } = require("espree");
 
   const textToParse = replaceHashbang(originalText);
diff --git a/src/language-js/parse/flow.js b/src/language-js/parse/flow.js
index c97d984476fc..c43218b2966a 100644
--- a/src/language-js/parse/flow.js
+++ b/src/language-js/parse/flow.js
@@ -45,7 +45,7 @@ function createParseError(error) {
   });
 }
 
-function parse(text, parsers, opts) {
+function parse(text, parsers, options = {}) {
   // Inline the require to avoid loading all the JS if we don't use it
   const { parse } = require("flow-parser");
   const ast = parse(replaceHashbang(text), parseOptions);
@@ -54,8 +54,8 @@ function parse(text, parsers, opts) {
     throw createParseError(error);
   }
 
-  opts.originalText = text;
-  return postprocess(ast, opts);
+  options.originalText = text;
+  return postprocess(ast, options);
 }
 
 // Export as a plugin so we can reuse the same bundle for UMD loading
diff --git a/src/language-js/parse/meriyah.js b/src/language-js/parse/meriyah.js
index 08f46b872131..c790b6bb5f98 100644
--- a/src/language-js/parse/meriyah.js
+++ b/src/language-js/parse/meriyah.js
@@ -71,7 +71,7 @@ function createParseError(error) {
   return createError(message, { start: { line, column } });
 }
 
-function parse(text, parsers, options) {
+function parse(text, parsers, options = {}) {
   const { result: ast, error: moduleParseError } = tryCombinations(
     () => parseWithOptions(text, /* module */ true),
     () => parseWithOptions(text, /* module */ false)
diff --git a/src/language-js/parse/typescript.js b/src/language-js/parse/typescript.js
index b3240918b811..a359fe8480f9 100644
--- a/src/language-js/parse/typescript.js
+++ b/src/language-js/parse/typescript.js
@@ -32,7 +32,7 @@ function createParseError(error) {
   });
 }
 
-function parse(text, parsers, opts) {
+function parse(text, parsers, options = {}) {
   const textToParse = replaceHashbang(text);
   const jsx = isProbablyJsx(text);
 
@@ -49,9 +49,9 @@ function parse(text, parsers, opts) {
     throw createParseError(firstError);
   }
 
-  opts.originalText = text;
-  opts.tsParseResult = result;
-  return postprocess(result.ast, opts);
+  options.originalText = text;
+  options.tsParseResult = result;
+  return postprocess(result.ast, options);
 }
 
 /**


Test patch:
diff --git a/tests/integration/__tests__/parser-api.js b/tests/integration/__tests__/parser-api.js
index 3d6920c31a3d..8f88a67a5e68 100644
--- a/tests/integration/__tests__/parser-api.js
+++ b/tests/integration/__tests__/parser-api.js
@@ -29,6 +29,33 @@ test("allows usage of prettier's supported parsers", () => {
   expect(output).toBe("bar();\n");
 });
 
+test("parsers should allow omit optional arguments", () => {
+  let parsers;
+  try {
+    prettier.format("{}", {
+      parser(text, builtinParsers) {
+        parsers = builtinParsers;
+      },
+    });
+  } catch {
+    // noop
+  }
+
+  expect(typeof parsers.babel).toBe("function");
+  const code = {
+    graphql: "type A {hero: Character}",
+    default: "{}",
+  };
+  for (const [name, parse] of Object.entries(parsers)) {
+    // Private parser should not be used by users
+    if (name.startsWith("__")) {
+      continue;
+    }
+
+    expect(() => parse(code[name] || code.default)).not.toThrow();
+  }
+});
+
 test("allows add empty `trailingComments` array", () => {
   const output = prettier.format("(foo /* comment */)( )", {
     parser(text, parsers) {