'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports.saveInlineSnapshots = void 0;

var _fs = _interopRequireDefault(require('fs'));

var _path = _interopRequireDefault(require('path'));

var _semver = _interopRequireDefault(require('semver'));

var _types = require('@babel/types');

var _utils = require('./utils');

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}

var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;

var jestWriteFile =
  global[Symbol.for('jest-native-write-file')] || _fs.default.writeFileSync;

function _objectSpread(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};
    var ownKeys = Object.keys(source);
    if (typeof Object.getOwnPropertySymbols === 'function') {
      ownKeys = ownKeys.concat(
        Object.getOwnPropertySymbols(source).filter(function(sym) {
          return Object.getOwnPropertyDescriptor(source, sym).enumerable;
        })
      );
    }
    ownKeys.forEach(function(key) {
      _defineProperty(target, key, source[key]);
    });
  }
  return target;
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;

var jestReadFile =
  global[Symbol.for('jest-native-read-file')] || _fs.default.readFileSync;

const saveInlineSnapshots = (snapshots, prettier, babelTraverse) => {
  if (!prettier) {
    throw new Error(
      `Jest: Inline Snapshots requires Prettier.\n` +
        `Please ensure "prettier" is installed in your project.`
    );
  } // Custom parser API was added in 1.5.0

  if (_semver.default.lt(prettier.version, '1.5.0')) {
    throw new Error(
      `Jest: Inline Snapshots require prettier>=1.5.0.\n` +
        `Please upgrade "prettier".`
    );
  }

  const snapshotsByFile = groupSnapshotsByFile(snapshots);

  var _arr = Object.keys(snapshotsByFile);

  for (var _i = 0; _i < _arr.length; _i++) {
    const sourceFilePath = _arr[_i];
    saveSnapshotsForFile(
      snapshotsByFile[sourceFilePath],
      sourceFilePath,
      prettier,
      babelTraverse
    );
  }
};

exports.saveInlineSnapshots = saveInlineSnapshots;

const saveSnapshotsForFile = (
  snapshots,
  sourceFilePath,
  prettier,
  babelTraverse
) => {
  const sourceFile = jestReadFile(sourceFilePath, 'utf8'); // Resolve project configuration.
  // For older versions of Prettier, do not load configuration.

  const config = prettier.resolveConfig
    ? prettier.resolveConfig.sync(sourceFilePath, {
        editorconfig: true
      })
    : null; // Detect the parser for the test file.
  // For older versions of Prettier, fallback to a simple parser detection.

  const inferredParser = prettier.getFileInfo
    ? prettier.getFileInfo.sync(sourceFilePath).inferredParser
    : (config && config.parser) || simpleDetectParser(sourceFilePath); // Insert snapshots using the custom parser API. After insertion, the code is
  // formatted, except snapshot indentation. Snapshots cannot be formatted until
  // after the initial format because we don't know where the call expression
  // will be placed (specifically its indentation).

  const newSourceFile = prettier.format(
    sourceFile,
    _objectSpread({}, config, {
      filepath: sourceFilePath,
      parser: createInsertionParser(snapshots, inferredParser, babelTraverse)
    })
  ); // Format the snapshots using the custom parser API.

  const formattedNewSourceFile = prettier.format(
    newSourceFile,
    _objectSpread({}, config, {
      filepath: sourceFilePath,
      parser: createFormattingParser(inferredParser, babelTraverse)
    })
  );

  if (formattedNewSourceFile !== sourceFile) {
    jestWriteFile(sourceFilePath, formattedNewSourceFile);
  }
};

const groupSnapshotsBy = createKey => snapshots =>
  snapshots.reduce((object, inlineSnapshot) => {
    const key = createKey(inlineSnapshot);
    return _objectSpread({}, object, {
      [key]: (object[key] || []).concat(inlineSnapshot)
    });
  }, {});

const groupSnapshotsByFrame = groupSnapshotsBy(({frame: {line, column}}) =>
  typeof line === 'number' && typeof column === 'number'
    ? `${line}:${column - 1}`
    : ''
);
const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file);

const indent = (snapshot, numIndents, indentation) => {
  const lines = snapshot.split('\n');
  return lines
    .map((line, index) => {
      if (index === 0) {
        // First line is either a 1-line snapshot or a blank line.
        return line;
      } else if (index !== lines.length - 1) {
        // Do not indent empty lines.
        if (line === '') {
          return line;
        } // Not last line, indent one level deeper than expect call.

        return indentation.repeat(numIndents + 1) + line;
      } else {
        // The last line should be placed on the same level as the expect call.
        return indentation.repeat(numIndents) + line;
      }
    })
    .join('\n');
};

const getAst = (parsers, inferredParser, text) => {
  // Flow uses a 'Program' parent node, babel expects a 'File'.
  let ast = parsers[inferredParser](text);

  if (ast.type !== 'File') {
    ast = (0, _types.file)(ast, ast.comments, ast.tokens);
    delete ast.program.comments;
  }

  return ast;
}; // This parser inserts snapshots into the AST.

const createInsertionParser = (snapshots, inferredParser, babelTraverse) => (
  text,
  parsers,
  options
) => {
  // Workaround for https://github.com/prettier/prettier/issues/3150
  options.parser = inferredParser;
  const groupedSnapshots = groupSnapshotsByFrame(snapshots);
  const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot));
  const ast = getAst(parsers, inferredParser, text);
  babelTraverse(ast, {
    CallExpression({node: {arguments: args, callee}}) {
      if (
        callee.type !== 'MemberExpression' ||
        callee.property.type !== 'Identifier'
      ) {
        return;
      }

      const _callee$property$loc$ = callee.property.loc.start,
        line = _callee$property$loc$.line,
        column = _callee$property$loc$.column;
      const snapshotsForFrame = groupedSnapshots[`${line}:${column}`];

      if (!snapshotsForFrame) {
        return;
      }

      if (snapshotsForFrame.length > 1) {
        throw new Error(
          'Jest: Multiple inline snapshots for the same call are not supported.'
        );
      }

      const snapshotIndex = args.findIndex(
        ({type}) => type === 'TemplateLiteral'
      );
      const values = snapshotsForFrame.map(({snapshot}) => {
        remainingSnapshots.delete(snapshot);
        return (0, _types.templateLiteral)(
          [
            (0, _types.templateElement)({
              raw: (0, _utils.escapeBacktickString)(snapshot)
            })
          ],
          []
        );
      });
      const replacementNode = values[0];

      if (snapshotIndex > -1) {
        args[snapshotIndex] = replacementNode;
      } else {
        args.push(replacementNode);
      }
    }
  });

  if (remainingSnapshots.size) {
    throw new Error(`Jest: Couldn't locate all inline snapshots.`);
  }

  return ast;
}; // This parser formats snapshots to the correct indentation.

const createFormattingParser = (inferredParser, babelTraverse) => (
  text,
  parsers,
  options
) => {
  // Workaround for https://github.com/prettier/prettier/issues/3150
  options.parser = inferredParser;
  const ast = getAst(parsers, inferredParser, text);
  babelTraverse(ast, {
    CallExpression({node: {arguments: args, callee}}) {
      if (
        callee.type !== 'MemberExpression' ||
        callee.property.type !== 'Identifier' ||
        callee.property.name !== 'toMatchInlineSnapshot' ||
        !callee.loc ||
        callee.computed
      ) {
        return;
      }

      let snapshotIndex;
      let snapshot;

      for (let i = 0; i < args.length; i++) {
        const node = args[i];

        if (node.type === 'TemplateLiteral') {
          snapshotIndex = i;
          snapshot = node.quasis[0].value.raw;
        }
      }

      if (snapshot === undefined || snapshotIndex === undefined) {
        return;
      }

      const useSpaces = !options.useTabs;
      snapshot = indent(
        snapshot,
        Math.ceil(
          useSpaces
            ? callee.loc.start.column / options.tabWidth
            : callee.loc.start.column / 2 // Each tab is 2 characters.
        ),
        useSpaces ? ' '.repeat(options.tabWidth) : '\t'
      );
      const replacementNode = (0, _types.templateLiteral)(
        [
          (0, _types.templateElement)({
            raw: snapshot
          })
        ],
        []
      );
      args[snapshotIndex] = replacementNode;
    }
  });
  return ast;
};

const simpleDetectParser = filePath => {
  const extname = _path.default.extname(filePath);

  if (/tsx?$/.test(extname)) {
    return 'typescript';
  }

  return 'babylon';
};