Files
LearnNode/node-course/node_modules/@eslint/css/dist/rules/selector-complexity.js
T
2026-08-06 12:00:39 +10:00

373 lines
16 KiB
JavaScript

/**
* @fileoverview Rule to limit and disallow CSS selectors.
* @author Tanuj Kanti
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @import { CSSRuleDefinition } from "../types.js"
* @typedef {"maxSelectors" | "disallowedSelectors"} SelectorComplexityMessageIds
* @typedef {[{
* maxIds?: number,
* maxClasses?: number,
* maxTypes?: number,
* maxAttributes?: number,
* maxPseudoClasses?: number,
* maxUniversals?: number,
* maxCompounds?: number,
* maxCombinators?: number,
* disallowCombinators?: string[],
* disallowPseudoClasses?: string[],
* disallowPseudoElements?: string[],
* disallowAttributes?: string[],
* disallowAttributeMatchers?: string[],
* }]} SelectorComplexityOptions
* @typedef {CSSRuleDefinition<{ RuleOptions: SelectorComplexityOptions, MessageIds: SelectorComplexityMessageIds }> } SelectorComplexityRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Get the location of a selector of a given name.
* @param {Array<Object>} allSelector All CSS selector nodes.
* @param {string} disallowedSelector The name of the disallowed selector.
* @returns {Object} The location of the disallowed selector.
*/
function getDisallowedSelectorsLocation(allSelector, disallowedSelector) {
return allSelector.find(selector => selector.name === disallowedSelector)
.loc;
}
/**
* An error for exceeding the maximum allowed selectors of a specific type.
* @param {Object} context The ESLint rule context object.
* @param {Object} selectorLoc The location of the selector.
* @param {number} maxValue The max number of selectors that are allowed.
* @param {string} selectorType The type of CSS selector.
* @returns {void}
*/
function exceedLimitError(context, selectorLoc, maxValue, selectorType) {
context.report({
loc: selectorLoc,
messageId: "maxSelectors",
data: {
selector: selectorType,
limit: maxValue,
},
});
}
/**
* Gives an array of CSS selectors of a specific type.
* @param {Array<Object>} selectors All CSS selectors nodes.
* @param {string} selectorType The type of CSS selector to filter out.
* @returns {Array<Object>} Filtered selectors.
*/
function getSelectors(selectors, selectorType) {
return selectors.filter(selector => selector.type === selectorType);
}
/**
* Get the names of all CSS selectors.
* @param {Array<Object>} selectors All CSS selector nodes.
* @returns {Array<string>} Array of selector names.
*/
function getSelectorNames(selectors) {
return selectors.map(selector => selector.name);
}
/**
* Get the location of the attribute matcher or operator in a given attribute selector.
* @param {Array<Object>} selectors All CSS selector nodes.
* @param {number} index The index of the attribute selector in the selectors array.
* @returns {{ startLoc: Object, endLoc: Object }} The start and end locations of the operator.
*/
function getOperatorLocation(selectors, index) {
const selector = selectors[index];
let startLoc;
let endLoc;
if (selector.name.type === "Identifier") {
startLoc = selector.name.loc.end;
}
if (selector.value) {
endLoc = selector.value.loc.start;
}
return { startLoc, endLoc };
}
/**
* Get the location of a given disallowed combinator.
* @param {Array<Object>} selectors All CSS selector nodes.
* @param {Array<Object>} combinatorNodes All combinator nodes.
* @param {string} combinator Name of combinator.
* @param {number} index The index of the given combinator.
* @returns {Object} The location of the disallowed combinator.
*/
function getDisallowedCombinatorsLocation(selectors, combinatorNodes, combinator, index) {
let location;
if (combinator === " ") {
const selectorsArr = [];
let selectorsGroup = [];
selectors.forEach(selector => {
if (selector.type === "Combinator") {
selectorsArr.push(selectorsGroup);
selectorsGroup = [];
}
else {
selectorsGroup.push(selector);
}
});
if (selectorsGroup.length > 0) {
selectorsArr.push(selectorsGroup);
}
location = {
start: selectorsArr[index].at(-1).loc.end,
end: selectorsArr[index + 1][0].loc.start,
};
}
else {
const currentCombinatorNode = combinatorNodes[index];
location = currentCombinatorNode.loc;
}
return location;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {SelectorComplexityRuleDefinition} */ ({
meta: {
type: "problem",
docs: {
description: "Disallow and limit CSS selectors",
recommended: false,
url: "https://github.com/eslint/css/blob/main/docs/rules/selector-complexity.md",
},
schema: [
{
type: "object",
properties: {
maxIds: {
type: "integer",
minimum: 0,
},
maxClasses: {
type: "integer",
minimum: 0,
},
maxTypes: {
type: "integer",
minimum: 0,
},
maxAttributes: {
type: "integer",
minimum: 0,
},
maxPseudoClasses: {
type: "integer",
minimum: 0,
},
maxUniversals: {
type: "integer",
minimum: 0,
},
maxCompounds: {
type: "integer",
minimum: 0,
},
maxCombinators: {
type: "integer",
minimum: 0,
},
disallowCombinators: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
disallowPseudoClasses: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
disallowPseudoElements: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
disallowAttributes: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
disallowAttributeMatchers: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
defaultOptions: [
{
maxIds: Infinity,
maxClasses: Infinity,
maxTypes: Infinity,
maxAttributes: Infinity,
maxPseudoClasses: Infinity,
maxUniversals: Infinity,
maxCompounds: Infinity,
maxCombinators: Infinity,
disallowCombinators: [],
disallowPseudoClasses: [],
disallowPseudoElements: [],
disallowAttributes: [],
disallowAttributeMatchers: [],
},
],
messages: {
maxSelectors: "Exceeded maximum {{selector}} selector. Only {{limit}} allowed.",
disallowedSelectors: "'{{selectorName}}' {{selector}} is not allowed.",
},
},
create(context) {
const [{ maxIds, maxClasses, maxTypes, maxAttributes, maxPseudoClasses, maxUniversals, maxCompounds, maxCombinators, disallowCombinators, disallowPseudoClasses, disallowPseudoElements, disallowAttributes, disallowAttributeMatchers, },] = context.options;
return {
Selector(node) {
const selectors = node.children;
const selectorLoc = node.loc;
const idSelectors = getSelectors(selectors, "IdSelector");
const classSelectors = getSelectors(selectors, "ClassSelector");
const typeSelectors = selectors.filter(child => child.type === "TypeSelector" && child.name !== "*");
const attributeSelectors = getSelectors(selectors, "AttributeSelector");
const pseudoClassSelectors = getSelectors(selectors, "PseudoClassSelector");
const universalSelectors = selectors.filter(child => child.type === "TypeSelector" && child.name === "*");
const combinatorNodes = getSelectors(selectors, "Combinator");
const combinators = getSelectorNames(combinatorNodes);
const pseudoClassSelectorsNames = getSelectorNames(pseudoClassSelectors);
const pseudoElementSelectors = getSelectors(selectors, "PseudoElementSelector");
const pseudoElementNames = getSelectorNames(pseudoElementSelectors);
const attributeNames = attributeSelectors.map(s => s.name.name);
const attributeMatchers = attributeSelectors
.map(child => child.matcher)
.filter(Boolean);
if (idSelectors.length > maxIds) {
exceedLimitError(context, selectorLoc, maxIds, "id");
}
if (classSelectors.length > maxClasses) {
exceedLimitError(context, selectorLoc, maxClasses, "class");
}
if (typeSelectors.length > maxTypes) {
exceedLimitError(context, selectorLoc, maxTypes, "type");
}
if (attributeSelectors.length > maxAttributes) {
exceedLimitError(context, selectorLoc, maxAttributes, "attribute");
}
if (pseudoClassSelectors.length > maxPseudoClasses) {
exceedLimitError(context, selectorLoc, maxPseudoClasses, "pseudo-class");
}
if (universalSelectors.length > maxUniversals) {
exceedLimitError(context, selectorLoc, maxUniversals, "universal");
}
if (combinatorNodes.length > maxCombinators) {
exceedLimitError(context, selectorLoc, maxCombinators, "combinator");
}
if (combinatorNodes.length + 1 > maxCompounds) {
exceedLimitError(context, selectorLoc, maxCompounds, "compound");
}
if (disallowPseudoClasses.length > 0) {
let disallowedPseudoClassLocation;
for (const pseudoClassName of pseudoClassSelectorsNames) {
if (disallowPseudoClasses.includes(pseudoClassName)) {
disallowedPseudoClassLocation =
getDisallowedSelectorsLocation(pseudoClassSelectors, pseudoClassName);
context.report({
loc: disallowedPseudoClassLocation,
messageId: "disallowedSelectors",
data: {
selectorName: pseudoClassName,
selector: "pseudo-class",
},
});
}
}
}
if (disallowCombinators.length > 0) {
let disallowedCombinatorLocation;
for (const [index, combinator] of combinators.entries()) {
if (disallowCombinators.includes(combinator)) {
disallowedCombinatorLocation =
getDisallowedCombinatorsLocation(selectors, combinatorNodes, combinator, index);
context.report({
loc: disallowedCombinatorLocation,
messageId: "disallowedSelectors",
data: {
selectorName: combinator,
selector: "combinator",
},
});
}
}
}
if (disallowPseudoElements.length > 0) {
let disallowPseudoElementsLocation;
for (const pseudoElement of pseudoElementNames) {
if (disallowPseudoElements.includes(pseudoElement)) {
disallowPseudoElementsLocation =
getDisallowedSelectorsLocation(pseudoElementSelectors, pseudoElement);
context.report({
loc: disallowPseudoElementsLocation,
messageId: "disallowedSelectors",
data: {
selectorName: pseudoElement,
selector: "pseudo-element",
},
});
}
}
}
if (disallowAttributes.length > 0) {
let disallowAttributesLocation;
for (const attributeName of attributeNames) {
if (disallowAttributes.includes(attributeName)) {
disallowAttributesLocation =
attributeSelectors.find(selector => selector.name.name === attributeName).name.loc;
context.report({
loc: disallowAttributesLocation,
messageId: "disallowedSelectors",
data: {
selectorName: attributeName,
selector: "attribute",
},
});
}
}
}
if (disallowAttributeMatchers.length > 0) {
for (const [index, attributeMatcher,] of attributeMatchers.entries()) {
if (disallowAttributeMatchers.includes(attributeMatcher)) {
const { startLoc, endLoc } = getOperatorLocation(attributeSelectors.filter(s => s.matcher), index);
context.report({
loc: {
start: startLoc,
end: endLoc,
},
messageId: "disallowedSelectors",
data: {
selectorName: attributeMatcher,
selector: "attribute-matcher",
},
});
}
}
}
},
};
},
});