Skip to content

⬅️ Back to Table of Contents

📄 plugin-test-formatting

📊 Analysis Summary

Metric Count
🔧 Functions 16
📦 Imports 8
📊 Variables & Constants 4
📑 Type Aliases 3

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'plugin-test-formatting'
meta.type 'problem'
meta.docs.description Enforce that eslint-plugin test snippets are correctly formatted
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.invalidFormatting 'This snippet should be formatted correctly. Use the fixer to format the code.'
meta.messages.invalidFormattingErrorTest 'This snippet should be formatted correctly. Use the fixer to format the code. Note that the automated fixer may brea...
meta.messages.noUnnecessaryNoFormat 'noFormat is unnecessary here. Use the fixer to remove it.'
meta.messages.prettierException 'Prettier was unable to format this snippet: {{message}}'
meta.messages.singleLineQuotes 'Use quotes (\' or ") for single line tests.'
meta.messages.templateLiteralEmptyEnds 'Template literals must start and end with an empty line.'
meta.messages.templateLiteralLastLineIndent 'The closing line of the template literal must be indented to align with its parent.'
meta.schema [ { type: 'object', additionalProperties: false, properties: { formatWithPrettier: { type: 'boolean', description: 'W...
defaultOptions [ { formatWithPrettier: true, }, ]

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
prettier @prettier/sync
getContextualType @typescript-eslint/type-utils
AST_NODE_TYPES @typescript-eslint/utils
ESLintUtils @typescript-eslint/utils
path node:path
fileURLToPath node:url
createRule ../util/index.js

Variables & Constants

Name Type Kind Value Exported
prettierConfig any const prettier.resolveConfig(__dirname) ?? {}
START_OF_LINE_WHITESPACE_MA... RegExp const /^( *)/
BACKTICK_REGEX RegExp const //g`
TEMPLATE_EXPR_OPENER RegExp const /\$\{/g

Functions

create(context: any, [{ formatWithPrettier }]: any): { [x: string]: (test: TSESTree.ObjectExpression, isErrorTes…

Parameters:

  • context any
  • [{ formatWithPrettier }] any

Returns: { [x: string]: (test: TSESTree.ObjectExpression, isErrorTest?: boolean) => void; 'CallExpression > ObjectExpression > Property[key.name = "valid"] > ArrayExpression': (tests: TSESTree.ArrayExpression) => void; ObjectExpression(node: any): void; }

Calls:

  • ESLintUtils.getParserServices
  • services.program.getTypeChecker
  • prettier .format(code, { ...prettierConfig, parser: 'typescript', }) .trimEnd
  • getCodeFormatted
  • message.replace
  • context.report
  • checkLiteral
  • checkTemplateLiteral
  • checkTaggedTemplateExpression
  • checkCallExpression
  • getCodeFormattedOrReport
  • output.includes
  • fixer.replaceText
  • escapeForTemplateString
  • getSafeWrappingQuote
  • escapeForStringLiteral
  • fixer.replaceTextRange
  • text.split
  • lines[0].trimEnd
  • lastLine.trimStart
  • getExpectedIndentForNode
  • ' '.repeat
  • lines.pop
  • lines.shift
  • lines.join
  • checkForUnnecessaryNoFormat
  • isNoFormatTemplateTag
  • checkExpression
  • checkedObjects.has
  • checkedObjects.add
  • checkInvalidTest
  • [ AST_NODE_TYPES.CallExpression, AST_NODE_TYPES.ObjectExpression, 'Property[key.name = "invalid"]', AST_NODE_TYPES.ArrayExpression, AST_NODE_TYPES.ObjectExpression, ].join
  • getContextualType (from @typescript-eslint/type-utils)
  • services.esTreeNodeToTSNodeMap.get
  • checker.typeToString
  • /^(TSESLint\.)?RunTests\b/.test
  • checkValidTest
  • /^(TSESLint\.)?ValidTestCase\b/.test
  • /^(TSESLint\.)?InvalidTestCase\b/.test

Internal Comments:

// ex instanceof Error is false as of @prettier/sync@0.3.0, as is ex instanceof SyntaxError
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (x4)
// formatted string is multiline, then have to use backticks
// ignore template literals with ${expressions} for simplicity
// don't use template strings for single line tests
// prettier will trim out the end of line on save, but eslint will check before then (x2)
// last line can be indented (x2)
// multiline template strings must have an empty first/last line
// remove the empty lines (x4)
// all we do on single line test cases is check format, but there's no formatting to do
// handle cases like 'aa'.trimRight and `aa`.trimRight() (x3)
// delegate object-style tests to the invalid checker (x3)
// valid (x2)
// invalid - errors (x2)
/**
       * generic, type-aware handling for any old object
       * this is a fallback to handle random variables people declare or object
       * literals that are passed via array maps, etc
       */ (x2)

Code
create(context, [{ formatWithPrettier }]) {
    const services = ESLintUtils.getParserServices(context);
    const checker = services.program.getTypeChecker();

    const checkedObjects = new Set<TSESTree.ObjectExpression>();

    function getCodeFormatted(code: string): string | FormattingError {
      try {
        return prettier
          .format(code, {
            ...prettierConfig,
            parser: 'typescript',
          })
          .trimEnd(); // prettier will insert a new line at the end of the code
      } catch (ex) {
        // ex instanceof Error is false as of @prettier/sync@0.3.0, as is ex instanceof SyntaxError
        if (
          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
          (ex as Partial<Error> | undefined)?.constructor?.name !==
          'SyntaxError'
        ) {
          throw ex;
        }

        return ex as FormattingError;
      }
    }

    function getCodeFormattedOrReport(
      code: string,
      location: TSESTree.Node,
    ): string | null {
      if (formatWithPrettier === false) {
        return null;
      }

      const formatted = getCodeFormatted(code);
      if (typeof formatted === 'string') {
        return formatted;
      }

      let message = formatted.message;

      if (formatted.codeFrame) {
        message = message.replace(`\n${formatted.codeFrame}`, '');
      }
      if (formatted.loc) {
        message = message.replace(/ \(\d+:\d+\)$/, '');
      }

      context.report({
        node: location,
        messageId: 'prettierException',
        data: {
          message,
        },
      });
      return null;
    }

    function checkExpression(
      node: TSESTree.Node | null,
      isErrorTest: boolean,
    ): void {
      switch (node?.type) {
        case AST_NODE_TYPES.Literal:
          checkLiteral(node, isErrorTest);
          break;

        case AST_NODE_TYPES.TemplateLiteral:
          checkTemplateLiteral(node, isErrorTest);
          break;

        case AST_NODE_TYPES.TaggedTemplateExpression:
          checkTaggedTemplateExpression(node, isErrorTest);
          break;

        case AST_NODE_TYPES.CallExpression:
          checkCallExpression(node, isErrorTest);
          break;
      }
    }

    function checkLiteral(
      literal: TSESTree.Literal,
      isErrorTest: boolean,
      quoteIn?: string,
    ): void {
      if (typeof literal.value === 'string') {
        const output = getCodeFormattedOrReport(literal.value, literal);
        if (output && output !== literal.value) {
          context.report({
            node: literal,
            messageId: isErrorTest
              ? 'invalidFormattingErrorTest'
              : 'invalidFormatting',
            fix(fixer) {
              if (output.includes('\n')) {
                // formatted string is multiline, then have to use backticks
                return fixer.replaceText(
                  literal,
                  `\`${escapeForTemplateString(output)}\``,
                );
              }

              const quote = quoteIn ?? getSafeWrappingQuote(output) ?? "'";

              return fixer.replaceText(
                literal,
                `${quote}${escapeForStringLiteral(output, quote)}${quote}`,
              );
            },
          });
        }
      }
    }

    function checkTemplateLiteral(
      literal: TSESTree.TemplateLiteral,
      isErrorTest: boolean,
      isNoFormatTagged = false,
    ): void {
      if (literal.quasis.length > 1) {
        // ignore template literals with ${expressions} for simplicity
        return;
      }

      const text = literal.quasis[0].value.cooked;

      if (text == null) {
        return;
      }

      if (literal.loc.end.line === literal.loc.start.line) {
        // don't use template strings for single line tests
        return context.report({
          node: literal,
          messageId: 'singleLineQuotes',
          fix(fixer) {
            const quote = getSafeWrappingQuote(text);
            if (quote == null) {
              return null;
            }

            return [
              fixer.replaceTextRange(
                [literal.range[0], literal.range[0] + 1],
                quote,
              ),
              fixer.replaceTextRange(
                [literal.range[1] - 1, literal.range[1]],
                quote,
              ),
            ];
          },
        });
      }

      const lines = text.split('\n');
      const lastLine = lines[lines.length - 1];
      // prettier will trim out the end of line on save, but eslint will check before then
      const isStartEmpty = lines[0].trimEnd() === '';
      // last line can be indented
      const isEndEmpty = lastLine.trimStart() === '';
      if (!isStartEmpty || !isEndEmpty) {
        // multiline template strings must have an empty first/last line
        return context.report({
          node: literal,
          messageId: 'templateLiteralEmptyEnds',
          *fix(fixer) {
            if (!isStartEmpty) {
              yield fixer.replaceTextRange(
                [literal.range[0], literal.range[0] + 1],
                '`\n',
              );
            }

            if (!isEndEmpty) {
              yield fixer.replaceTextRange(
                [literal.range[1] - 1, literal.range[1]],
                '\n`',
              );
            }
          },
        });
      }

      const parentIndent = getExpectedIndentForNode(
        literal,
        context.sourceCode.lines,
      );
      if (lastLine.length !== parentIndent) {
        return context.report({
          node: literal,
          messageId: 'templateLiteralLastLineIndent',
          fix(fixer) {
            return fixer.replaceTextRange(
              [literal.range[1] - lastLine.length - 1, literal.range[1]],
              `${' '.repeat(parentIndent)}\``,
            );
          },
        });
      }

      // remove the empty lines
      lines.pop();
      lines.shift();

      const code = lines.join('\n');

      if (isNoFormatTagged) {
        if (literal.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
          checkForUnnecessaryNoFormat(code, literal.parent);
        }
        return;
      }

      const formatted = getCodeFormattedOrReport(code, literal);
      if (formatted && formatted !== code) {
        return context.report({
          node: literal,
          messageId: isErrorTest
            ? 'invalidFormattingErrorTest'
            : 'invalidFormatting',
          fix(fixer) {
            return fixer.replaceText(
              literal,
              `\`
${escapeForTemplateString(formatted)}
${' '.repeat(parentIndent)}\``,
            );
          },
        });
      }
    }

    function isNoFormatTemplateTag(tag: TSESTree.Expression): boolean {
      return tag.type === AST_NODE_TYPES.Identifier && tag.name === 'noFormat';
    }

    function checkForUnnecessaryNoFormat(
      text: string | null,
      expr: TSESTree.TaggedTemplateExpression,
    ): void {
      if (text == null) {
        return;
      }

      const formatted = getCodeFormatted(text);
      if (formatted === text) {
        context.report({
          node: expr,
          messageId: 'noUnnecessaryNoFormat',
          fix(fixer) {
            if (expr.loc.start.line === expr.loc.end.line) {
              return fixer.replaceText(
                expr,
                `'${escapeForTemplateString(text)}'`,
              );
            }
            return fixer.replaceText(expr.tag, '');
          },
        });
      }
    }

    function checkTaggedTemplateExpression(
      expr: TSESTree.TaggedTemplateExpression,
      isErrorTest: boolean,
    ): void {
      if (isNoFormatTemplateTag(expr.tag)) {
        const { cooked } = expr.quasi.quasis[0].value;
        checkForUnnecessaryNoFormat(cooked, expr);
      } else {
        return;
      }

      if (expr.loc.start.line === expr.loc.end.line) {
        // all we do on single line test cases is check format, but there's no formatting to do
        return;
      }

      checkTemplateLiteral(
        expr.quasi,
        isErrorTest,
        isNoFormatTemplateTag(expr.tag),
      );
    }

    function checkCallExpression(
      callExpr: TSESTree.CallExpression,
      isErrorTest: boolean,
    ): void {
      if (callExpr.callee.type !== AST_NODE_TYPES.MemberExpression) {
        return;
      }
      const memberExpr = callExpr.callee;
      // handle cases like 'aa'.trimRight and `aa`.trimRight()
      checkExpression(memberExpr.object, isErrorTest);
    }

    function checkInvalidTest(
      test: TSESTree.ObjectExpression,
      isErrorTest = true,
    ): void {
      if (checkedObjects.has(test)) {
        return;
      }

      checkedObjects.add(test);

      for (const prop of test.properties) {
        if (
          prop.type !== AST_NODE_TYPES.Property ||
          prop.computed ||
          prop.key.type !== AST_NODE_TYPES.Identifier
        ) {
          continue;
        }

        if (prop.key.name === 'code') {
          checkExpression(prop.value, isErrorTest);
        }
      }
    }

    function checkValidTest(tests: TSESTree.ArrayExpression): void {
      for (const test of tests.elements) {
        switch (test?.type) {
          case AST_NODE_TYPES.ObjectExpression:
            // delegate object-style tests to the invalid checker
            checkInvalidTest(test, false);
            break;

          default:
            checkExpression(test, false);
            break;
        }
      }
    }

    return {
      // valid
      'CallExpression > ObjectExpression > Property[key.name = "valid"] > ArrayExpression':
        checkValidTest,

      // invalid - errors
      [[
        AST_NODE_TYPES.CallExpression,
        AST_NODE_TYPES.ObjectExpression,
        'Property[key.name = "invalid"]',
        AST_NODE_TYPES.ArrayExpression,
        AST_NODE_TYPES.ObjectExpression,
      ].join(' > ')]: checkInvalidTest,

      /**
       * generic, type-aware handling for any old object
       * this is a fallback to handle random variables people declare or object
       * literals that are passed via array maps, etc
       */
      ObjectExpression(node): void {
        if (checkedObjects.has(node)) {
          return;
        }

        const type = getContextualType(
          checker,
          services.esTreeNodeToTSNodeMap.get(node),
        );
        if (!type) {
          return;
        }

        const typeString = checker.typeToString(type);
        if (/^(TSESLint\.)?RunTests\b/.test(typeString)) {
          checkedObjects.add(node);

          for (const prop of node.properties) {
            if (
              prop.type === AST_NODE_TYPES.SpreadElement ||
              prop.computed ||
              prop.key.type !== AST_NODE_TYPES.Identifier ||
              prop.value.type !== AST_NODE_TYPES.ArrayExpression
            ) {
              continue;
            }

            switch (prop.key.name) {
              case 'valid':
                checkValidTest(prop.value);
                break;

              case 'invalid':
                for (const element of prop.value.elements) {
                  if (element?.type === AST_NODE_TYPES.ObjectExpression) {
                    checkInvalidTest(element);
                  }
                }
                break;
            }
          }
          return;
        }

        if (/^(TSESLint\.)?ValidTestCase\b/.test(typeString)) {
          checkInvalidTest(node);
          return;
        }

        if (/^(TSESLint\.)?InvalidTestCase\b/.test(typeString)) {
          checkInvalidTest(node);
          for (const testProp of node.properties) {
            if (
              testProp.type === AST_NODE_TYPES.SpreadElement ||
              testProp.computed ||
              testProp.key.type !== AST_NODE_TYPES.Identifier ||
              testProp.key.name !== 'errors' ||
              testProp.value.type !== AST_NODE_TYPES.ArrayExpression
            ) {
              continue;
            }

            for (const errorElement of testProp.value.elements) {
              if (errorElement?.type !== AST_NODE_TYPES.ObjectExpression) {
                continue;
              }

              checkInvalidTest(errorElement);
            }
          }
        }
      },
    };
  }

getExpectedIndentForNode(node: TSESTree.Node, sourceCodeLines: string[]): number

Parameters:

  • node TSESTree.Node
  • sourceCodeLines string[]

Returns: number

Calls:

  • START_OF_LINE_WHITESPACE_MATCHER.exec

Internal Comments:

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)

Code
function getExpectedIndentForNode(
  node: TSESTree.Node,
  sourceCodeLines: string[],
): number {
  const lineIdx = node.loc.start.line - 1;
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const indent = START_OF_LINE_WHITESPACE_MATCHER.exec(
    sourceCodeLines[lineIdx],
  )![1];
  return indent.length;
}

getSafeWrappingQuote(code: string): "'" | '"' | null

Parameters:

  • code string

Returns: "'" | '"' | null

Calls:

  • code.includes

Internal Comments:

// be lazy and make them fix and escape the quotes manually

Code
function getSafeWrappingQuote(code: string): "'" | '"' | null {
  const hasSingleQuote = code.includes("'");
  const hasDoubleQuote = code.includes('"');
  if (hasSingleQuote && hasDoubleQuote) {
    // be lazy and make them fix and escape the quotes manually
    return null;
  }

  return hasSingleQuote ? '"' : "'";
}

escapeForTemplateString(code: string): string

Parameters:

  • code string

Returns: string

Calls:

  • fixed.replaceAll
Code
function escapeForTemplateString(code: string): string {
  let fixed = code;
  fixed = fixed.replaceAll('\\', '\\\\');
  fixed = fixed.replaceAll(BACKTICK_REGEX, '\\`');
  fixed = fixed.replaceAll(TEMPLATE_EXPR_OPENER, '\\${');
  return fixed;
}

escapeForStringLiteral(code: string, quote: string): string

Parameters:

  • code string
  • quote string

Returns: string

Calls:

  • fixed.replaceAll
Code
function escapeForStringLiteral(code: string, quote: string): string {
  let fixed = code;
  fixed = fixed.replaceAll('\\', '\\\\');
  fixed = fixed.replaceAll(quote, `\\${quote}`);
  return fixed;
}

Internal helpers

Declared inside another function in this file.

getCodeFormatted(code: string): string | FormattingError

Parameters:

  • code string

Returns: string | FormattingError

Calls:

  • prettier .format(code, { ...prettierConfig, parser: 'typescript', }) .trimEnd

Internal Comments:

// ex instanceof Error is false as of @prettier/sync@0.3.0, as is ex instanceof SyntaxError
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition (x4)

Code
function getCodeFormatted(code: string): string | FormattingError {
      try {
        return prettier
          .format(code, {
            ...prettierConfig,
            parser: 'typescript',
          })
          .trimEnd(); // prettier will insert a new line at the end of the code
      } catch (ex) {
        // ex instanceof Error is false as of @prettier/sync@0.3.0, as is ex instanceof SyntaxError
        if (
          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
          (ex as Partial<Error> | undefined)?.constructor?.name !==
          'SyntaxError'
        ) {
          throw ex;
        }

        return ex as FormattingError;
      }
    }

getCodeFormattedOrReport(code: string, location: TSESTree.Node): string | null

Parameters:

  • code string
  • location TSESTree.Node

Returns: string | null

Calls:

  • getCodeFormatted
  • message.replace
  • context.report
Code
function getCodeFormattedOrReport(
      code: string,
      location: TSESTree.Node,
    ): string | null {
      if (formatWithPrettier === false) {
        return null;
      }

      const formatted = getCodeFormatted(code);
      if (typeof formatted === 'string') {
        return formatted;
      }

      let message = formatted.message;

      if (formatted.codeFrame) {
        message = message.replace(`\n${formatted.codeFrame}`, '');
      }
      if (formatted.loc) {
        message = message.replace(/ \(\d+:\d+\)$/, '');
      }

      context.report({
        node: location,
        messageId: 'prettierException',
        data: {
          message,
        },
      });
      return null;
    }

checkExpression(node: TSESTree.Node | null, isErrorTest: boolean): void

Parameters:

  • node TSESTree.Node | null
  • isErrorTest boolean

Returns: void

Calls:

  • checkLiteral
  • checkTemplateLiteral
  • checkTaggedTemplateExpression
  • checkCallExpression
Code
function checkExpression(
      node: TSESTree.Node | null,
      isErrorTest: boolean,
    ): void {
      switch (node?.type) {
        case AST_NODE_TYPES.Literal:
          checkLiteral(node, isErrorTest);
          break;

        case AST_NODE_TYPES.TemplateLiteral:
          checkTemplateLiteral(node, isErrorTest);
          break;

        case AST_NODE_TYPES.TaggedTemplateExpression:
          checkTaggedTemplateExpression(node, isErrorTest);
          break;

        case AST_NODE_TYPES.CallExpression:
          checkCallExpression(node, isErrorTest);
          break;
      }
    }

checkLiteral(literal: TSESTree.Literal, isErrorTest: boolean, quoteIn: string): void

Parameters:

  • literal TSESTree.Literal
  • isErrorTest boolean
  • quoteIn string

Returns: void

Calls:

  • getCodeFormattedOrReport
  • context.report
  • output.includes
  • fixer.replaceText
  • escapeForTemplateString
  • getSafeWrappingQuote
  • escapeForStringLiteral

Internal Comments:

// formatted string is multiline, then have to use backticks

Code
function checkLiteral(
      literal: TSESTree.Literal,
      isErrorTest: boolean,
      quoteIn?: string,
    ): void {
      if (typeof literal.value === 'string') {
        const output = getCodeFormattedOrReport(literal.value, literal);
        if (output && output !== literal.value) {
          context.report({
            node: literal,
            messageId: isErrorTest
              ? 'invalidFormattingErrorTest'
              : 'invalidFormatting',
            fix(fixer) {
              if (output.includes('\n')) {
                // formatted string is multiline, then have to use backticks
                return fixer.replaceText(
                  literal,
                  `\`${escapeForTemplateString(output)}\``,
                );
              }

              const quote = quoteIn ?? getSafeWrappingQuote(output) ?? "'";

              return fixer.replaceText(
                literal,
                `${quote}${escapeForStringLiteral(output, quote)}${quote}`,
              );
            },
          });
        }
      }
    }

checkTemplateLiteral(literal: TSESTree.TemplateLiteral, isErrorTest: boolean, isNoFormatTagged: boolean): void

Parameters:

  • literal TSESTree.TemplateLiteral
  • isErrorTest boolean
  • isNoFormatTagged boolean

Returns: void

Calls:

  • context.report
  • getSafeWrappingQuote
  • fixer.replaceTextRange
  • text.split
  • lines[0].trimEnd
  • lastLine.trimStart
  • getExpectedIndentForNode
  • ' '.repeat
  • lines.pop
  • lines.shift
  • lines.join
  • checkForUnnecessaryNoFormat
  • getCodeFormattedOrReport
  • fixer.replaceText
  • escapeForTemplateString

Internal Comments:

// ignore template literals with ${expressions} for simplicity
// don't use template strings for single line tests
// prettier will trim out the end of line on save, but eslint will check before then (x2)
// last line can be indented (x2)
// multiline template strings must have an empty first/last line
// remove the empty lines (x4)

Code
function checkTemplateLiteral(
      literal: TSESTree.TemplateLiteral,
      isErrorTest: boolean,
      isNoFormatTagged = false,
    ): void {
      if (literal.quasis.length > 1) {
        // ignore template literals with ${expressions} for simplicity
        return;
      }

      const text = literal.quasis[0].value.cooked;

      if (text == null) {
        return;
      }

      if (literal.loc.end.line === literal.loc.start.line) {
        // don't use template strings for single line tests
        return context.report({
          node: literal,
          messageId: 'singleLineQuotes',
          fix(fixer) {
            const quote = getSafeWrappingQuote(text);
            if (quote == null) {
              return null;
            }

            return [
              fixer.replaceTextRange(
                [literal.range[0], literal.range[0] + 1],
                quote,
              ),
              fixer.replaceTextRange(
                [literal.range[1] - 1, literal.range[1]],
                quote,
              ),
            ];
          },
        });
      }

      const lines = text.split('\n');
      const lastLine = lines[lines.length - 1];
      // prettier will trim out the end of line on save, but eslint will check before then
      const isStartEmpty = lines[0].trimEnd() === '';
      // last line can be indented
      const isEndEmpty = lastLine.trimStart() === '';
      if (!isStartEmpty || !isEndEmpty) {
        // multiline template strings must have an empty first/last line
        return context.report({
          node: literal,
          messageId: 'templateLiteralEmptyEnds',
          *fix(fixer) {
            if (!isStartEmpty) {
              yield fixer.replaceTextRange(
                [literal.range[0], literal.range[0] + 1],
                '`\n',
              );
            }

            if (!isEndEmpty) {
              yield fixer.replaceTextRange(
                [literal.range[1] - 1, literal.range[1]],
                '\n`',
              );
            }
          },
        });
      }

      const parentIndent = getExpectedIndentForNode(
        literal,
        context.sourceCode.lines,
      );
      if (lastLine.length !== parentIndent) {
        return context.report({
          node: literal,
          messageId: 'templateLiteralLastLineIndent',
          fix(fixer) {
            return fixer.replaceTextRange(
              [literal.range[1] - lastLine.length - 1, literal.range[1]],
              `${' '.repeat(parentIndent)}\``,
            );
          },
        });
      }

      // remove the empty lines
      lines.pop();
      lines.shift();

      const code = lines.join('\n');

      if (isNoFormatTagged) {
        if (literal.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
          checkForUnnecessaryNoFormat(code, literal.parent);
        }
        return;
      }

      const formatted = getCodeFormattedOrReport(code, literal);
      if (formatted && formatted !== code) {
        return context.report({
          node: literal,
          messageId: isErrorTest
            ? 'invalidFormattingErrorTest'
            : 'invalidFormatting',
          fix(fixer) {
            return fixer.replaceText(
              literal,
              `\`
${escapeForTemplateString(formatted)}
${' '.repeat(parentIndent)}\``,
            );
          },
        });
      }
    }

isNoFormatTemplateTag(tag: TSESTree.Expression): boolean

Parameters:

  • tag TSESTree.Expression

Returns: boolean

Code
function isNoFormatTemplateTag(tag: TSESTree.Expression): boolean {
      return tag.type === AST_NODE_TYPES.Identifier && tag.name === 'noFormat';
    }

checkForUnnecessaryNoFormat(text: string | null, expr: TSESTree.TaggedTemplateExpression): void

Parameters:

  • text string | null
  • expr TSESTree.TaggedTemplateExpression

Returns: void

Calls:

  • getCodeFormatted
  • context.report
  • fixer.replaceText
  • escapeForTemplateString
Code
function checkForUnnecessaryNoFormat(
      text: string | null,
      expr: TSESTree.TaggedTemplateExpression,
    ): void {
      if (text == null) {
        return;
      }

      const formatted = getCodeFormatted(text);
      if (formatted === text) {
        context.report({
          node: expr,
          messageId: 'noUnnecessaryNoFormat',
          fix(fixer) {
            if (expr.loc.start.line === expr.loc.end.line) {
              return fixer.replaceText(
                expr,
                `'${escapeForTemplateString(text)}'`,
              );
            }
            return fixer.replaceText(expr.tag, '');
          },
        });
      }
    }

checkTaggedTemplateExpression(expr: TSESTree.TaggedTemplateExpression, isErrorTest: boolean): void

Parameters:

  • expr TSESTree.TaggedTemplateExpression
  • isErrorTest boolean

Returns: void

Calls:

  • isNoFormatTemplateTag
  • checkForUnnecessaryNoFormat
  • checkTemplateLiteral

Internal Comments:

// all we do on single line test cases is check format, but there's no formatting to do

Code
function checkTaggedTemplateExpression(
      expr: TSESTree.TaggedTemplateExpression,
      isErrorTest: boolean,
    ): void {
      if (isNoFormatTemplateTag(expr.tag)) {
        const { cooked } = expr.quasi.quasis[0].value;
        checkForUnnecessaryNoFormat(cooked, expr);
      } else {
        return;
      }

      if (expr.loc.start.line === expr.loc.end.line) {
        // all we do on single line test cases is check format, but there's no formatting to do
        return;
      }

      checkTemplateLiteral(
        expr.quasi,
        isErrorTest,
        isNoFormatTemplateTag(expr.tag),
      );
    }

checkCallExpression(callExpr: TSESTree.CallExpression, isErrorTest: boolean): void

Parameters:

  • callExpr TSESTree.CallExpression
  • isErrorTest boolean

Returns: void

Calls:

  • checkExpression

Internal Comments:

// handle cases like 'aa'.trimRight and `aa`.trimRight() (x3)

Code
function checkCallExpression(
      callExpr: TSESTree.CallExpression,
      isErrorTest: boolean,
    ): void {
      if (callExpr.callee.type !== AST_NODE_TYPES.MemberExpression) {
        return;
      }
      const memberExpr = callExpr.callee;
      // handle cases like 'aa'.trimRight and `aa`.trimRight()
      checkExpression(memberExpr.object, isErrorTest);
    }

checkInvalidTest(test: TSESTree.ObjectExpression, isErrorTest: boolean): void

Parameters:

  • test TSESTree.ObjectExpression
  • isErrorTest boolean

Returns: void

Calls:

  • checkedObjects.has
  • checkedObjects.add
  • checkExpression
Code
function checkInvalidTest(
      test: TSESTree.ObjectExpression,
      isErrorTest = true,
    ): void {
      if (checkedObjects.has(test)) {
        return;
      }

      checkedObjects.add(test);

      for (const prop of test.properties) {
        if (
          prop.type !== AST_NODE_TYPES.Property ||
          prop.computed ||
          prop.key.type !== AST_NODE_TYPES.Identifier
        ) {
          continue;
        }

        if (prop.key.name === 'code') {
          checkExpression(prop.value, isErrorTest);
        }
      }
    }

checkValidTest(tests: TSESTree.ArrayExpression): void

Parameters:

  • tests TSESTree.ArrayExpression

Returns: void

Calls:

  • checkInvalidTest
  • checkExpression

Internal Comments:

// delegate object-style tests to the invalid checker (x3)

Code
function checkValidTest(tests: TSESTree.ArrayExpression): void {
      for (const test of tests.elements) {
        switch (test?.type) {
          case AST_NODE_TYPES.ObjectExpression:
            // delegate object-style tests to the invalid checker
            checkInvalidTest(test, false);
            break;

          default:
            checkExpression(test, false);
            break;
        }
      }
    }

Type Aliases

Options

type Options = [
  {
    // This option exists so that rules like type-annotation-spacing can exist without every test needing a prettier-ignore
    formatWithPrettier?: boolean;
  },
];

MessageIds

type MessageIds = | 'invalidFormatting'
  | 'invalidFormattingErrorTest'
  | 'noUnnecessaryNoFormat'
  | 'prettierException'
  | 'singleLineQuotes'
  | 'templateLiteralEmptyEnds'
  | 'templateLiteralLastLineIndent';

FormattingError

type FormattingError = {
  codeFrame: string;
  loc?: unknown;
} & Error;

Generated by Syntax Scribe