Skip to content

⬅️ Back to Table of Contents

📄 no-unnecessary-template-expression

📊 Analysis Summary

Metric Count
🔧 Functions 19
📦 Imports 14
📊 Variables & Constants 1
📐 Interfaces 1
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts

📤 Default Export

export default createRule<[], MessageId>({ ... })
Property Value
name 'no-unnecessary-template-expression'
meta.type 'suggestion'
meta.docs.description 'Disallow unnecessary template expressions'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.noUnnecessaryTemplateExpression 'Template literal expression is unnecessary and can be simplified.'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
ASTUtils @typescript-eslint/utils
createRule ../util
getConstraintInfo ../util
getMovedNodeCode ../util
getParserServices ../util
isNodeOfType ../util
isStringLike ../util
isUndefinedIdentifier ../util
nullThrows ../util
NullThrowsReasons ../util
rangeToLoc ../util/rangeToLoc

Variables & Constants

Name Type Kind Value Exported
evenNumOfBackslashesRegExp RegExp const /(?<!(?:[^\\]\|^)(?:\\\\)*\\)/

Functions

create(context: any): { TemplateLiteral(node: TSESTree.TemplateLiteral): void; TS…

Parameters:

  • context any

Returns: { TemplateLiteral(node: TSESTree.TemplateLiteral): void; TSTemplateLiteralType(node: TSESTree.TSTemplateLiteralType): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • tsutils.typeConstituents(type).some
  • t.getSymbol
  • ts.isEnumMember
  • isNodeOfType (from ../util)
  • isUndefinedIdentifier (from ../util)
  • isInfinityIdentifier
  • isNaNIdentifier
  • nullThrows (from ../util)
  • context.sourceCode.getTokenByRangeStart
  • NullThrowsReasons.MissingToken
  • context.sourceCode.commentsExistBetween
  • getInterpolations(node).map
  • isLiteral
  • isTemplateLiteral
  • getInterpolations
  • context.report
  • rangeToLoc (from ../util/rangeToLoc)
  • getMovedNodeCode (from ../util)
  • fixer.replaceText
  • hasCommentsBetweenQuasi
  • isFixableIdentifier
  • startsWithNewLine
  • isWhitespace
  • getLiteral
  • getTemplateLiteral
  • [...infos].reverse
  • nextQuasi.value.raw.startsWith
  • ( typeof literal.value === 'string' ? // The value is already a string, so we're removing quotes: // "'value'" -> "value" literal.raw.slice(1, -1) : // The value may be one of number | bigint | boolean | RegExp | null. // In regular expressions, we escape every backslash String(literal.value).replaceAll('\\', '\\\\') ) // The string or RegExp may contain or ${. // We want both of these to be escaped in the final template expression. // // A pair of backslashes means "escaped backslash", so backslashes // from this pair won't escape or ${. Therefore, to escape these // sequences in the resulting template expression, we need to escape // all sequences that are preceded by an even number of backslashes. // // This RegExp does the following transformations: // \ -> ` // \-> \\\ // \${ -> \${ // \${ -> \\${ .replaceAll`
  • literal.raw.slice
  • String(literal.value).replaceAll
  • endsWithUnescapedDollarSign
  • escapedValue.replaceAll
  • escapedValue.startsWith
  • fixers.push
  • fixer.replaceTextRange
  • templateLiteral.quasis[0].value.raw.startsWith
  • fixer.removeRange
  • reportDescriptors.push
  • fixers.flatMap
  • cb
  • isTrivialInterpolation
  • getConstraintInfo (from ../util)
  • services.getTypeAtLocation
  • isStringLike (from ../util)
  • reportSingleInterpolation
  • getInterpolationInfos(node).filter
  • getReportDescriptors
  • isEnumMemberType

Internal Comments:

// allow trailing whitespace literal (x4)
// "'va`lue'" -> "va`lue" (x4)
// In regular expressions, we escape every backslash (x4)
// `...${'...$'}{...`
//           ^^^^
// Since we iterate from the last expression to the first,
// a subsequent expression can tell the current expression
// that it starts with {.
//
// `... ${`... $`}${'{...'} ...`
//             ^     ^ subsequent expression starts with {
//             current expression ends with a dollar sign,
//             so '$' + '{' === '${' (bad news for us).
//             Let's escape the dollar sign at the end.
// Remove the beginning and trailing backtick characters. (x4)
// `... $${'{...'} ...`
//      ^^^^^
// Remove the quasis' parts that are related to the current expression. (x3)

Code
create(context) {
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();

    function isEnumMemberType(type: ts.Type): boolean {
      return tsutils.typeConstituents(type).some(t => {
        const symbol = t.getSymbol();
        return !!(
          symbol?.valueDeclaration && ts.isEnumMember(symbol.valueDeclaration)
        );
      });
    }

    const isLiteral = isNodeOfType(TSESTree.AST_NODE_TYPES.Literal);

    function isTemplateLiteral(
      node: TSESTree.Node,
    ): node is TSESTree.TemplateLiteral {
      return node.type === AST_NODE_TYPES.TemplateLiteral;
    }

    function isInfinityIdentifier(node: TSESTree.Node): boolean {
      return (
        node.type === AST_NODE_TYPES.Identifier && node.name === 'Infinity'
      );
    }

    function isNaNIdentifier(node: TSESTree.Node): boolean {
      return node.type === AST_NODE_TYPES.Identifier && node.name === 'NaN';
    }

    function isFixableIdentifier(node: TSESTree.Node): boolean {
      return (
        isUndefinedIdentifier(node) ||
        isInfinityIdentifier(node) ||
        isNaNIdentifier(node)
      );
    }

    function hasCommentsBetweenQuasi(
      startQuasi: TSESTree.TemplateElement,
      endQuasi: TSESTree.TemplateElement,
    ): boolean {
      const startToken = nullThrows(
        context.sourceCode.getTokenByRangeStart(startQuasi.range[0]),
        NullThrowsReasons.MissingToken('`${', 'opening template literal'),
      );
      const endToken = nullThrows(
        context.sourceCode.getTokenByRangeStart(endQuasi.range[0]),
        NullThrowsReasons.MissingToken('}', 'closing template literal'),
      );

      return context.sourceCode.commentsExistBetween(startToken, endToken);
    }

    function isTrivialInterpolation(
      node: TSESTree.TemplateLiteral | TSESTree.TSTemplateLiteralType,
    ) {
      return (
        node.quasis.length === 2 &&
        node.quasis[0].value.raw === '' &&
        node.quasis[1].value.raw === ''
      );
    }

    function getInterpolations(
      node: TemplateLiteralTypeOrValue,
    ): TSESTree.Expression[] | TSESTree.TypeNode[] {
      if (node.type === AST_NODE_TYPES.TemplateLiteral) {
        return node.expressions;
      }
      return node.types;
    }

    function getInterpolationInfos(
      node: TemplateLiteralTypeOrValue,
    ): InterpolationInfo[] {
      return getInterpolations(node).map((interpolation, index) => ({
        interpolation,
        nextQuasi: node.quasis[index + 1],
        prevQuasi: node.quasis[index],
      }));
    }

    function getLiteral(
      node: TSESTree.Expression | TSESTree.TypeNode,
    ): TSESTree.Literal | null {
      const maybeLiteral =
        node.type === AST_NODE_TYPES.TSLiteralType ? node.literal : node;
      return isLiteral(maybeLiteral) ? maybeLiteral : null;
    }

    function getTemplateLiteral(
      node: TSESTree.Expression | TSESTree.TypeNode,
    ): TSESTree.TemplateLiteral | null {
      const maybeTemplateLiteral =
        node.type === AST_NODE_TYPES.TSLiteralType ? node.literal : node;
      return isTemplateLiteral(maybeTemplateLiteral)
        ? maybeTemplateLiteral
        : null;
    }

    function reportSingleInterpolation(node: TemplateLiteralTypeOrValue): void {
      const interpolations = getInterpolations(node);
      context.report({
        loc: rangeToLoc(context.sourceCode, [
          interpolations[0].range[0] - 2,
          interpolations[0].range[1] + 1,
        ]),
        messageId: 'noUnnecessaryTemplateExpression',
        fix(fixer): TSESLint.RuleFix | null {
          const wrappingCode = getMovedNodeCode({
            destinationNode: node,
            nodeToMove: interpolations[0],
            sourceCode: context.sourceCode,
          });

          return fixer.replaceText(node, wrappingCode);
        },
      });
    }

    function isUnnecessaryValueInterpolation({
      interpolation,
      nextQuasi,
      prevQuasi,
    }: InterpolationInfo): boolean {
      if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) {
        return false;
      }

      if (isFixableIdentifier(interpolation)) {
        return true;
      }

      if (isLiteral(interpolation)) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            typeof interpolation.value === 'string' &&
            isWhitespace(interpolation.value)
          );
        }
        return true;
      }

      if (isTemplateLiteral(interpolation)) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            interpolation.quasis.length === 1 &&
            isWhitespace(interpolation.quasis[0].value.raw)
          );
        }
        return true;
      }

      return false;
    }

    function isUnnecessaryTypeInterpolation({
      interpolation,
      nextQuasi,
      prevQuasi,
    }: InterpolationInfo): boolean {
      if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) {
        return false;
      }

      const literal = getLiteral(interpolation);
      if (literal) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            typeof literal.value === 'string' && isWhitespace(literal.value)
          );
        }
        return true;
      }

      if (
        interpolation.type === AST_NODE_TYPES.TSNullKeyword ||
        interpolation.type === AST_NODE_TYPES.TSUndefinedKeyword
      ) {
        return true;
      }

      const templateLiteral = getTemplateLiteral(interpolation);
      if (templateLiteral) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            templateLiteral.quasis.length === 1 &&
            isWhitespace(templateLiteral.quasis[0].value.raw)
          );
        }
        return true;
      }

      return false;
    }

    function getReportDescriptors(
      infos: InterpolationInfo[],
    ): TSESLint.ReportDescriptor<MessageId>[] {
      let nextCharacterIsOpeningCurlyBrace = false;
      const reportDescriptors: TSESLint.ReportDescriptor<MessageId>[] = [];
      const reversedInfos = [...infos].reverse();
      for (const { interpolation, nextQuasi, prevQuasi } of reversedInfos) {
        const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] =
          [];

        if (nextQuasi.value.raw !== '') {
          nextCharacterIsOpeningCurlyBrace =
            nextQuasi.value.raw.startsWith('{');
        }

        const literal = getLiteral(interpolation);
        const templateLiteral = getTemplateLiteral(interpolation);
        if (literal) {
          let escapedValue = (
            typeof literal.value === 'string'
              ? // The value is already a string, so we're removing quotes:
                // "'va`lue'" -> "va`lue"
                literal.raw.slice(1, -1)
              : // The value may be one of number | bigint | boolean | RegExp | null.
                // In regular expressions, we escape every backslash
                String(literal.value).replaceAll('\\', '\\\\')
          )
            // The string or RegExp may contain ` or ${.
            // We want both of these to be escaped in the final template expression.
            //
            // A pair of backslashes means "escaped backslash", so backslashes
            // from this pair won't escape ` or ${. Therefore, to escape these
            // sequences in the resulting template expression, we need to escape
            // all sequences that are preceded by an even number of backslashes.
            //
            // This RegExp does the following transformations:
            // \` -> \`
            // \\` -> \\\`
            // \${ -> \${
            // \\${ -> \\\${
            .replaceAll(
              new RegExp(`${evenNumOfBackslashesRegExp.source}(\`|\\\${)`, 'g'),
              '\\$1',
            );

          // `...${'...$'}{...`
          //           ^^^^
          if (
            nextCharacterIsOpeningCurlyBrace &&
            endsWithUnescapedDollarSign(escapedValue)
          ) {
            escapedValue = escapedValue.replaceAll(/\$$/g, '\\$');
          }

          if (escapedValue.length !== 0) {
            nextCharacterIsOpeningCurlyBrace = escapedValue.startsWith('{');
          }

          fixers.push(fixer => [fixer.replaceText(literal, escapedValue)]);
        } else if (templateLiteral) {
          // Since we iterate from the last expression to the first,
          // a subsequent expression can tell the current expression
          // that it starts with {.
          //
          // `... ${`... $`}${'{...'} ...`
          //             ^     ^ subsequent expression starts with {
          //             current expression ends with a dollar sign,
          //             so '$' + '{' === '${' (bad news for us).
          //             Let's escape the dollar sign at the end.
          if (
            nextCharacterIsOpeningCurlyBrace &&
            endsWithUnescapedDollarSign(
              templateLiteral.quasis[templateLiteral.quasis.length - 1].value
                .raw,
            )
          ) {
            fixers.push(fixer => [
              fixer.replaceTextRange(
                [templateLiteral.range[1] - 2, templateLiteral.range[1] - 2],
                '\\',
              ),
            ]);
          }
          if (
            templateLiteral.quasis.length === 1 &&
            templateLiteral.quasis[0].value.raw.length !== 0
          ) {
            nextCharacterIsOpeningCurlyBrace =
              templateLiteral.quasis[0].value.raw.startsWith('{');
          }

          // Remove the beginning and trailing backtick characters.
          fixers.push(fixer => [
            fixer.removeRange([
              templateLiteral.range[0],
              templateLiteral.range[0] + 1,
            ]),
            fixer.removeRange([
              templateLiteral.range[1] - 1,
              templateLiteral.range[1],
            ]),
          ]);
        } else {
          nextCharacterIsOpeningCurlyBrace = false;
        }

        // `... $${'{...'} ...`
        //      ^^^^^
        if (
          nextCharacterIsOpeningCurlyBrace &&
          endsWithUnescapedDollarSign(prevQuasi.value.raw)
        ) {
          fixers.push(fixer => [
            fixer.replaceTextRange(
              [prevQuasi.range[1] - 3, prevQuasi.range[1] - 2],
              '\\$',
            ),
          ]);
        }

        const warnLocStart = prevQuasi.range[1] - 2;
        const warnLocEnd = nextQuasi.range[0] + 1;
        reportDescriptors.push({
          loc: rangeToLoc(context.sourceCode, [warnLocStart, warnLocEnd]),
          messageId: 'noUnnecessaryTemplateExpression',
          fix(fixer): TSESLint.RuleFix[] {
            return [
              // Remove the quasis' parts that are related to the current expression.
              fixer.removeRange([warnLocStart, interpolation.range[0]]),
              fixer.removeRange([interpolation.range[1], warnLocEnd]),

              ...fixers.flatMap(cb => cb(fixer)),
            ];
          },
        });
      }
      return reportDescriptors;
    }

    return {
      TemplateLiteral(node: TSESTree.TemplateLiteral): void {
        if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
          return;
        }
        if (
          isTrivialInterpolation(node) &&
          !hasCommentsBetweenQuasi(node.quasis[0], node.quasis[1])
        ) {
          const { constraintType } = getConstraintInfo(
            checker,
            services.getTypeAtLocation(node.expressions[0]),
          );
          if (constraintType && isStringLike(constraintType)) {
            reportSingleInterpolation(node);
            return;
          }
        }

        const infos = getInterpolationInfos(node).filter(
          isUnnecessaryValueInterpolation,
        );

        for (const reportDescriptor of getReportDescriptors(infos)) {
          context.report(reportDescriptor);
        }
      },
      TSTemplateLiteralType(node: TSESTree.TSTemplateLiteralType): void {
        if (
          isTrivialInterpolation(node) &&
          !hasCommentsBetweenQuasi(node.quasis[0], node.quasis[1])
        ) {
          const { constraintType, isTypeParameter } = getConstraintInfo(
            checker,
            services.getTypeAtLocation(node.types[0]),
          );

          if (
            constraintType &&
            !isTypeParameter &&
            isStringLike(constraintType) &&
            !isEnumMemberType(constraintType)
          ) {
            reportSingleInterpolation(node);
            return;
          }
        }

        const infos = getInterpolationInfos(node).filter(
          isUnnecessaryTypeInterpolation,
        );

        for (const reportDescriptor of getReportDescriptors(infos)) {
          context.report(reportDescriptor);
        }
      },
    };
  }

endsWithUnescapedDollarSign(str: string): boolean

Parameters:

  • str string

Returns: boolean

Calls:

  • new RegExp(${evenNumOfBackslashesRegExp.source}\$$).test
Code
function endsWithUnescapedDollarSign(str: string): boolean {
  return new RegExp(`${evenNumOfBackslashesRegExp.source}\\$$`).test(str);
}

isWhitespace(x: string): boolean

Parameters:

  • x string

Returns: boolean

Calls:

  • /^\s*$/.test

Internal Comments:

// allow empty string too since we went to allow
// `      ${''}
// `; (x2)
// (x2)
// in addition to
// `${'        '}

Code
function isWhitespace(x: string): boolean {
  // allow empty string too since we went to allow
  // `      ${''}
  // `;
  //
  // in addition to
  // `${'        '}
  // `;
  //
  return /^\s*$/.test(x);
}

startsWithNewLine(x: string): boolean

Parameters:

  • x string

Returns: boolean

Calls:

  • ASTUtils.LINEBREAK_MATCHER.exec
Code
function startsWithNewLine(x: string): boolean {
  return ASTUtils.LINEBREAK_MATCHER.exec(x)?.index === 0;
}

Internal helpers

Declared inside another function in this file.

isEnumMemberType(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • tsutils.typeConstituents(type).some
  • t.getSymbol
  • ts.isEnumMember
Code
function isEnumMemberType(type: ts.Type): boolean {
      return tsutils.typeConstituents(type).some(t => {
        const symbol = t.getSymbol();
        return !!(
          symbol?.valueDeclaration && ts.isEnumMember(symbol.valueDeclaration)
        );
      });
    }

isTemplateLiteral(node: TSESTree.Node): node is TSESTree.TemplateLiteral

Parameters:

  • node TSESTree.Node

Returns: node is TSESTree.TemplateLiteral

Code
function isTemplateLiteral(
      node: TSESTree.Node,
    ): node is TSESTree.TemplateLiteral {
      return node.type === AST_NODE_TYPES.TemplateLiteral;
    }

isInfinityIdentifier(node: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node

Returns: boolean

Code
function isInfinityIdentifier(node: TSESTree.Node): boolean {
      return (
        node.type === AST_NODE_TYPES.Identifier && node.name === 'Infinity'
      );
    }

isNaNIdentifier(node: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node

Returns: boolean

Code
function isNaNIdentifier(node: TSESTree.Node): boolean {
      return node.type === AST_NODE_TYPES.Identifier && node.name === 'NaN';
    }

isFixableIdentifier(node: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node

Returns: boolean

Calls:

  • isUndefinedIdentifier (from ../util)
  • isInfinityIdentifier
  • isNaNIdentifier
Code
function isFixableIdentifier(node: TSESTree.Node): boolean {
      return (
        isUndefinedIdentifier(node) ||
        isInfinityIdentifier(node) ||
        isNaNIdentifier(node)
      );
    }

hasCommentsBetweenQuasi(startQuasi: TSESTree.TemplateElement, endQuasi: TSESTree.TemplateElement): boolean

Parameters:

  • startQuasi TSESTree.TemplateElement
  • endQuasi TSESTree.TemplateElement

Returns: boolean

Calls:

  • nullThrows (from ../util)
  • context.sourceCode.getTokenByRangeStart
  • NullThrowsReasons.MissingToken
  • context.sourceCode.commentsExistBetween
Code
function hasCommentsBetweenQuasi(
      startQuasi: TSESTree.TemplateElement,
      endQuasi: TSESTree.TemplateElement,
    ): boolean {
      const startToken = nullThrows(
        context.sourceCode.getTokenByRangeStart(startQuasi.range[0]),
        NullThrowsReasons.MissingToken('`${', 'opening template literal'),
      );
      const endToken = nullThrows(
        context.sourceCode.getTokenByRangeStart(endQuasi.range[0]),
        NullThrowsReasons.MissingToken('}', 'closing template literal'),
      );

      return context.sourceCode.commentsExistBetween(startToken, endToken);
    }

isTrivialInterpolation(node: TSESTree.TemplateLiteral | TSESTree.TST…): boolean

Parameters:

  • node TSESTree.TemplateLiteral | TSESTree.TSTemplateLiteralType

Returns: boolean

Code
function isTrivialInterpolation(
      node: TSESTree.TemplateLiteral | TSESTree.TSTemplateLiteralType,
    ) {
      return (
        node.quasis.length === 2 &&
        node.quasis[0].value.raw === '' &&
        node.quasis[1].value.raw === ''
      );
    }

getInterpolations(node: TemplateLiteralTypeOrValue): TSESTree.Expression[] | TSESTree.TypeNode[]

Parameters:

  • node TemplateLiteralTypeOrValue

Returns: TSESTree.Expression[] | TSESTree.TypeNode[]

Code
function getInterpolations(
      node: TemplateLiteralTypeOrValue,
    ): TSESTree.Expression[] | TSESTree.TypeNode[] {
      if (node.type === AST_NODE_TYPES.TemplateLiteral) {
        return node.expressions;
      }
      return node.types;
    }

getInterpolationInfos(node: TemplateLiteralTypeOrValue): InterpolationInfo[]

Parameters:

  • node TemplateLiteralTypeOrValue

Returns: InterpolationInfo[]

Calls:

  • getInterpolations(node).map
Code
function getInterpolationInfos(
      node: TemplateLiteralTypeOrValue,
    ): InterpolationInfo[] {
      return getInterpolations(node).map((interpolation, index) => ({
        interpolation,
        nextQuasi: node.quasis[index + 1],
        prevQuasi: node.quasis[index],
      }));
    }

getLiteral(node: TSESTree.Expression | TSESTree.TypeNode): TSESTree.Literal | null

Parameters:

  • node TSESTree.Expression | TSESTree.TypeNode

Returns: TSESTree.Literal | null

Calls:

  • isLiteral
Code
function getLiteral(
      node: TSESTree.Expression | TSESTree.TypeNode,
    ): TSESTree.Literal | null {
      const maybeLiteral =
        node.type === AST_NODE_TYPES.TSLiteralType ? node.literal : node;
      return isLiteral(maybeLiteral) ? maybeLiteral : null;
    }

getTemplateLiteral(node: TSESTree.Expression | TSESTree.TypeNode): TSESTree.TemplateLiteral | null

Parameters:

  • node TSESTree.Expression | TSESTree.TypeNode

Returns: TSESTree.TemplateLiteral | null

Calls:

  • isTemplateLiteral
Code
function getTemplateLiteral(
      node: TSESTree.Expression | TSESTree.TypeNode,
    ): TSESTree.TemplateLiteral | null {
      const maybeTemplateLiteral =
        node.type === AST_NODE_TYPES.TSLiteralType ? node.literal : node;
      return isTemplateLiteral(maybeTemplateLiteral)
        ? maybeTemplateLiteral
        : null;
    }

reportSingleInterpolation(node: TemplateLiteralTypeOrValue): void

Parameters:

  • node TemplateLiteralTypeOrValue

Returns: void

Calls:

  • getInterpolations
  • context.report
  • rangeToLoc (from ../util/rangeToLoc)
  • getMovedNodeCode (from ../util)
  • fixer.replaceText
Code
function reportSingleInterpolation(node: TemplateLiteralTypeOrValue): void {
      const interpolations = getInterpolations(node);
      context.report({
        loc: rangeToLoc(context.sourceCode, [
          interpolations[0].range[0] - 2,
          interpolations[0].range[1] + 1,
        ]),
        messageId: 'noUnnecessaryTemplateExpression',
        fix(fixer): TSESLint.RuleFix | null {
          const wrappingCode = getMovedNodeCode({
            destinationNode: node,
            nodeToMove: interpolations[0],
            sourceCode: context.sourceCode,
          });

          return fixer.replaceText(node, wrappingCode);
        },
      });
    }

isUnnecessaryValueInterpolation({ interpolation, nextQuasi, pre…: InterpolationInfo): boolean

Parameters:

  • { interpolation, nextQuasi, prevQuasi, } InterpolationInfo

Returns: boolean

Calls:

  • hasCommentsBetweenQuasi
  • isFixableIdentifier
  • isLiteral
  • startsWithNewLine
  • isWhitespace
  • isTemplateLiteral

Internal Comments:

// allow trailing whitespace literal (x2)

Code
function isUnnecessaryValueInterpolation({
      interpolation,
      nextQuasi,
      prevQuasi,
    }: InterpolationInfo): boolean {
      if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) {
        return false;
      }

      if (isFixableIdentifier(interpolation)) {
        return true;
      }

      if (isLiteral(interpolation)) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            typeof interpolation.value === 'string' &&
            isWhitespace(interpolation.value)
          );
        }
        return true;
      }

      if (isTemplateLiteral(interpolation)) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            interpolation.quasis.length === 1 &&
            isWhitespace(interpolation.quasis[0].value.raw)
          );
        }
        return true;
      }

      return false;
    }

isUnnecessaryTypeInterpolation({ interpolation, nextQuasi, pre…: InterpolationInfo): boolean

Parameters:

  • { interpolation, nextQuasi, prevQuasi, } InterpolationInfo

Returns: boolean

Calls:

  • hasCommentsBetweenQuasi
  • getLiteral
  • startsWithNewLine
  • isWhitespace
  • getTemplateLiteral

Internal Comments:

// allow trailing whitespace literal (x2)

Code
function isUnnecessaryTypeInterpolation({
      interpolation,
      nextQuasi,
      prevQuasi,
    }: InterpolationInfo): boolean {
      if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) {
        return false;
      }

      const literal = getLiteral(interpolation);
      if (literal) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            typeof literal.value === 'string' && isWhitespace(literal.value)
          );
        }
        return true;
      }

      if (
        interpolation.type === AST_NODE_TYPES.TSNullKeyword ||
        interpolation.type === AST_NODE_TYPES.TSUndefinedKeyword
      ) {
        return true;
      }

      const templateLiteral = getTemplateLiteral(interpolation);
      if (templateLiteral) {
        // allow trailing whitespace literal
        if (startsWithNewLine(nextQuasi.value.raw)) {
          return !(
            templateLiteral.quasis.length === 1 &&
            isWhitespace(templateLiteral.quasis[0].value.raw)
          );
        }
        return true;
      }

      return false;
    }

getReportDescriptors(infos: InterpolationInfo[]): TSESLint.ReportDescriptor<MessageId>[]

Parameters:

  • infos InterpolationInfo[]

Returns: TSESLint.ReportDescriptor<MessageId>[]

Calls:

  • [...infos].reverse
  • nextQuasi.value.raw.startsWith
  • getLiteral
  • getTemplateLiteral
  • ( typeof literal.value === 'string' ? // The value is already a string, so we're removing quotes: // "'value'" -> "value" literal.raw.slice(1, -1) : // The value may be one of number | bigint | boolean | RegExp | null. // In regular expressions, we escape every backslash String(literal.value).replaceAll('\\', '\\\\') ) // The string or RegExp may contain or ${. // We want both of these to be escaped in the final template expression. // // A pair of backslashes means "escaped backslash", so backslashes // from this pair won't escape or ${. Therefore, to escape these // sequences in the resulting template expression, we need to escape // all sequences that are preceded by an even number of backslashes. // // This RegExp does the following transformations: // \ -> ` // \-> \\\ // \${ -> \${ // \${ -> \\${ .replaceAll`
  • literal.raw.slice
  • String(literal.value).replaceAll
  • endsWithUnescapedDollarSign
  • escapedValue.replaceAll
  • escapedValue.startsWith
  • fixers.push
  • fixer.replaceText
  • fixer.replaceTextRange
  • templateLiteral.quasis[0].value.raw.startsWith
  • fixer.removeRange
  • reportDescriptors.push
  • rangeToLoc (from ../util/rangeToLoc)
  • fixers.flatMap
  • cb

Internal Comments:

// "'va`lue'" -> "va`lue" (x4)
// In regular expressions, we escape every backslash (x4)
// `...${'...$'}{...`
//           ^^^^
// Since we iterate from the last expression to the first,
// a subsequent expression can tell the current expression
// that it starts with {.
//
// `... ${`... $`}${'{...'} ...`
//             ^     ^ subsequent expression starts with {
//             current expression ends with a dollar sign,
//             so '$' + '{' === '${' (bad news for us).
//             Let's escape the dollar sign at the end.
// Remove the beginning and trailing backtick characters. (x4)
// `... $${'{...'} ...`
//      ^^^^^
// Remove the quasis' parts that are related to the current expression. (x3)

Code
function getReportDescriptors(
      infos: InterpolationInfo[],
    ): TSESLint.ReportDescriptor<MessageId>[] {
      let nextCharacterIsOpeningCurlyBrace = false;
      const reportDescriptors: TSESLint.ReportDescriptor<MessageId>[] = [];
      const reversedInfos = [...infos].reverse();
      for (const { interpolation, nextQuasi, prevQuasi } of reversedInfos) {
        const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] =
          [];

        if (nextQuasi.value.raw !== '') {
          nextCharacterIsOpeningCurlyBrace =
            nextQuasi.value.raw.startsWith('{');
        }

        const literal = getLiteral(interpolation);
        const templateLiteral = getTemplateLiteral(interpolation);
        if (literal) {
          let escapedValue = (
            typeof literal.value === 'string'
              ? // The value is already a string, so we're removing quotes:
                // "'va`lue'" -> "va`lue"
                literal.raw.slice(1, -1)
              : // The value may be one of number | bigint | boolean | RegExp | null.
                // In regular expressions, we escape every backslash
                String(literal.value).replaceAll('\\', '\\\\')
          )
            // The string or RegExp may contain ` or ${.
            // We want both of these to be escaped in the final template expression.
            //
            // A pair of backslashes means "escaped backslash", so backslashes
            // from this pair won't escape ` or ${. Therefore, to escape these
            // sequences in the resulting template expression, we need to escape
            // all sequences that are preceded by an even number of backslashes.
            //
            // This RegExp does the following transformations:
            // \` -> \`
            // \\` -> \\\`
            // \${ -> \${
            // \\${ -> \\\${
            .replaceAll(
              new RegExp(`${evenNumOfBackslashesRegExp.source}(\`|\\\${)`, 'g'),
              '\\$1',
            );

          // `...${'...$'}{...`
          //           ^^^^
          if (
            nextCharacterIsOpeningCurlyBrace &&
            endsWithUnescapedDollarSign(escapedValue)
          ) {
            escapedValue = escapedValue.replaceAll(/\$$/g, '\\$');
          }

          if (escapedValue.length !== 0) {
            nextCharacterIsOpeningCurlyBrace = escapedValue.startsWith('{');
          }

          fixers.push(fixer => [fixer.replaceText(literal, escapedValue)]);
        } else if (templateLiteral) {
          // Since we iterate from the last expression to the first,
          // a subsequent expression can tell the current expression
          // that it starts with {.
          //
          // `... ${`... $`}${'{...'} ...`
          //             ^     ^ subsequent expression starts with {
          //             current expression ends with a dollar sign,
          //             so '$' + '{' === '${' (bad news for us).
          //             Let's escape the dollar sign at the end.
          if (
            nextCharacterIsOpeningCurlyBrace &&
            endsWithUnescapedDollarSign(
              templateLiteral.quasis[templateLiteral.quasis.length - 1].value
                .raw,
            )
          ) {
            fixers.push(fixer => [
              fixer.replaceTextRange(
                [templateLiteral.range[1] - 2, templateLiteral.range[1] - 2],
                '\\',
              ),
            ]);
          }
          if (
            templateLiteral.quasis.length === 1 &&
            templateLiteral.quasis[0].value.raw.length !== 0
          ) {
            nextCharacterIsOpeningCurlyBrace =
              templateLiteral.quasis[0].value.raw.startsWith('{');
          }

          // Remove the beginning and trailing backtick characters.
          fixers.push(fixer => [
            fixer.removeRange([
              templateLiteral.range[0],
              templateLiteral.range[0] + 1,
            ]),
            fixer.removeRange([
              templateLiteral.range[1] - 1,
              templateLiteral.range[1],
            ]),
          ]);
        } else {
          nextCharacterIsOpeningCurlyBrace = false;
        }

        // `... $${'{...'} ...`
        //      ^^^^^
        if (
          nextCharacterIsOpeningCurlyBrace &&
          endsWithUnescapedDollarSign(prevQuasi.value.raw)
        ) {
          fixers.push(fixer => [
            fixer.replaceTextRange(
              [prevQuasi.range[1] - 3, prevQuasi.range[1] - 2],
              '\\$',
            ),
          ]);
        }

        const warnLocStart = prevQuasi.range[1] - 2;
        const warnLocEnd = nextQuasi.range[0] + 1;
        reportDescriptors.push({
          loc: rangeToLoc(context.sourceCode, [warnLocStart, warnLocEnd]),
          messageId: 'noUnnecessaryTemplateExpression',
          fix(fixer): TSESLint.RuleFix[] {
            return [
              // Remove the quasis' parts that are related to the current expression.
              fixer.removeRange([warnLocStart, interpolation.range[0]]),
              fixer.removeRange([interpolation.range[1], warnLocEnd]),

              ...fixers.flatMap(cb => cb(fixer)),
            ];
          },
        });
      }
      return reportDescriptors;
    }

Interfaces

InterpolationInfo

Interface Code
interface InterpolationInfo {
  interpolation: TSESTree.Expression | TSESTree.TypeNode;
  prevQuasi: TSESTree.TemplateElement;
  nextQuasi: TSESTree.TemplateElement;
}

Properties

Name Type Optional Description
interpolation TSESTree.Expression \| TSESTree.TypeNode not shown
prevQuasi TSESTree.TemplateElement not shown
nextQuasi TSESTree.TemplateElement not shown

Type Aliases

MessageId

type MessageId = 'noUnnecessaryTemplateExpression';

TemplateLiteralTypeOrValue

type TemplateLiteralTypeOrValue = TSESTree.TemplateLiteral | TSESTree.TSTemplateLiteralType;

Generated by Syntax Scribe