570 lines
28 KiB
Plaintext
570 lines
28 KiB
Plaintext
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const types_1 = require("@typescript-eslint/types");
|
|
const eslint_utils_1 = require("@eslint-community/eslint-utils");
|
|
const eslint_utils_2 = require("@eslint-community/eslint-utils");
|
|
const utils_1 = require("../utils");
|
|
const ast_utils_1 = require("../utils/ast-utils");
|
|
exports.default = (0, utils_1.createRule)("prefer-object-class-list", {
|
|
meta: {
|
|
docs: {
|
|
description: "require use object instead of ternary expression in `class:list`",
|
|
category: "Stylistic Issues",
|
|
recommended: false,
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
unexpected: "Unexpected class using the ternary operator.",
|
|
},
|
|
fixable: "code",
|
|
type: "suggestion",
|
|
},
|
|
create(context) {
|
|
if (!context.parserServices.isAstro) {
|
|
return {};
|
|
}
|
|
const sourceCode = context.getSourceCode();
|
|
class NewObjectProps {
|
|
constructor() {
|
|
this.props = [];
|
|
}
|
|
toObjectString() {
|
|
return `{${this.toPropsString()}}`;
|
|
}
|
|
fixObject({ fixer, object, }) {
|
|
const closeBrace = sourceCode.getLastToken(object);
|
|
const maybeComma = sourceCode.getTokenBefore(closeBrace);
|
|
let text;
|
|
if ((0, eslint_utils_2.isCommaToken)(maybeComma)) {
|
|
text = this.toPropsString();
|
|
}
|
|
else {
|
|
text = `,${this.toPropsString()}`;
|
|
}
|
|
return fixer.insertTextAfterRange(maybeComma.range, text);
|
|
}
|
|
toPropsString() {
|
|
return `${this.props
|
|
.map(({ key, value }) => `${key}: ${value}`)
|
|
.join(", ")}`;
|
|
}
|
|
}
|
|
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(Object.assign(Object.assign({}, 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 === "TemplateElement") {
|
|
return [node.value.cooked];
|
|
}
|
|
if (node.type === "ConditionalExpression") {
|
|
const values = parseConditionalExpression(node);
|
|
if (values == null) {
|
|
return null;
|
|
}
|
|
return [...values.values()];
|
|
}
|
|
const str = (0, ast_utils_1.getStringIfConstant)(node);
|
|
if (str == null) {
|
|
return null;
|
|
}
|
|
return [str];
|
|
}
|
|
function endsWithSpace(elements) {
|
|
for (let i = elements.length - 1; i >= 0; i--) {
|
|
const valueNode = elements[i];
|
|
const strings = getStrings(valueNode);
|
|
if (strings == null) {
|
|
if (valueNode.type === types_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
const quasiValue = valueNode.quasis[valueNode.quasis.length - 1].value.cooked;
|
|
if (quasiValue && !quasiValue[quasiValue.length - 1].trim()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
let hasEmpty = false;
|
|
for (const str of strings) {
|
|
if (str) {
|
|
if (str[str.length - 1].trim()) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
hasEmpty = true;
|
|
}
|
|
}
|
|
if (!hasEmpty) {
|
|
return true;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function startsWithSpace(elements) {
|
|
for (let i = 0; i < elements.length; i++) {
|
|
const valueNode = elements[i];
|
|
const strings = getStrings(valueNode);
|
|
if (strings == null) {
|
|
if (valueNode.type === types_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
const quasiValue = valueNode.quasis[0].value.cooked;
|
|
if (quasiValue && !quasiValue[0].trim()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
let hasEmpty = false;
|
|
for (const str of strings) {
|
|
if (str) {
|
|
if (str[0].trim()) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
hasEmpty = true;
|
|
}
|
|
}
|
|
if (!hasEmpty) {
|
|
return true;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function report(node, map, state) {
|
|
context.report({
|
|
node,
|
|
messageId: "unexpected",
|
|
*fix(fixer) {
|
|
const classProps = new NewObjectProps();
|
|
let beforeSpaces = "";
|
|
let afterSpaces = "";
|
|
for (const [expr, className] of map) {
|
|
const trimmedClassName = className.trim();
|
|
if (trimmedClassName) {
|
|
classProps.props.push({
|
|
key: JSON.stringify(trimmedClassName),
|
|
value: exprToString(expr),
|
|
});
|
|
}
|
|
else {
|
|
if (!classProps.props.length) {
|
|
beforeSpaces += className;
|
|
}
|
|
else {
|
|
afterSpaces += className;
|
|
}
|
|
}
|
|
}
|
|
yield* state.fixExpression({
|
|
newProps: classProps,
|
|
beforeSpaces,
|
|
afterSpaces,
|
|
node,
|
|
fixer,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
function verifyConditionalExpression(node, state) {
|
|
const map = parseConditionalExpression(node);
|
|
if (map == null) {
|
|
return;
|
|
}
|
|
let canTransform = true;
|
|
for (const className of map.values()) {
|
|
if (className) {
|
|
if ((className[0].trim() && state.beforeIsWord()) ||
|
|
(className[className.length - 1].trim() && state.afterIsWord())) {
|
|
canTransform = false;
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
if (state.beforeIsWord() && state.afterIsWord()) {
|
|
canTransform = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!canTransform) {
|
|
return;
|
|
}
|
|
report(node, map, state);
|
|
}
|
|
function verifyAttr(attr) {
|
|
if ((0, ast_utils_1.getAttributeName)(attr) !== "class:list") {
|
|
return;
|
|
}
|
|
if (!attr.value ||
|
|
attr.value.type !== types_1.AST_NODE_TYPES.JSXExpressionContainer ||
|
|
attr.value.expression.type === types_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
return;
|
|
}
|
|
const expression = attr.value.expression;
|
|
for (const element of extractElements(expression)) {
|
|
visitElementExpression(element.node, {
|
|
beforeIsWord: () => false,
|
|
afterIsWord: () => false,
|
|
*fixArrayElement(data) {
|
|
yield data.fixer.removeRange((0, ast_utils_1.getParenthesizedRange)(data.node, sourceCode));
|
|
if (!element.array) {
|
|
let open, close;
|
|
if (attr.type === "AstroTemplateLiteralAttribute") {
|
|
open = "{[";
|
|
close = "]}";
|
|
}
|
|
else {
|
|
open = "[";
|
|
close = "]";
|
|
}
|
|
yield data.fixer.insertTextBeforeRange(expression.range, open);
|
|
yield data.fixer.insertTextAfterRange(expression.range, `,${data.newProps.toObjectString()}${close}`);
|
|
return;
|
|
}
|
|
const object = findClosestObject(element.array, element.node);
|
|
if (object) {
|
|
yield data.newProps.fixObject({ fixer: data.fixer, object });
|
|
return;
|
|
}
|
|
const tokens = (0, ast_utils_1.getParenthesizedTokens)(element.node, sourceCode);
|
|
const maybeComma = sourceCode.getTokenAfter(tokens.right);
|
|
let insertOffset, text;
|
|
if ((0, eslint_utils_2.isCommaToken)(maybeComma)) {
|
|
insertOffset = maybeComma.range[1];
|
|
text = data.newProps.toObjectString();
|
|
}
|
|
else {
|
|
insertOffset = tokens.right.range[1];
|
|
text = `,${data.newProps.toObjectString()}`;
|
|
}
|
|
if (element.array.elements[element.array.elements.length - 1] !==
|
|
element.node) {
|
|
text += ",";
|
|
}
|
|
yield data.fixer.insertTextAfterRange([insertOffset, insertOffset], text);
|
|
},
|
|
*fixExpression(data) {
|
|
if (element.array) {
|
|
const object = findClosestObject(element.array, element.node);
|
|
if (object) {
|
|
yield data.fixer.removeRange((0, ast_utils_1.getParenthesizedRange)(data.node, sourceCode));
|
|
const tokens = (0, ast_utils_1.getParenthesizedTokens)(element.node, sourceCode);
|
|
const maybeComma = sourceCode.getTokenAfter(tokens.right);
|
|
if ((0, eslint_utils_2.isCommaToken)(maybeComma)) {
|
|
yield data.fixer.removeRange(maybeComma.range);
|
|
}
|
|
else {
|
|
const maybeBeforeComma = sourceCode.getTokenBefore(tokens.left);
|
|
if ((0, eslint_utils_2.isCommaToken)(maybeBeforeComma)) {
|
|
yield data.fixer.removeRange(maybeBeforeComma.range);
|
|
}
|
|
}
|
|
yield data.newProps.fixObject({ fixer: data.fixer, object });
|
|
return;
|
|
}
|
|
}
|
|
yield data.fixer.replaceTextRange((0, ast_utils_1.getParenthesizedRange)(data.node, sourceCode), data.newProps.toObjectString());
|
|
},
|
|
});
|
|
}
|
|
function findClosestObject(array, target) {
|
|
const index = array.elements.indexOf(target);
|
|
const afterElements = array.elements.slice(index + 1);
|
|
const beforeElements = array.elements.slice(0, index).reverse();
|
|
const length = Math.max(afterElements.length, beforeElements.length);
|
|
for (let index = 0; index < length; index++) {
|
|
const after = afterElements[index];
|
|
if ((after === null || after === void 0 ? void 0 : after.type) === types_1.AST_NODE_TYPES.ObjectExpression) {
|
|
return after;
|
|
}
|
|
const before = beforeElements[index];
|
|
if ((before === null || before === void 0 ? void 0 : before.type) === types_1.AST_NODE_TYPES.ObjectExpression) {
|
|
return before;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function visitElementExpression(node, state) {
|
|
if (node.type === types_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
verifyConditionalExpression(node, state);
|
|
}
|
|
else if (node.type === types_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
const quasis = [...node.quasis];
|
|
let beforeQuasiWk = quasis.shift();
|
|
for (const expression of node.expressions) {
|
|
const beforeQuasi = beforeQuasiWk;
|
|
const afterQuasi = quasis.shift();
|
|
visitElementExpression(expression, {
|
|
beforeIsWord() {
|
|
const beforeElements = [];
|
|
const targetIndex = node.expressions.indexOf(expression);
|
|
for (let index = 0; index < targetIndex; index++) {
|
|
beforeElements.push(node.quasis[index], node.expressions[index]);
|
|
}
|
|
beforeElements.push(node.quasis[targetIndex]);
|
|
const isSpace = endsWithSpace(beforeElements);
|
|
return isSpace == null ? state.beforeIsWord() : !isSpace;
|
|
},
|
|
afterIsWord() {
|
|
const targetIndex = node.expressions.indexOf(expression);
|
|
const afterElements = [node.quasis[targetIndex + 1]];
|
|
for (let index = targetIndex + 1; index < node.expressions.length; index++) {
|
|
afterElements.push(node.expressions[index], node.quasis[index + 1]);
|
|
}
|
|
const isSpace = startsWithSpace(afterElements);
|
|
return isSpace == null ? state.afterIsWord() : !isSpace;
|
|
},
|
|
fixArrayElement: state.fixArrayElement,
|
|
*fixExpression(data) {
|
|
const fixer = data.fixer;
|
|
if (beforeQuasi.value.cooked.trim() ||
|
|
afterQuasi.value.cooked.trim() ||
|
|
node.expressions.length > 1) {
|
|
yield fixer.replaceTextRange([beforeQuasi.range[1] - 2, beforeQuasi.range[1]], data.beforeSpaces);
|
|
yield fixer.replaceTextRange([afterQuasi.range[0], afterQuasi.range[0] + 1], data.afterSpaces);
|
|
yield* state.fixArrayElement(data);
|
|
return;
|
|
}
|
|
const tokens = (0, ast_utils_1.getParenthesizedTokens)(node, sourceCode);
|
|
yield fixer.removeRange([
|
|
tokens.left.range[0],
|
|
beforeQuasi.range[1],
|
|
]);
|
|
yield fixer.removeRange([
|
|
afterQuasi.range[0],
|
|
tokens.right.range[1],
|
|
]);
|
|
yield* state.fixExpression(Object.assign(Object.assign({}, data), { beforeSpaces: beforeQuasi.value.cooked + data.beforeSpaces, afterSpaces: data.afterSpaces + afterQuasi.value.cooked }));
|
|
},
|
|
});
|
|
beforeQuasiWk = afterQuasi;
|
|
}
|
|
}
|
|
else if (node.type === types_1.AST_NODE_TYPES.CallExpression) {
|
|
if ((0, ast_utils_1.isStringCallExpression)(node) &&
|
|
node.arguments[0] &&
|
|
node.arguments[0].type !== types_1.AST_NODE_TYPES.SpreadElement) {
|
|
visitElementExpression(node.arguments[0], {
|
|
beforeIsWord: state.beforeIsWord,
|
|
afterIsWord: state.afterIsWord,
|
|
fixArrayElement: state.fixArrayElement,
|
|
*fixExpression(data) {
|
|
const openParen = sourceCode.getTokenAfter((0, ast_utils_1.getParenthesizedTokens)(node.callee, sourceCode).right);
|
|
const stripStart = sourceCode.getTokenAfter((0, ast_utils_1.getParenthesizedTokens)(node.arguments[0], sourceCode).right);
|
|
const tokens = (0, ast_utils_1.getParenthesizedTokens)(node, sourceCode);
|
|
yield data.fixer.removeRange([
|
|
tokens.left.range[0],
|
|
openParen.range[1],
|
|
]);
|
|
yield data.fixer.removeRange([
|
|
stripStart.range[0],
|
|
tokens.right.range[1],
|
|
]);
|
|
yield* state.fixExpression(data);
|
|
},
|
|
});
|
|
}
|
|
else if (node.callee.type === types_1.AST_NODE_TYPES.MemberExpression &&
|
|
(0, eslint_utils_1.getPropertyName)(node.callee) === "trim") {
|
|
const men = node.callee;
|
|
visitElementExpression(men.object, {
|
|
beforeIsWord: state.beforeIsWord,
|
|
afterIsWord: state.afterIsWord,
|
|
fixArrayElement: state.fixArrayElement,
|
|
*fixExpression(data) {
|
|
const tokens = (0, ast_utils_1.getParenthesizedTokens)(men.object, sourceCode);
|
|
yield data.fixer.removeRange([
|
|
tokens.right.range[1],
|
|
node.range[1],
|
|
]);
|
|
yield* state.fixExpression(data);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
else if (node.type === types_1.AST_NODE_TYPES.BinaryExpression) {
|
|
const elements = (0, ast_utils_1.extractConcatExpressions)(node, sourceCode);
|
|
if (!elements) {
|
|
return;
|
|
}
|
|
for (const expression of elements) {
|
|
visitElementExpression(expression, {
|
|
beforeIsWord() {
|
|
const index = elements.indexOf(expression);
|
|
const beforeElements = elements.slice(0, index);
|
|
const isSpace = endsWithSpace(beforeElements);
|
|
return isSpace == null ? state.beforeIsWord() : !isSpace;
|
|
},
|
|
afterIsWord() {
|
|
const index = elements.indexOf(expression);
|
|
const afterElements = elements.slice(index + 1);
|
|
const isSpace = startsWithSpace(afterElements);
|
|
return isSpace == null ? state.afterIsWord() : !isSpace;
|
|
},
|
|
fixArrayElement: state.fixArrayElement,
|
|
*fixExpression(data) {
|
|
const fixer = data.fixer;
|
|
const index = elements.indexOf(expression);
|
|
const beforeElements = elements.slice(0, index);
|
|
const afterElements = elements.slice(index + 1);
|
|
const tokens = (0, ast_utils_1.getParenthesizedTokens)(expression, sourceCode);
|
|
if (beforeElements.some((element) => {
|
|
const str = (0, ast_utils_1.getStringIfConstant)(element);
|
|
return str == null || Boolean(str.trim());
|
|
}) ||
|
|
afterElements.some((element) => {
|
|
const str = (0, ast_utils_1.getStringIfConstant)(element);
|
|
return str == null || Boolean(str.trim());
|
|
})) {
|
|
const beforeElement = beforeElements[beforeElements.length - 1];
|
|
const afterElement = afterElements[0];
|
|
if (beforeElement &&
|
|
(0, ast_utils_1.isStringLiteral)(beforeElement) &&
|
|
afterElement &&
|
|
(0, ast_utils_1.isStringLiteral)(afterElement)) {
|
|
if (sourceCode.text[beforeElement.range[0]] !==
|
|
sourceCode.text[afterElement.range[0]]) {
|
|
const targetIsBefore = sourceCode.text[beforeElement.range[0]] === "'";
|
|
const replaceLiteral = targetIsBefore
|
|
? beforeElement
|
|
: afterElement;
|
|
yield fixer.replaceTextRange([
|
|
replaceLiteral.range[0] + 1,
|
|
replaceLiteral.range[1] - 1,
|
|
], JSON.stringify(replaceLiteral.value).slice(1, -1));
|
|
yield fixer.replaceTextRange(targetIsBefore
|
|
? [
|
|
replaceLiteral.range[0],
|
|
replaceLiteral.range[0] + 1,
|
|
]
|
|
: [
|
|
replaceLiteral.range[1] - 1,
|
|
replaceLiteral.range[1],
|
|
], '"');
|
|
}
|
|
yield fixer.replaceTextRange([beforeElement.range[1] - 1, tokens.left.range[0]], data.beforeSpaces);
|
|
yield fixer.replaceTextRange([tokens.right.range[1], afterElement.range[0] + 1], data.afterSpaces);
|
|
}
|
|
else {
|
|
const beforeToken = sourceCode.getTokenBefore(tokens.left);
|
|
if ((beforeToken === null || beforeToken === void 0 ? void 0 : beforeToken.value) === "+") {
|
|
yield fixer.removeRange(beforeToken.range);
|
|
}
|
|
else {
|
|
const afterToken = sourceCode.getTokenAfter(tokens.right);
|
|
yield fixer.removeRange(afterToken.range);
|
|
}
|
|
}
|
|
yield* state.fixArrayElement(data);
|
|
return;
|
|
}
|
|
if (beforeElements.length) {
|
|
const beforeToken = sourceCode.getTokenBefore(tokens.left);
|
|
yield fixer.removeRange([
|
|
beforeElements[0].range[0],
|
|
beforeToken.range[1],
|
|
]);
|
|
}
|
|
if (afterElements.length) {
|
|
const afterToken = sourceCode.getTokenAfter(tokens.right);
|
|
yield fixer.removeRange([
|
|
afterToken.range[0],
|
|
afterElements[afterElements.length - 1].range[1],
|
|
]);
|
|
}
|
|
yield* state.fixExpression(Object.assign(Object.assign({}, data), { beforeSpaces: beforeElements
|
|
.map((e) => (0, ast_utils_1.getStringIfConstant)(e))
|
|
.join("") + data.beforeSpaces, afterSpaces: data.afterSpaces +
|
|
afterElements.map((e) => (0, ast_utils_1.getStringIfConstant)(e)).join("") }));
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function extractElements(node) {
|
|
if (node.type === types_1.AST_NODE_TYPES.ArrayExpression) {
|
|
const result = [];
|
|
for (const element of node.elements) {
|
|
if (!element || element.type === types_1.AST_NODE_TYPES.SpreadElement) {
|
|
continue;
|
|
}
|
|
result.push(...extractElements(element).map((e) => {
|
|
if (e.array == null) {
|
|
return {
|
|
array: node,
|
|
node: e.node,
|
|
};
|
|
}
|
|
return e;
|
|
}));
|
|
}
|
|
return result;
|
|
}
|
|
return [{ node, array: null }];
|
|
}
|
|
return {
|
|
JSXAttribute: verifyAttr,
|
|
AstroTemplateLiteralAttribute: verifyAttr,
|
|
};
|
|
},
|
|
});
|