first commit
This commit is contained in:
Generated
Vendored
+250
@@ -0,0 +1,250 @@
|
||||
"use strict";
|
||||
const require_runtime = require("../_virtual/_rolldown/runtime.js");
|
||||
const require_index = require("../utils/index.js");
|
||||
//#region lib/rules/no-duplicate-class-names.js
|
||||
/**
|
||||
* @fileoverview disallow duplication of class names in class attributes
|
||||
* @author Yizack Rangel
|
||||
* See LICENSE file in root directory for full license.
|
||||
*/
|
||||
var require_no_duplicate_class_names = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
|
||||
const utils = require_index.default;
|
||||
/**
|
||||
* @param {VDirective} node
|
||||
* @param {Expression} [expression]
|
||||
* @param {boolean} [isUnconditional=true] whether the expression is unconditional
|
||||
* @param {Expression} [parentExpr] parent expression for context
|
||||
* @return {IterableIterator<{ node: Literal | TemplateElement, isUnconditional: boolean, parentExpr?: Expression }>}
|
||||
*/
|
||||
function* extractClassNodes(node, expression, isUnconditional = true, parentExpr) {
|
||||
const nodeExpression = expression ?? node.value?.expression;
|
||||
if (!nodeExpression) return;
|
||||
switch (nodeExpression.type) {
|
||||
case "Literal":
|
||||
yield {
|
||||
node: nodeExpression,
|
||||
isUnconditional,
|
||||
parentExpr
|
||||
};
|
||||
break;
|
||||
case "ObjectExpression":
|
||||
for (const prop of nodeExpression.properties) if (prop.type === "Property" && prop.key?.type === "Literal" && typeof prop.key.value === "string") yield {
|
||||
node: prop.key,
|
||||
isUnconditional: false,
|
||||
parentExpr: nodeExpression
|
||||
};
|
||||
break;
|
||||
case "ArrayExpression":
|
||||
for (const element of nodeExpression.elements) {
|
||||
if (!element || element.type === "SpreadElement") continue;
|
||||
yield* extractClassNodes(node, element, isUnconditional, nodeExpression);
|
||||
}
|
||||
break;
|
||||
case "ConditionalExpression":
|
||||
yield* extractClassNodes(node, nodeExpression.consequent, false, nodeExpression);
|
||||
yield* extractClassNodes(node, nodeExpression.alternate, false, nodeExpression);
|
||||
break;
|
||||
case "TemplateLiteral":
|
||||
for (const quasi of nodeExpression.quasis) yield {
|
||||
node: quasi,
|
||||
isUnconditional,
|
||||
parentExpr: nodeExpression
|
||||
};
|
||||
for (const expr of nodeExpression.expressions) yield* extractClassNodes(node, expr, isUnconditional, nodeExpression);
|
||||
break;
|
||||
case "BinaryExpression":
|
||||
if (nodeExpression.operator === "+") {
|
||||
yield* extractClassNodes(node, nodeExpression.left, isUnconditional, nodeExpression);
|
||||
yield* extractClassNodes(node, nodeExpression.right, isUnconditional, nodeExpression);
|
||||
}
|
||||
break;
|
||||
case "LogicalExpression":
|
||||
yield* extractClassNodes(node, nodeExpression.left, isUnconditional, nodeExpression);
|
||||
yield* extractClassNodes(node, nodeExpression.right, false, nodeExpression);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} classList
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getClassNames(classList) {
|
||||
return classList.split(/\s+/).filter(Boolean);
|
||||
}
|
||||
/**
|
||||
* Append the given token to the last non-whitespace item in `kept`.
|
||||
* @param {string[]} kept
|
||||
* @param {string} token
|
||||
*/
|
||||
function appendToLastClassName(kept, token) {
|
||||
for (let j = kept.length - 1; j >= 0; j--) if (!/^\s+$/.test(kept[j])) {
|
||||
kept[j] = kept[j].split(/(\s+)/)[0] + token;
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} raw - raw class names string including quotes
|
||||
* @returns {string}
|
||||
*/
|
||||
function removeDuplicateClassNames(raw) {
|
||||
const quote = raw[0];
|
||||
const inner = raw.slice(1, -1);
|
||||
const tokens = inner.split(/(\s+)/);
|
||||
/** @type {string[]} */
|
||||
const kept = [];
|
||||
const used = /* @__PURE__ */ new Set();
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (!token) continue;
|
||||
if (/^\s+$/.test(token)) if (kept.length > 0) kept[kept.length - 1] += token;
|
||||
else kept.push(token);
|
||||
else if (used.has(token)) {
|
||||
const nextToken = tokens[i + 1];
|
||||
if (kept.length > 0 && i + 1 < tokens.length && /^\s+$/.test(nextToken)) {
|
||||
appendToLastClassName(kept, nextToken);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
kept.push(token);
|
||||
used.add(token);
|
||||
}
|
||||
}
|
||||
const hasTrailingSpace = /\s$/.test(inner);
|
||||
const lastItem = kept.at(-1);
|
||||
if (lastItem && !hasTrailingSpace) {
|
||||
if (!/^\s+$/.test(lastItem)) {
|
||||
const parts = lastItem.split(/(\s+)/);
|
||||
kept[kept.length - 1] = parts[0];
|
||||
}
|
||||
}
|
||||
return quote + kept.join("") + quote;
|
||||
}
|
||||
/** @param {VLiteral | Literal | TemplateElement | null} node */
|
||||
function getRawValue(node) {
|
||||
if (!node?.value) return null;
|
||||
return typeof node.value === "object" && "raw" in node.value ? node.value.raw : node.value;
|
||||
}
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
url: "https://eslint.vuejs.org/rules/no-duplicate-class-names.html",
|
||||
description: "disallow duplication of class names in class attributes",
|
||||
categories: void 0
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: { duplicateClassNames: "Duplicate class name{{plural}} {{names}}." }
|
||||
},
|
||||
/** @param {RuleContext} context */
|
||||
create: (context) => {
|
||||
/**
|
||||
* @param {VLiteral | Literal | TemplateElement | null} node
|
||||
*/
|
||||
function reportDuplicateClasses(node) {
|
||||
if (!node?.value) return;
|
||||
const classList = getRawValue(node);
|
||||
if (typeof classList !== "string") return;
|
||||
const classNames = getClassNames(classList);
|
||||
if (classNames.length <= 1) return;
|
||||
const seen = /* @__PURE__ */ new Set();
|
||||
const duplicates = /* @__PURE__ */ new Set();
|
||||
for (const className of classNames) if (seen.has(className)) duplicates.add(className);
|
||||
else seen.add(className);
|
||||
if (duplicates.size === 0) return;
|
||||
context.report({
|
||||
node,
|
||||
messageId: "duplicateClassNames",
|
||||
data: {
|
||||
names: [...duplicates].map((name) => `'${name}'`).join(", "),
|
||||
plural: duplicates.size > 1 ? "s" : ""
|
||||
},
|
||||
fix: (fixer) => {
|
||||
const raw = context.sourceCode.text.slice(node.range[0], node.range[1]);
|
||||
return fixer.replaceText(node, removeDuplicateClassNames(raw));
|
||||
}
|
||||
});
|
||||
return duplicates;
|
||||
}
|
||||
return utils.defineTemplateBodyVisitor(context, {
|
||||
/** @param {VAttribute} node */
|
||||
"VAttribute[directive=false][key.name='class'][value.type='VLiteral']"(node) {
|
||||
reportDuplicateClasses(node.value);
|
||||
},
|
||||
/** @param {VDirective} node */
|
||||
"VAttribute[directive=true][key.argument.name='class'][value.type='VExpressionContainer']"(node) {
|
||||
const parent = node.parent;
|
||||
const staticAttr = (parent.attributes || []).find((attr) => attr.key && attr.key.name === "class" && attr.value && attr.value.type === "VLiteral");
|
||||
/** @type {Set<string> | null} */
|
||||
const staticClasses = staticAttr && staticAttr.value && staticAttr.value.type === "VLiteral" ? new Set(getClassNames(String(staticAttr.value.value))) : null;
|
||||
/** @type {Set<string>} */
|
||||
const reported = /* @__PURE__ */ new Set();
|
||||
/** @type {Set<string>} */
|
||||
const duplicatesInExpression = /* @__PURE__ */ new Set();
|
||||
/** @type {Map<string, ASTNode>} */
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
/** @type {Map<string, {node: ASTNode, isUnconditional: boolean, parentExpr?: Expression}>} */
|
||||
const collected = /* @__PURE__ */ new Map();
|
||||
/**
|
||||
* @param {string} className
|
||||
* @param {Literal | TemplateElement} reportNode
|
||||
* @param {boolean} isUnconditional
|
||||
* @param {Expression} [parentExpr]
|
||||
*/
|
||||
function collectClassName(className, reportNode, isUnconditional, parentExpr) {
|
||||
if (reported.has(className)) return;
|
||||
const existing = collected.get(className);
|
||||
if (existing) {
|
||||
const isSameParent = parentExpr && existing.parentExpr === parentExpr && (parentExpr.type === "BinaryExpression" || parentExpr.type === "TemplateLiteral");
|
||||
if (existing.isUnconditional || isUnconditional || isSameParent) duplicatesInExpression.add(className);
|
||||
} else collected.set(className, {
|
||||
node: reportNode.parent,
|
||||
isUnconditional,
|
||||
parentExpr
|
||||
});
|
||||
if (isUnconditional) if (seen.has(className)) duplicatesInExpression.add(className);
|
||||
else seen.set(className, reportNode.parent);
|
||||
}
|
||||
const classNodes = extractClassNodes(node);
|
||||
for (const { node: reportNode, isUnconditional, parentExpr } of classNodes) {
|
||||
const reportedClasses = reportDuplicateClasses(reportNode);
|
||||
if (reportedClasses) for (const reportedClass of reportedClasses) reported.add(reportedClass);
|
||||
const classList = getRawValue(reportNode);
|
||||
if (typeof classList !== "string") continue;
|
||||
const classNames = getClassNames(classList);
|
||||
for (const className of classNames) collectClassName(className, reportNode, isUnconditional, parentExpr);
|
||||
if (staticClasses) {
|
||||
const intersection = classNames.filter((n) => staticClasses.has(n));
|
||||
if (intersection.length > 0 && parent) context.report({
|
||||
node: parent,
|
||||
messageId: "duplicateClassNames",
|
||||
data: {
|
||||
names: intersection.map((name) => `'${name}'`).join(", "),
|
||||
plural: intersection.length > 1 ? "s" : ""
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const className of duplicatesInExpression) {
|
||||
const reportNode = seen.get(className) || collected.get(className)?.node;
|
||||
if (reportNode) context.report({
|
||||
node: reportNode,
|
||||
messageId: "duplicateClassNames",
|
||||
data: {
|
||||
names: `'${className}'`,
|
||||
plural: ""
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}));
|
||||
//#endregion
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return require_no_duplicate_class_names();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user