eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
178 lines (153 loc) • 4.99 kB
JavaScript
import {isStringLiteral, isMemberExpression} from './ast/index.js';
import {
addParenthesesToReturnOrThrowExpression,
fixSpaceAroundKeyword,
removeParentheses,
replaceTemplateElement,
} from './fix/index.js';
import isJestInlineSnapshot from './shared/is-jest-inline-snapshot.js';
import {isOnSameLine, isParenthesized, isStringLiteralRequired} from './utils/index.js';
import needsSemicolon from './utils/needs-semicolon.js';
const MESSAGE_ID = 'prefer-string-raw';
const MESSAGE_ID_UNNECESSARY_STRING_RAW = 'unnecessary-string-raw';
const messages = {
[MESSAGE_ID]: '`String.raw` should be used to avoid escaping `\\`.',
[MESSAGE_ID_UNNECESSARY_STRING_RAW]: 'Using `String.raw` is unnecessary as the string does not contain any `\\`.',
};
const BACKSLASH = '\\';
function unescapeBackslash(text, quote = '') {
return text.replaceAll(new RegExp(String.raw`\\(?<escapedCharacter>[\\${quote}])`, 'g'), '$<escapedCharacter>');
}
function shouldParenthesizeYieldArgument(parent, node, tokenBefore, context) {
return parent.type === 'YieldExpression'
&& !parent.delegate
&& parent.argument === node
&& !isOnSameLine(tokenBefore, node.quasi, context)
&& !isParenthesized(node, context);
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
context.on('Literal', node => {
if (!isStringLiteral(node) || isStringLiteralRequired(node) || isJestInlineSnapshot(node)) {
return;
}
const {sourceCode} = context;
const {raw} = node;
if (
raw.at(-2) === BACKSLASH
|| !raw.includes(BACKSLASH + BACKSLASH)
|| raw.includes('`')
|| raw.includes('${')
|| sourceCode.getLoc(node).start.line !== sourceCode.getLoc(node).end.line
) {
return;
}
const unescaped = unescapeBackslash(raw.slice(1, -1), raw.charAt(0));
if (unescaped !== node.value) {
return;
}
return {
node,
messageId: MESSAGE_ID,
* fix(fixer) {
yield fixSpaceAroundKeyword(fixer, node, context);
yield fixer.replaceText(node, `String.raw\`${unescaped}\``);
},
};
});
context.on('TemplateLiteral', node => {
if (
(node.parent.type === 'TaggedTemplateExpression' && node.parent.quasi === node)
|| node.quasis.every(({value: {cooked, raw}}) => cooked === raw)
|| node.quasis.some(({value: {cooked, raw}}) => cooked.at(-1) === BACKSLASH || unescapeBackslash(raw) !== cooked)
|| isJestInlineSnapshot(node)
) {
return;
}
return {
node,
messageId: MESSAGE_ID,
* fix(fixer) {
yield fixSpaceAroundKeyword(fixer, node, context);
yield fixer.insertTextBefore(node, 'String.raw');
for (const quasis of node.quasis) {
const {cooked, raw} = quasis.value;
if (cooked === raw) {
continue;
}
yield replaceTemplateElement(quasis, cooked, context, fixer);
}
},
};
});
context.on('TaggedTemplateExpression', node => {
const {quasi, tag, parent} = node;
const {sourceCode} = context;
if (
!isMemberExpression(tag, {object: 'String', property: 'raw', optional: false})
|| !sourceCode.isGlobalReference(tag.object)
) {
return;
}
const hasBackslash = quasi.quasis.some(
quasi => quasi.value.raw.includes(BACKSLASH),
);
if (hasBackslash) {
return;
}
const rawQuasi = sourceCode.getText(quasi);
// A line break, including a bare `\r`, is not allowed in a string literal. `\u2028` and `\u2029` are only allowed in one since ES2019, so they are kept in the template literal too.
const suggestion = quasi.expressions.length > 0 || /[\n\r\u{2028}\u{2029}]/u.test(rawQuasi)
? rawQuasi
: `'${rawQuasi.slice(1, -1).replaceAll('\'', () => String.raw`\'`)}'`;
return {
node: tag,
messageId: MESSAGE_ID_UNNECESSARY_STRING_RAW,
* fix(fixer, {abort}) {
// The tag is removed as a whole, a comment inside it would be dropped
if (sourceCode.getCommentsInside(tag).length > 0) {
abort();
}
const tokenBefore = sourceCode.getTokenBefore(node);
if (needsSemicolon(tokenBefore, context, suggestion)) {
yield fixer.insertTextBefore(node, ';');
}
if (
(parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
&& !isOnSameLine(tokenBefore, node.quasi, context)
&& !isParenthesized(node, context)
) {
yield addParenthesesToReturnOrThrowExpression(fixer, parent, context);
}
if (shouldParenthesizeYieldArgument(parent, node, tokenBefore, context)) {
yield fixer.insertTextBefore(node, '(');
yield fixer.insertTextAfter(node, ')');
}
yield fixer.replaceText(node.quasi, suggestion);
yield removeParentheses(node.tag, fixer, context);
yield fixer.remove(node.tag);
},
};
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer using the `String.raw` tag to avoid escaping `\\`.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
languages: [
'js/js',
],
},
};
export default config;