UNPKG

eslint-plugin-svelte

Version:
311 lines (310 loc) 12.3 kB
import { createRule } from '../utils/index.js'; import { getStringIfConstant, isHTMLElementLike, needParentheses } from '../utils/ast-utils.js'; export default createRule('prefer-class-directive', { meta: { docs: { description: 'require class directives instead of ternary expressions', category: 'Stylistic Issues', recommended: false, conflictWithPrettier: false }, fixable: 'code', schema: [ { type: 'object', properties: { prefer: { enum: ['always', 'empty'] } }, additionalProperties: false } ], messages: { unexpected: 'Unexpected class using the ternary operator.' }, type: 'suggestion' }, create(context) { const sourceCode = context.sourceCode; const preferEmpty = context.options[0]?.prefer !== 'always'; /** * Returns a map of expressions and strings from ConditionalExpression. * Returns null if it has an unknown string. */ function parseConditionalExpression(node) { const result = new Map(); if (!processItems({ node: node.test }, node.consequent)) { return null; } if (!processItems({ not: true, node: node.test }, node.alternate)) { return null; } return result; /** Process items */ function processItems(key, e) { if (e.type === 'ConditionalExpression') { const sub = parseConditionalExpression(e); if (sub == null) { return false; } for (const [expr, str] of sub) { result.set({ ...key, chains: expr }, str); } } else { const str = getStringIfConstant(e); if (str == null) { return false; } result.set(key, str); } return true; } } /** * Expr to string */ function exprToString({ node, not }) { let text = sourceCode.text.slice(...node.range); // *Currently not supported. // if (chains) { // if (needParentheses(node, "logical")) { // text = `(${text})` // } // let chainsText = exprToString(chains) // const needParenForChains = // !/^[!(]/u.test(chainsText) && needParentheses(chains.node, "logical") // if (needParenForChains) { // chainsText = `(${chainsText})` // } // text = `${text} && ${chainsText}` // if (not) { // text = `!(${text})` // } // return text // } if (not) { if (node.type === 'BinaryExpression') { if (node.operator === '===' || node.operator === '==' || node.operator === '!==' || node.operator === '!=') { const left = sourceCode.text.slice(...node.left.range); const op = sourceCode.text.slice(node.left.range[1], node.right.range[0]); const right = sourceCode.text.slice(...node.right.range); return `${left}${node.operator === '===' || node.operator === '==' ? op.replace(/[=](={1,2})/g, '!$1') : op.replace(/!(={1,2})/g, '=$1')}${right}`; } } else if (node.type === 'UnaryExpression') { if (node.operator === '!' && node.prefix) { return sourceCode.text.slice(...node.argument.range); } } if (needParentheses(node, 'not')) { text = `(${text})`; } text = `!${text}`; } return text; } /** * Returns all possible strings. */ function getStrings(node) { if (node.type === 'SvelteLiteral') { return [node.value]; } if (node.expression.type === 'ConditionalExpression') { const values = parseConditionalExpression(node.expression); if (values == null) { // unknown return null; } return [...values.values()]; } const str = getStringIfConstant(node.expression); if (str == null) { // unknown return null; } return [str]; } /** * Checks if the last character is a non word. */ function endsWithNonWord(node, index) { for (let i = index; i >= 0; i--) { const valueNode = node.value[i]; const strings = getStrings(valueNode); if (strings == null) { // unknown return false; } for (const str of strings) { if (str) { return !str[str.length - 1].trim(); } } // If the string is empty, check the previous string. } return true; } /** * Checks if the first character is a non word. */ function startsWithNonWord(node, index) { for (let i = index; i < node.value.length; i++) { const valueNode = node.value[i]; const strings = getStrings(valueNode); if (strings == null) { // unknown return false; } for (const str of strings) { if (str) { return !str[0].trim(); } } // If the string is empty, check the previous string. } return true; } /** Report */ function report(node, map, attr) { context.report({ node, messageId: 'unexpected', *fix(fixer) { const classDirectives = []; let space = ' '; for (const [expr, className] of map) { const trimmedClassName = className.trim(); if (trimmedClassName) { classDirectives.push(`class:${trimmedClassName}={${exprToString(expr)}}`); } else { space = className; } } const fixesBuffer = []; const index = attr.value.indexOf(node); const beforeAttrValues = attr.value.slice(0, index); const afterAttrValues = attr.value.slice(index + 1); let valueNode; while ((valueNode = beforeAttrValues[beforeAttrValues.length - 1])) { if (valueNode.type === 'SvelteLiteral') { if (!valueNode.value.trim()) { // Before spaces beforeAttrValues.pop(); fixesBuffer.push(fixer.remove(valueNode)); continue; } if (valueNode.value.trimEnd() !== valueNode.value) { // Before spaces fixesBuffer.push(fixer.replaceText(valueNode, valueNode.value.trimEnd())); } } break; } while ((valueNode = afterAttrValues[0])) { if (valueNode.type === 'SvelteLiteral') { if (!valueNode.value.trim()) { // After spaces afterAttrValues.shift(); fixesBuffer.push(fixer.remove(valueNode)); continue; } if (valueNode.value.trimStart() !== valueNode.value) { // After spaces fixesBuffer.push(fixer.replaceText(valueNode, valueNode.value.trimStart())); } } break; } if (!beforeAttrValues.length && !afterAttrValues.length) { yield fixer.replaceText(attr, classDirectives.join(' ')); } else { yield* fixesBuffer; if (beforeAttrValues.length && afterAttrValues.length) { yield fixer.replaceText(node, space || ' '); } else { yield fixer.remove(node); } yield fixer.insertTextAfterRange([attr.range[1], attr.range[1]], ` ${classDirectives.join(' ')}`); } } }); } /** verify */ function verify(node, index, attr) { if (node.expression.type !== 'ConditionalExpression') { return; } const map = parseConditionalExpression(node.expression); if (map == null) { // has unknown return; } if (map.size > 2) { // It's too complicated. return; } if (preferEmpty && [...map.values()].every((x) => x.trim())) { // We prefer directives when there's an empty string, but they're all not empty return; } const prevIsWord = !startsWithNonWord(attr, index + 1); const nextIsWord = !endsWithNonWord(attr, index - 1); let canTransform = true; for (const className of map.values()) { if (className) { if (!/^[\w-]*$/u.test(className.trim())) { // Cannot be transformed to an attribute. canTransform = false; break; } if ((className[0].trim() && prevIsWord) || (className[className.length - 1].trim() && nextIsWord)) { // The previous or next may be connected to this element. canTransform = false; break; } } else if (prevIsWord && nextIsWord) { // The previous and next may be connected. canTransform = false; break; } } if (!canTransform) { return; } report(node, map, attr); } return { 'SvelteStartTag > SvelteAttribute'(node) { if (!isHTMLElementLike(node.parent.parent) || node.key.name !== 'class') { return; } for (let index = 0; index < node.value.length; index++) { const valueElement = node.value[index]; if (valueElement.type !== 'SvelteMustacheTag') { continue; } verify(valueElement, index, node); } } }; } });