📄 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¶
| 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:
contextany
Returns: { TemplateLiteral(node: TSESTree.TemplateLiteral): void; TSTemplateLiteralType(node: TSESTree.TSTemplateLiteralType): void; }
Calls:
getParserServices (from ../util)services.program.getTypeCheckertsutils.typeConstituents(type).somet.getSymbolts.isEnumMemberisNodeOfType (from ../util)isUndefinedIdentifier (from ../util)isInfinityIdentifierisNaNIdentifiernullThrows (from ../util)context.sourceCode.getTokenByRangeStartNullThrowsReasons.MissingTokencontext.sourceCode.commentsExistBetweengetInterpolations(node).mapisLiteralisTemplateLiteralgetInterpolationscontext.reportrangeToLoc (from ../util/rangeToLoc)getMovedNodeCode (from ../util)fixer.replaceTexthasCommentsBetweenQuasiisFixableIdentifierstartsWithNewLineisWhitespacegetLiteralgetTemplateLiteral[...infos].reversenextQuasi.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 containor ${. // 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 escapeor ${. 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.sliceString(literal.value).replaceAllendsWithUnescapedDollarSignescapedValue.replaceAllescapedValue.startsWithfixers.pushfixer.replaceTextRangetemplateLiteral.quasis[0].value.raw.startsWithfixer.removeRangereportDescriptors.pushfixers.flatMapcbisTrivialInterpolationgetConstraintInfo (from ../util)services.getTypeAtLocationisStringLike (from ../util)reportSingleInterpolationgetInterpolationInfos(node).filtergetReportDescriptorsisEnumMemberType
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:
strstring
Returns: boolean
Calls:
new RegExp(${evenNumOfBackslashesRegExp.source}\$$).test
Code
isWhitespace(x: string): boolean¶
Parameters:
xstring
Returns: boolean
Calls:
/^\s*$/.test
Internal Comments:
// allow empty string too since we went to allow
// ` ${''}
// `; (x2)
// (x2)
// in addition to
// `${' '}
Code
startsWithNewLine(x: string): boolean¶
Parameters:
xstring
Returns: boolean
Calls:
ASTUtils.LINEBREAK_MATCHER.exec
Code
Internal helpers¶
Declared inside another function in this file.
isEnumMemberType(type: ts.Type): boolean¶
Parameters:
typets.Type
Returns: boolean
Calls:
tsutils.typeConstituents(type).somet.getSymbolts.isEnumMember
Code
isTemplateLiteral(node: TSESTree.Node): node is TSESTree.TemplateLiteral¶
Parameters:
nodeTSESTree.Node
Returns: node is TSESTree.TemplateLiteral
Code
isInfinityIdentifier(node: TSESTree.Node): boolean¶
Parameters:
nodeTSESTree.Node
Returns: boolean
Code
isNaNIdentifier(node: TSESTree.Node): boolean¶
Parameters:
nodeTSESTree.Node
Returns: boolean
Code
isFixableIdentifier(node: TSESTree.Node): boolean¶
Parameters:
nodeTSESTree.Node
Returns: boolean
Calls:
isUndefinedIdentifier (from ../util)isInfinityIdentifierisNaNIdentifier
Code
hasCommentsBetweenQuasi(startQuasi: TSESTree.TemplateElement, endQuasi: TSESTree.TemplateElement): boolean¶
Parameters:
startQuasiTSESTree.TemplateElementendQuasiTSESTree.TemplateElement
Returns: boolean
Calls:
nullThrows (from ../util)context.sourceCode.getTokenByRangeStartNullThrowsReasons.MissingTokencontext.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:
nodeTSESTree.TemplateLiteral | TSESTree.TSTemplateLiteralType
Returns: boolean
Code
getInterpolations(node: TemplateLiteralTypeOrValue): TSESTree.Expression[] | TSESTree.TypeNode[]¶
Parameters:
nodeTemplateLiteralTypeOrValue
Returns: TSESTree.Expression[] | TSESTree.TypeNode[]
Code
getInterpolationInfos(node: TemplateLiteralTypeOrValue): InterpolationInfo[]¶
Parameters:
nodeTemplateLiteralTypeOrValue
Returns: InterpolationInfo[]
Calls:
getInterpolations(node).map
Code
getLiteral(node: TSESTree.Expression | TSESTree.TypeNode): TSESTree.Literal | null¶
Parameters:
nodeTSESTree.Expression | TSESTree.TypeNode
Returns: TSESTree.Literal | null
Calls:
isLiteral
Code
getTemplateLiteral(node: TSESTree.Expression | TSESTree.TypeNode): TSESTree.TemplateLiteral | null¶
Parameters:
nodeTSESTree.Expression | TSESTree.TypeNode
Returns: TSESTree.TemplateLiteral | null
Calls:
isTemplateLiteral
Code
reportSingleInterpolation(node: TemplateLiteralTypeOrValue): void¶
Parameters:
nodeTemplateLiteralTypeOrValue
Returns: void
Calls:
getInterpolationscontext.reportrangeToLoc (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:
hasCommentsBetweenQuasiisFixableIdentifierisLiteralstartsWithNewLineisWhitespaceisTemplateLiteral
Internal Comments:
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:
hasCommentsBetweenQuasigetLiteralstartsWithNewLineisWhitespacegetTemplateLiteral
Internal Comments:
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:
infosInterpolationInfo[]
Returns: TSESLint.ReportDescriptor<MessageId>[]
Calls:
[...infos].reversenextQuasi.value.raw.startsWithgetLiteralgetTemplateLiteral( 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 containor ${. // 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 escapeor ${. 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.sliceString(literal.value).replaceAllendsWithUnescapedDollarSignescapedValue.replaceAllescapedValue.startsWithfixers.pushfixer.replaceTextfixer.replaceTextRangetemplateLiteral.quasis[0].value.raw.startsWithfixer.removeRangereportDescriptors.pushrangeToLoc (from ../util/rangeToLoc)fixers.flatMapcb
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
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¶
TemplateLiteralTypeOrValue¶
Generated by Syntax Scribe