Skip to content

Commit

Permalink
better escaping!
Browse files Browse the repository at this point in the history
  • Loading branch information
auvred committed Mar 30, 2024
1 parent 98bc977 commit 4d28bc4
Show file tree
Hide file tree
Showing 2 changed files with 442 additions and 48 deletions.
153 changes: 105 additions & 48 deletions packages/eslint-plugin/src/rules/no-useless-template-literals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export default createRule<[], MessageId>({
return expression.type === AST_NODE_TYPES.Literal;
}

function isTemplateLiteral(expression: TSESTree.Expression): boolean {
function isTemplateLiteral(
expression: TSESTree.Expression,
): expression is TSESTree.TemplateLiteral {
return expression.type === AST_NODE_TYPES.TemplateLiteral;
}

Expand Down Expand Up @@ -114,68 +116,123 @@ export default createRule<[], MessageId>({
return;
}

const fixableExpressions = node.expressions.filter(
expression =>
isLiteral(expression) ||
isTemplateLiteral(expression) ||
isUndefinedIdentifier(expression) ||
isInfinityIdentifier(expression) ||
isNaNIdentifier(expression),
);
const fixableExpressions = node.expressions
.filter(
expression =>
isLiteral(expression) ||
isTemplateLiteral(expression) ||
isUndefinedIdentifier(expression) ||
isInfinityIdentifier(expression) ||
isNaNIdentifier(expression),
)
.reverse();

let nextCharacterIsOpeningCurlyBrace = false;

fixableExpressions.forEach(expression => {
const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] =
[];
const index = node.expressions.indexOf(expression);
const prevQuasi = node.quasis[index];
const nextQuasi = node.quasis[index + 1];

const evenNumOfBackslashesRegExp = /(?<!(?:[^\\]|^)(?:\\\\)*\\)/;

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

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

if (isLiteral(expression)) {
let escapedValue = (
typeof expression.value === 'string'
? expression.raw.slice(1, -1)
: // 1 -> '1'
// /\\/ -> '/\\\\/'
String(expression.value).replace(/\\\\/g, '\\\\\\\\')
).replace(
new RegExp(`${evenNumOfBackslashesRegExp.source}(\`|\\\${)`, 'g'),
'\\$1',
);

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

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

fixers.push(fixer => [fixer.replaceText(expression, escapedValue)]);
} else if (isTemplateLiteral(expression)) {
if (
nextCharacterIsOpeningCurlyBrace &&
endsWithEscapedDollarSign(
expression.quasis[expression.quasis.length - 1].value.raw,
)
) {
fixers.push(fixer => [
fixer.replaceTextRange(
[expression.range[1] - 2, expression.range[1] - 2],
'\\',
),
]);
}
if (
expression.quasis.length === 1 &&
expression.quasis[0].value.raw.length !== 0
) {
nextCharacterIsOpeningCurlyBrace =
expression.quasis[0].value.raw.startsWith('{');
}

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

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

context.report({
node: expression,
messageId: 'noUselessTemplateLiteral',
fix(fixer): TSESLint.RuleFix[] {
const index = node.expressions.indexOf(expression);
const prevQuasi = node.quasis[index];
const nextQuasi = node.quasis[index + 1];

// Remove the quasis' parts that are related to the current expression.
const fixes = [
return [
// Remove the quasis' parts that are related to the current expression.
fixer.removeRange([
prevQuasi.range[1] - 2,
expression.range[0],
]),

fixer.removeRange([
expression.range[1],
nextQuasi.range[0] + 1,
]),
];

if (isLiteral(expression)) {
const escapedValue =
typeof expression.value === 'string'
? // '1' -> 1
// '`' -> \`
// '${}' -> \${}
// '\\' -> \\
expression.raw.slice(1, -1).replace(/([`$])/g, '\\$1')
: // 1 -> 1
// /`/ -> /\`/
// /${}/ -> /\${}/
// /\\/ -> /\\\\/
String(expression.value).replace(/([`$\\])/g, '\\$1');

fixes.push(fixer.replaceText(expression, escapedValue));
} else if (isTemplateLiteral(expression)) {
// Note that some template literals get handled in the previous branch too.
// Remove the beginning and trailing backtick characters.
fixes.push(
fixer.removeRange([
expression.range[0],
expression.range[0] + 1,
]),
fixer.removeRange([
expression.range[1] - 1,
expression.range[1],
]),
);
}

return fixes;
...fixers.flatMap(cb => cb(fixer)),
];
},
});
});
Expand Down

0 comments on commit 4d28bc4

Please sign in to comment.