routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
import {isStringLiteral, isDirective} from './ast/index.js';
|
||||
import {fixSpaceAroundKeyword, replaceTemplateElement} from './fix/index.js';
|
||||
import isJestInlineSnapshot from './shared/is-jest-inline-snapshot.js';
|
||||
|
||||
const MESSAGE_ID = 'prefer-string-raw';
|
||||
const messages = {
|
||||
[MESSAGE_ID]: '`String.raw` should be used to avoid escaping `\\`.',
|
||||
};
|
||||
|
||||
const BACKSLASH = '\\';
|
||||
|
||||
function unescapeBackslash(text, quote = '') {
|
||||
return text.replaceAll(new RegExp(String.raw`\\(?<escapedCharacter>[\\${quote}])`, 'g'), '$<escapedCharacter>');
|
||||
}
|
||||
|
||||
/**
|
||||
Check if a string literal is restricted to replace with a `String.raw`
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
function isStringRawRestricted(node) {
|
||||
const {parent} = node;
|
||||
const {type} = parent;
|
||||
return (
|
||||
// Directive
|
||||
isDirective(parent)
|
||||
// Property, method, or accessor key (only non-computed)
|
||||
|| (
|
||||
(
|
||||
type === 'Property'
|
||||
|| type === 'PropertyDefinition'
|
||||
|| type === 'MethodDefinition'
|
||||
|| type === 'AccessorProperty'
|
||||
)
|
||||
&& !parent.computed && parent.key === node
|
||||
)
|
||||
// Property, method, or accessor key (always)
|
||||
|| (
|
||||
(
|
||||
type === 'TSAbstractPropertyDefinition'
|
||||
|| type === 'TSAbstractMethodDefinition'
|
||||
|| type === 'TSAbstractAccessorProperty'
|
||||
|| type === 'TSPropertySignature'
|
||||
)
|
||||
&& parent.key === node
|
||||
)
|
||||
// Module source
|
||||
|| (
|
||||
(
|
||||
type === 'ImportDeclaration'
|
||||
|| type === 'ExportNamedDeclaration'
|
||||
|| type === 'ExportAllDeclaration'
|
||||
) && parent.source === node
|
||||
)
|
||||
// Import attribute key and value
|
||||
|| (type === 'ImportAttribute' && (parent.key === node || parent.value === node))
|
||||
// Module specifier
|
||||
|| (type === 'ImportSpecifier' && parent.imported === node)
|
||||
|| (type === 'ExportSpecifier' && (parent.local === node || parent.exported === node))
|
||||
|| (type === 'ExportAllDeclaration' && parent.exported === node)
|
||||
// JSX attribute value
|
||||
|| (type === 'JSXAttribute' && parent.value === node)
|
||||
// (TypeScript) Enum member key and value
|
||||
|| (type === 'TSEnumMember' && (parent.initializer === node || parent.id === node))
|
||||
// (TypeScript) Module declaration
|
||||
|| (type === 'TSModuleDeclaration' && parent.id === node)
|
||||
// (TypeScript) CommonJS module reference
|
||||
|| (type === 'TSExternalModuleReference' && parent.expression === node)
|
||||
// (TypeScript) Literal type
|
||||
|| (type === 'TSLiteralType' && parent.literal === node)
|
||||
// (TypeScript) Import type
|
||||
|| (type === 'TSImportType' && parent.source === node)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {import('eslint').Rule.RuleContext} context */
|
||||
const create = context => {
|
||||
context.on('Literal', node => {
|
||||
if (!isStringLiteral(node) || isStringRawRestricted(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);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** @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,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user