You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
9.9 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const ast_utils_1 = require("../utils/ast-utils");
const compat_1 = require("../utils/compat");
exports.default = (0, utils_1.createRule)('prefer-class-directive', {
meta: {
docs: {
description: 'require class directives instead of ternary expressions',
category: 'Stylistic Issues',
recommended: false,
conflictWithPrettier: false
},
fixable: 'code',
schema: [],
messages: {
unexpected: 'Unexpected class using the ternary operator.'
},
type: 'suggestion'
},
create(context) {
const sourceCode = (0, compat_1.getSourceCode)(context);
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;
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 = (0, ast_utils_1.getStringIfConstant)(e);
if (str == null) {
return false;
}
result.set(key, str);
}
return true;
}
}
function exprToString({ node, not }) {
let text = sourceCode.text.slice(...node.range);
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 ((0, ast_utils_1.needParentheses)(node, 'not')) {
text = `(${text})`;
}
text = `!${text}`;
}
return text;
}
function getStrings(node) {
if (node.type === 'SvelteLiteral') {
return [node.value];
}
if (node.expression.type === 'ConditionalExpression') {
const values = parseConditionalExpression(node.expression);
if (values == null) {
return null;
}
return [...values.values()];
}
const str = (0, ast_utils_1.getStringIfConstant)(node.expression);
if (str == null) {
return null;
}
return [str];
}
function endsWithNonWord(node, index) {
for (let i = index; i >= 0; i--) {
const valueNode = node.value[i];
const strings = getStrings(valueNode);
if (strings == null) {
return false;
}
for (const str of strings) {
if (str) {
return !str[str.length - 1].trim();
}
}
}
return true;
}
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) {
return false;
}
for (const str of strings) {
if (str) {
return !str[0].trim();
}
}
}
return true;
}
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()) {
beforeAttrValues.pop();
fixesBuffer.push(fixer.remove(valueNode));
continue;
}
if (valueNode.value.trimEnd() !== valueNode.value) {
fixesBuffer.push(fixer.replaceText(valueNode, valueNode.value.trimEnd()));
}
}
break;
}
while ((valueNode = afterAttrValues[0])) {
if (valueNode.type === 'SvelteLiteral') {
if (!valueNode.value.trim()) {
afterAttrValues.shift();
fixesBuffer.push(fixer.remove(valueNode));
continue;
}
if (valueNode.value.trimStart() !== valueNode.value) {
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(' ')}`);
}
}
});
}
function verify(node, index, attr) {
if (node.expression.type !== 'ConditionalExpression') {
return;
}
const map = parseConditionalExpression(node.expression);
if (map == null) {
return;
}
if (map.size > 2) {
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())) {
canTransform = false;
break;
}
if ((className[0].trim() && prevIsWord) ||
(className[className.length - 1].trim() && nextIsWord)) {
canTransform = false;
break;
}
}
else {
if (prevIsWord && nextIsWord) {
canTransform = false;
break;
}
}
}
if (!canTransform) {
return;
}
report(node, map, attr);
}
return {
'SvelteStartTag > SvelteAttribute'(node) {
if (!(0, ast_utils_1.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);
}
}
};
}
});