UNPKG

eslint-plugin-unicorn

Version:
201 lines (175 loc) 4.95 kB
import stripIndent from 'strip-indent'; import indentString from 'indent-string'; import {replaceTemplateElement} from './fix/index.js'; import {isTaggedTemplateLiteral} from './ast/index.js'; import {isNodeMatches} from './utils/index.js'; import isJestInlineSnapshot from './shared/is-jest-inline-snapshot.js'; const MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE = 'template-indent'; const messages = { [MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE]: 'Templates should be properly indented.', }; /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const {sourceCode} = context; const options = {...context.options[0]}; options.comments = options.comments.map(comment => comment.toLowerCase()); const checked = new WeakSet(); /** @param {import('@babel/core').types.TemplateLiteral} node */ const getProblem = node => { if (node.type !== 'TemplateLiteral' || checked.has(node)) { return; } checked.add(node); const delimiter = '__PLACEHOLDER__' + Math.random(); const joined = node.quasis .map(quasi => { const untrimmedText = sourceCode.getText(quasi); return untrimmedText.slice(1, quasi.tail ? -1 : -2); }) .join(delimiter); const eolMatch = joined.match(/\r?\n/); if (!eolMatch) { return; } const eol = eolMatch[0]; const startLine = sourceCode.lines[sourceCode.getLoc(node).start.line - 1]; const marginMatch = startLine.match(/^(\s*)\S/); const parentMargin = marginMatch ? marginMatch[1] : ''; let indent; if (typeof options.indent === 'string') { indent = options.indent; } else if (typeof options.indent === 'number') { indent = ' '.repeat(options.indent); } else { const tabs = parentMargin.startsWith('\t'); indent = tabs ? '\t' : ' '; } const dedented = stripIndent(joined); const trimmed = dedented.replaceAll(new RegExp(`^${eol}|${eol}[ \t]*$`, 'g'), ''); const fixed = eol + indentString(trimmed, 1, {indent: parentMargin + indent}) + eol + parentMargin; if (fixed === joined) { return; } return { node, messageId: MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE, fix: fixer => fixed .split(delimiter) .map((replacement, index) => replaceTemplateElement(node.quasis[index], replacement, context, fixer)), }; }; const shouldIndent = node => { if (options.comments.length > 0) { const previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); if (previousToken?.type === 'Block' && options.comments.includes(previousToken.value.trim().toLowerCase())) { return true; } } if (isJestInlineSnapshot(node)) { return true; } if ( options.tags.length > 0 && isTaggedTemplateLiteral(node, options.tags) ) { return true; } if ( options.functions.length > 0 && node.parent.type === 'CallExpression' && node.parent.arguments.includes(node) && isNodeMatches(node.parent.callee, options.functions) ) { return true; } return false; }; context.on('TemplateLiteral', /** @param {import('@babel/core').types.TemplateLiteral} node */ node => { if (!shouldIndent(node)) { return; } return getProblem(node); }); context.on(options.selectors, /** @param {import('@babel/core').types.TemplateLiteral} node */ node => getProblem(node)); }; /** @type {import('json-schema').JSONSchema7[]} */ const schema = [ { type: 'object', additionalProperties: false, properties: { indent: { oneOf: [ { type: 'string', pattern: /^\s+$/.source, description: 'Whitespace string to use as indentation.', }, { type: 'integer', minimum: 1, description: 'Number of spaces to use as indentation.', }, ], description: 'The indentation to use inside the template literal.', }, tags: { type: 'array', uniqueItems: true, items: { type: 'string', }, description: 'Tagged template names to check.', }, functions: { type: 'array', uniqueItems: true, items: { type: 'string', }, description: 'Function names whose template literal arguments to check.', }, selectors: { type: 'array', uniqueItems: true, items: { type: 'string', }, description: 'AST selectors for template literals to check.', }, comments: { type: 'array', uniqueItems: true, items: { type: 'string', }, description: 'Comment patterns to mark template literals for checking.', }, }, }, ]; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'suggestion', docs: { description: 'Fix whitespace-insensitive template indentation.', recommended: true, }, fixable: 'code', schema, defaultOptions: [{ tags: ['outdent', 'dedent', 'gql', 'sql', 'html', 'styled'], functions: ['dedent', 'stripIndent'], selectors: [], comments: ['HTML', 'indent'], }], messages, }, }; export default config;