first commit
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "suggestion";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let useFallbackFonts: string;
|
||||
let useGenericFont: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: FontFamilyFallbacksMessageIds;
|
||||
}>): {
|
||||
"Rule > Block > Declaration"(node: any): void;
|
||||
"Rule > Block > Declaration[property='font-family'] > Value"(node: any): void;
|
||||
"Rule > Block > Declaration[property='font'] > Value"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type FontFamilyFallbacksMessageIds = "useFallbackFonts" | "useGenericFont";
|
||||
export type FontFamilyFallbacksRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: FontFamilyFallbacksMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce the use of fallback fonts and a generic font last.
|
||||
* @author Tanuj Kanti
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"useFallbackFonts" | "useGenericFont"} FontFamilyFallbacksMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: FontFamilyFallbacksMessageIds }>} FontFamilyFallbacksRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const genericFonts = new Set([
|
||||
"serif",
|
||||
"sans-serif",
|
||||
"monospace",
|
||||
"cursive",
|
||||
"fantasy",
|
||||
"system-ui",
|
||||
"ui-serif",
|
||||
"ui-sans-serif",
|
||||
"ui-monospace",
|
||||
"ui-rounded",
|
||||
"emoji",
|
||||
"math",
|
||||
"fangsong",
|
||||
]);
|
||||
/**
|
||||
* Check if the value is a CSS-wide keyword.
|
||||
* @param {string} value The value to check.
|
||||
* @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
|
||||
* @returns {boolean} True if the value is a CSS-wide keyword, false otherwise.
|
||||
*/
|
||||
function isCSSWideKeyword(value, cssWideKeywords) {
|
||||
return cssWideKeywords.has(value.trim().toLowerCase());
|
||||
}
|
||||
/**
|
||||
* Check if the node is an identifier with a CSS-wide keyword.
|
||||
* @param {Object} node The node to check.
|
||||
* @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
|
||||
* @returns {boolean} True if the node is a CSS-wide keyword identifier, false otherwise.
|
||||
*/
|
||||
function isCSSWideKeywordIdentifier(node, cssWideKeywords) {
|
||||
return (node.type === "Identifier" &&
|
||||
isCSSWideKeyword(node.name, cssWideKeywords));
|
||||
}
|
||||
/**
|
||||
* Check if the node is a CSS variable function.
|
||||
* @param {Object} node The node to check.
|
||||
* @returns {boolean} True if the node is a variable function, false otherwise.
|
||||
*/
|
||||
function isVarFunction(node) {
|
||||
return node.type === "Function" && node.name === "var";
|
||||
}
|
||||
/**
|
||||
* Report an error if the font property values do not have fallbacks or a generic font.
|
||||
* @param {string} fontPropertyValues The font property values to check.
|
||||
* @param {Object} context The ESLint context object.
|
||||
* @param {Object} node The CSS node being checked.
|
||||
* @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords) {
|
||||
if (isCSSWideKeyword(fontPropertyValues, cssWideKeywords)) {
|
||||
return;
|
||||
}
|
||||
const valueList = fontPropertyValues.split(",").map(v => v.trim());
|
||||
if (valueList.length === 1) {
|
||||
const containsGenericFont = Array.from(genericFonts).some(font => valueList[0].includes(font));
|
||||
if (!containsGenericFont) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!genericFonts.has(valueList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {FontFamilyFallbacksRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description: "Enforce use of fallback fonts and a generic font last",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/font-family-fallbacks.md",
|
||||
},
|
||||
messages: {
|
||||
useFallbackFonts: "Use fallback fonts and a generic font last.",
|
||||
useGenericFont: "Use a generic font last.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const cssWideKeywords = new Set(sourceCode.lexer.cssWideKeywords.map(keyword => keyword.toLowerCase()));
|
||||
const variableMap = new Map();
|
||||
return {
|
||||
"Rule > Block > Declaration"(node) {
|
||||
if (node.property.startsWith("--")) {
|
||||
const variableName = node.property;
|
||||
const variableValue = node.value.type === "Raw" && node.value.value;
|
||||
variableMap.set(variableName, variableValue);
|
||||
}
|
||||
},
|
||||
"Rule > Block > Declaration[property='font-family'] > Value"(node) {
|
||||
const valueArr = node.children;
|
||||
if (valueArr.length === 1) {
|
||||
if (isCSSWideKeywordIdentifier(valueArr[0], cssWideKeywords)) {
|
||||
return;
|
||||
}
|
||||
if (valueArr[0].type === "Function" &&
|
||||
valueArr[0].name === "var") {
|
||||
const variableName = valueArr[0].children[0].type === "Identifier" &&
|
||||
valueArr[0].children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
if (isCSSWideKeyword(variableValue, cssWideKeywords)) {
|
||||
return;
|
||||
}
|
||||
const variableList = variableValue
|
||||
.split(",")
|
||||
.map(v => v.trim());
|
||||
if (variableList.length === 1 &&
|
||||
!genericFonts.has(variableList[0])) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
else if (!genericFonts.has(variableList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (valueArr[0].type === "Identifier" &&
|
||||
genericFonts.has(valueArr[0].name)) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const isUsingVariable = valueArr.some(child => isVarFunction(child));
|
||||
if (isUsingVariable) {
|
||||
const fontsList = [];
|
||||
const lastNode = valueArr.at(-1);
|
||||
if (lastNode.type === "Function" &&
|
||||
lastNode.name === "var") {
|
||||
const variableName = lastNode.children[0].type === "Identifier" &&
|
||||
lastNode.children[0].name;
|
||||
const lastVariable = variableMap.get(variableName);
|
||||
if (!lastVariable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
valueArr.forEach(child => {
|
||||
if (child.type === "String") {
|
||||
fontsList.push(child.value);
|
||||
}
|
||||
if (child.type === "Identifier") {
|
||||
fontsList.push(child.name);
|
||||
}
|
||||
if (child.type === "Function" &&
|
||||
child.name === "var") {
|
||||
const variableName = child.children[0].type === "Identifier" &&
|
||||
child.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (variableValue) {
|
||||
const variableList = variableValue
|
||||
.split(",")
|
||||
.map(v => v.trim());
|
||||
fontsList.push(...variableList);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fontsList.length > 0 &&
|
||||
!genericFonts.has(fontsList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const lastFont = valueArr.at(-1);
|
||||
if (!(lastFont.type === "Identifier" &&
|
||||
genericFonts.has(lastFont.name))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rule > Block > Declaration[property='font'] > Value"(node) {
|
||||
const valueArr = node.children;
|
||||
if (valueArr.length === 1) {
|
||||
const firstValue = valueArr[0];
|
||||
// If it font is set to system font, we don't need to check for fallbacks
|
||||
if (firstValue.type === "Identifier") {
|
||||
return;
|
||||
}
|
||||
// If the value is a variable function, we need to check the variable value
|
||||
if (firstValue.type === "Function" &&
|
||||
firstValue.name === "var") {
|
||||
// Check if the function is a variable
|
||||
const variableName = firstValue.children[0].type === "Identifier" &&
|
||||
firstValue.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const isUsingVariable = valueArr.some(child => isVarFunction(child));
|
||||
if (isUsingVariable) {
|
||||
const beforOperator = [];
|
||||
const afterOperator = [];
|
||||
const operator = valueArr.find(child => child.type === "Operator" &&
|
||||
child.value === ",");
|
||||
const operatorOffset = operator && operator.loc.end.offset;
|
||||
if (operatorOffset) {
|
||||
valueArr.forEach(child => {
|
||||
if (child.loc.end.offset < operatorOffset) {
|
||||
beforOperator.push(sourceCode.getText(child).trim());
|
||||
}
|
||||
else if (child.loc.end.offset > operatorOffset) {
|
||||
afterOperator.push(sourceCode.getText(child).trim());
|
||||
}
|
||||
});
|
||||
if (afterOperator.length !== 0) {
|
||||
const usingVar = afterOperator.some(value => value.startsWith("var"));
|
||||
if (!usingVar) {
|
||||
if (!genericFonts.has(afterOperator.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (afterOperator.at(-1).startsWith("var")) {
|
||||
const lastNode = valueArr.at(-1);
|
||||
const isFunctionVar = lastNode.type === "Function" &&
|
||||
lastNode.name === "var";
|
||||
const variableName = isFunctionVar &&
|
||||
lastNode.children[0].type ===
|
||||
"Identifier" &&
|
||||
lastNode.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
const variableList = variableValue
|
||||
.split(",")
|
||||
.map(v => v.trim());
|
||||
if (variableList.length > 0 &&
|
||||
!genericFonts.has(variableList.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!genericFonts.has(afterOperator.at(-1))) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useGenericFont",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (sourceCode
|
||||
.getText(valueArr.at(-1))
|
||||
.trim()
|
||||
.startsWith("var")) {
|
||||
const lastNode = valueArr.at(-1);
|
||||
const isFunctionVar = lastNode.type === "Function" &&
|
||||
lastNode.name === "var";
|
||||
const variableName = isFunctionVar &&
|
||||
lastNode.children[0].type ===
|
||||
"Identifier" &&
|
||||
lastNode.children[0].name;
|
||||
const variableValue = variableMap.get(variableName);
|
||||
if (!variableValue) {
|
||||
return;
|
||||
}
|
||||
reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
|
||||
}
|
||||
else {
|
||||
if (!genericFonts.has(sourceCode
|
||||
.getText(valueArr.at(-1))
|
||||
.trim())) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "useFallbackFonts",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const fontPropertyValues = sourceCode.getText(node);
|
||||
if (fontPropertyValues) {
|
||||
reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let fixable: "code";
|
||||
let hasSuggestions: true;
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let duplicateImport: string;
|
||||
let removeDuplicateImportWithModifiers: string;
|
||||
let removeDuplicateImportWithoutModifiers: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoDuplicateKeysMessageIds;
|
||||
}>): {
|
||||
"Atrule[name=/^import$/i]"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoDuplicateKeysMessageIds = "duplicateImport" | "removeDuplicateImportWithModifiers" | "removeDuplicateImportWithoutModifiers";
|
||||
export type NoDuplicateImportsRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoDuplicateKeysMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent duplicate imports in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"duplicateImport" | "removeDuplicateImportWithModifiers" | "removeDuplicateImportWithoutModifiers"} NoDuplicateKeysMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateImportsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Get the end index of import statement including a following newline if present.
|
||||
* @param {string} text The full text of the source code.
|
||||
* @param {number} end The end index of the import statement.
|
||||
* @returns {number} The end index of the import statement including a following newline.
|
||||
*/
|
||||
function getImportEnd(text, end) {
|
||||
let removeEnd = end;
|
||||
// Remove the node, and also remove a following newline if present
|
||||
if (text[removeEnd] === "\r") {
|
||||
removeEnd += text[removeEnd + 1] === "\n" ? 2 : 1;
|
||||
}
|
||||
else if (text[removeEnd] === "\n" || text[removeEnd] === "\f") {
|
||||
removeEnd += 1;
|
||||
}
|
||||
return removeEnd;
|
||||
}
|
||||
/**
|
||||
* Get the modifiers of an import statement.
|
||||
* @param {Object} importNode The import node to get modifiers from.
|
||||
* @param {Object} sourceCode The source code object.
|
||||
* @returns {string[]} An array of modifiers for the import statement.
|
||||
*/
|
||||
function getImportModifiers(importNode, sourceCode) {
|
||||
const importModifiers = [];
|
||||
const importHasModifiers = importNode.prelude?.children.length > 1;
|
||||
if (importHasModifiers) {
|
||||
importNode.prelude?.children.slice(1).forEach(modifier => {
|
||||
const modifierText = sourceCode.getText(modifier).trim();
|
||||
importModifiers.push(modifierText);
|
||||
});
|
||||
}
|
||||
return importModifiers;
|
||||
}
|
||||
/**
|
||||
* Get the fix for a duplicate import statement.
|
||||
* @param {Object} fixer The fixer object.
|
||||
* @param {string} text The full text of the source code.
|
||||
* @param {number} start The start index of the import statement to fix.
|
||||
* @param {number} end The end index of the import statement to fix.
|
||||
* @param {boolean} hasModifiers A boolean indicating whether the import statement has modifiers that differ from the original import.
|
||||
* @returns {Object|null} A fix object if a fix is applicable, or null if no fix should be applied.
|
||||
*/
|
||||
function getFixForImport(fixer, text, start, end, hasModifiers) {
|
||||
const removeEnd = getImportEnd(text, end);
|
||||
if (hasModifiers) {
|
||||
return fixer.removeRange([start, removeEnd]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoDuplicateImportsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
fixable: "code",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: "Disallow duplicate @import rules",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-imports.md",
|
||||
},
|
||||
messages: {
|
||||
duplicateImport: "Unexpected duplicate @import rule for '{{url}}'.",
|
||||
removeDuplicateImportWithModifiers: "Remove duplicate @import rule with modifier(s) - {{modifiers}}.",
|
||||
removeDuplicateImportWithoutModifiers: "Remove duplicate @import rule without modifiers.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const imports = [];
|
||||
return {
|
||||
"Atrule[name=/^import$/i]"(node) {
|
||||
const url = node.prelude?.children[0].value;
|
||||
const hasImport = imports.some(importNode => importNode.prelude?.children[0].value === url);
|
||||
if (hasImport) {
|
||||
const firstImportNode = imports.find(importNode => importNode.prelude?.children[0].value === url);
|
||||
const [firstImportStart, firstImportEnd] = sourceCode.getRange(firstImportNode);
|
||||
const firstImportHasModifiers = firstImportNode.prelude?.children.length > 1;
|
||||
const nodeHasModifiers = node.prelude?.children.length > 1;
|
||||
const [start, end] = sourceCode.getRange(node);
|
||||
const text = sourceCode.text;
|
||||
const firstImportModifiers = getImportModifiers(firstImportNode, sourceCode);
|
||||
const duplicateImportModifiers = getImportModifiers(node, sourceCode);
|
||||
const hasSameModifiers = firstImportModifiers.length ===
|
||||
duplicateImportModifiers.length &&
|
||||
firstImportModifiers.every((modifier, index) => modifier === duplicateImportModifiers[index]);
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "duplicateImport",
|
||||
data: { url },
|
||||
fix(fixer) {
|
||||
const hasModifiers = (!firstImportHasModifiers &&
|
||||
!nodeHasModifiers) ||
|
||||
hasSameModifiers;
|
||||
return getFixForImport(fixer, text, start, end, hasModifiers);
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: firstImportHasModifiers
|
||||
? "removeDuplicateImportWithModifiers"
|
||||
: "removeDuplicateImportWithoutModifiers",
|
||||
data: {
|
||||
modifiers: firstImportModifiers.join(" "),
|
||||
},
|
||||
fix(fixer) {
|
||||
const hasModifiers = (firstImportHasModifiers ||
|
||||
nodeHasModifiers) &&
|
||||
!hasSameModifiers;
|
||||
return getFixForImport(fixer, text, firstImportStart, firstImportEnd, hasModifiers);
|
||||
},
|
||||
},
|
||||
{
|
||||
messageId: nodeHasModifiers
|
||||
? "removeDuplicateImportWithModifiers"
|
||||
: "removeDuplicateImportWithoutModifiers",
|
||||
data: {
|
||||
modifiers: duplicateImportModifiers.join(" "),
|
||||
},
|
||||
fix(fixer) {
|
||||
const hasModifiers = (firstImportHasModifiers ||
|
||||
nodeHasModifiers) &&
|
||||
!hasSameModifiers;
|
||||
return getFixForImport(fixer, text, start, end, hasModifiers);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
else {
|
||||
imports.push(node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let duplicateKeyframeSelector: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "duplicateKeyframeSelector";
|
||||
}>): {
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]"(): void;
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]:exit"(): void;
|
||||
Rule(node: import("@eslint/css-tree").RulePlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type DuplicateKeyframeSelectorMessageIds = "duplicateKeyframeSelector";
|
||||
export type DuplicateKeyframeSelectorRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: DuplicateKeyframeSelectorMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow duplicate selectors within keyframe blocks.
|
||||
* @author Nitin Kumar
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"duplicateKeyframeSelector"} DuplicateKeyframeSelectorMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: DuplicateKeyframeSelectorMessageIds }>} DuplicateKeyframeSelectorRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {DuplicateKeyframeSelectorRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow duplicate selectors within keyframe blocks",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-duplicate-keyframe-selectors.md",
|
||||
},
|
||||
messages: {
|
||||
duplicateKeyframeSelector: "Unexpected duplicate selector '{{selector}}' found within keyframe block.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let insideKeyframes = false;
|
||||
const seen = new Map();
|
||||
return {
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]"() {
|
||||
insideKeyframes = true;
|
||||
seen.clear();
|
||||
},
|
||||
"Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i]:exit"() {
|
||||
insideKeyframes = false;
|
||||
},
|
||||
Rule(node) {
|
||||
if (!insideKeyframes) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore - children is a valid property for prelude
|
||||
const selector = node.prelude.children[0].children[0];
|
||||
let value;
|
||||
if (selector.type === "Percentage") {
|
||||
value = `${selector.value}%`;
|
||||
}
|
||||
else if (selector.type === "TypeSelector") {
|
||||
value = selector.name.toLowerCase();
|
||||
}
|
||||
else {
|
||||
value = selector.value;
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
context.report({
|
||||
loc: selector.loc,
|
||||
messageId: "duplicateKeyframeSelector",
|
||||
data: {
|
||||
selector: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
else {
|
||||
seen.set(value, true);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyBlock: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "emptyBlock";
|
||||
}>): {
|
||||
Block(node: import("@eslint/css-tree").BlockPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoEmptyBlocksMessageIds = "emptyBlock";
|
||||
export type NoEmptyBlocksRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoEmptyBlocksMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent empty blocks in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"emptyBlock"} NoEmptyBlocksMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoEmptyBlocksMessageIds }>} NoEmptyBlocksRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoEmptyBlocksRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow empty blocks",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-empty-blocks.md",
|
||||
},
|
||||
messages: {
|
||||
emptyBlock: "Unexpected empty block found.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Block(node) {
|
||||
if (node.children.length === 0) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "emptyBlock",
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let hasSuggestions: true;
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unexpectedImportant: string;
|
||||
let removeImportant: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoImportantMessageIds;
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoImportantMessageIds = "unexpectedImportant" | "removeImportant";
|
||||
export type NoImportantRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoImportantMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow `!important` flags.
|
||||
* @author thecalamiity
|
||||
* @author Yann Bertrand
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"unexpectedImportant" | "removeImportant"} NoImportantMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoImportantMessageIds }>} NoImportantRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const importantPattern = /!\s*important/iu;
|
||||
const commentPattern = /\/\*[\s\S]*?\*\//gu;
|
||||
const trailingWhitespacePattern = /\s*$/u;
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoImportantRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: "Disallow !important flags",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-important.md",
|
||||
},
|
||||
messages: {
|
||||
unexpectedImportant: "Unexpected !important flag found.",
|
||||
removeImportant: "Remove !important flag.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
Declaration(node) {
|
||||
if (node.important) {
|
||||
const declarationText = sourceCode.getText(node);
|
||||
const textWithoutComments = declarationText.replace(commentPattern,
|
||||
/* eslint-disable-next-line require-unicode-regexp -- we want to replace each code unit with a space */
|
||||
match => match.replace(/[^\r\n\f]/g, " "));
|
||||
const importantMatch = importantPattern.exec(textWithoutComments);
|
||||
const importantStartOffset = importantMatch.index;
|
||||
const importantEndOffset = importantStartOffset + importantMatch[0].length;
|
||||
const nodeStartOffset = node.loc.start.offset;
|
||||
context.report({
|
||||
loc: {
|
||||
start: sourceCode.getLocFromIndex(nodeStartOffset + importantStartOffset),
|
||||
end: sourceCode.getLocFromIndex(nodeStartOffset + importantEndOffset),
|
||||
},
|
||||
messageId: "unexpectedImportant",
|
||||
suggest: [
|
||||
{
|
||||
messageId: "removeImportant",
|
||||
fix(fixer) {
|
||||
// Find any trailing whitespace before the `!important`
|
||||
const whitespaceEndOffset = declarationText
|
||||
.slice(0, importantStartOffset)
|
||||
.search(trailingWhitespacePattern);
|
||||
return fixer.removeRange([
|
||||
nodeStartOffset + whitespaceEndOffset,
|
||||
nodeStartOffset + importantEndOffset,
|
||||
]);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let invalidCharsetPlacement: string;
|
||||
let invalidImportPlacement: string;
|
||||
let invalidNamespacePlacement: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidAtRulePlacementMessageIds;
|
||||
}>): {
|
||||
Atrule(node: import("@eslint/css-tree").AtrulePlain): void;
|
||||
Rule(): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidAtRulePlacementMessageIds = "invalidCharsetPlacement" | "invalidImportPlacement" | "invalidNamespacePlacement";
|
||||
export type NoInvalidAtRulePlacementRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoInvalidAtRulePlacementMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce correct placement of at-rules.
|
||||
* @author thecalamiity
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"invalidCharsetPlacement" | "invalidImportPlacement" | "invalidNamespacePlacement"} NoInvalidAtRulePlacementMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulePlacementMessageIds }>} NoInvalidAtRulePlacementRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidAtRulePlacementRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow invalid placement of at-rules",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-at-rule-placement.md",
|
||||
},
|
||||
messages: {
|
||||
invalidCharsetPlacement: "@charset must be placed at the very beginning of the stylesheet, before any rules, comments, or whitespace.",
|
||||
invalidImportPlacement: "@import must be placed before all other rules, except @charset and @layer statements.",
|
||||
invalidNamespacePlacement: "@namespace must be placed before all other rules, except @charset and @import.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let hasSeenNonImportRule = false;
|
||||
let hasSeenLayerBlock = false;
|
||||
let hasSeenLayer = false;
|
||||
let hasSeenNamespace = false;
|
||||
return {
|
||||
Atrule(node) {
|
||||
const name = node.name.toLowerCase();
|
||||
if (name === "charset") {
|
||||
if (node.loc.start.line !== 1 ||
|
||||
node.loc.start.column !== 1) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidCharsetPlacement",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (name === "layer") {
|
||||
if (node.block) {
|
||||
hasSeenLayerBlock = true;
|
||||
}
|
||||
hasSeenLayer = true;
|
||||
return;
|
||||
}
|
||||
if (name === "namespace") {
|
||||
if (hasSeenNonImportRule || hasSeenLayer) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidNamespacePlacement",
|
||||
});
|
||||
}
|
||||
hasSeenNamespace = true;
|
||||
return;
|
||||
}
|
||||
if (name === "import") {
|
||||
if (hasSeenNonImportRule ||
|
||||
hasSeenNamespace ||
|
||||
hasSeenLayerBlock) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "invalidImportPlacement",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
hasSeenNonImportRule = true;
|
||||
},
|
||||
Rule() {
|
||||
hasSeenNonImportRule = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let fixable: "code";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unknownAtRule: string;
|
||||
let invalidPrelude: string;
|
||||
let unknownDescriptor: string;
|
||||
let invalidDescriptor: string;
|
||||
let invalidExtraPrelude: string;
|
||||
let missingPrelude: string;
|
||||
let invalidCharsetSyntax: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidAtRulesMessageIds;
|
||||
}>): {
|
||||
Atrule(node: AtrulePlain): void;
|
||||
"AtRule > Block > Declaration"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidAtRulesMessageIds = "unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude" | "invalidCharsetSyntax";
|
||||
export type NoInvalidAtRulesRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoInvalidAtRulesMessageIds;
|
||||
}>;
|
||||
import type { AtrulePlain } from "@eslint/css-tree";
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent the use of unknown at-rules in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { isSyntaxMatchError } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { AtrulePlain } from "@eslint/css-tree"
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude" | "invalidCharsetSyntax"} NoInvalidAtRulesMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulesMessageIds }>} NoInvalidAtRulesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Set of at-rules that can be nested inside style rules.
|
||||
* @see https://www.w3.org/TR/css-nesting-1/#conditionals
|
||||
*/
|
||||
const nestableAtRules = new Set([
|
||||
"media",
|
||||
"supports",
|
||||
"layer",
|
||||
"scope",
|
||||
"container",
|
||||
"starting-style",
|
||||
]);
|
||||
/**
|
||||
* A valid `@charset` rule must:
|
||||
* - Enclose the encoding name in double quotes
|
||||
* - Include exactly one space character after `@charset`
|
||||
* - End immediately with a semicolon
|
||||
*/
|
||||
const charsetPattern = /^@charset "[^"]+";$/u;
|
||||
const charsetEncodingPattern = /^['"]?([^"';]+)['"]?/u;
|
||||
/**
|
||||
* Extracts metadata from an error object.
|
||||
* @param {SyntaxError} error The error object to extract metadata from.
|
||||
* @returns {Object} The metadata extracted from the error.
|
||||
*/
|
||||
function extractMetaDataFromError(error) {
|
||||
const message = error.message;
|
||||
const atRuleName = /`@(.*)`/u.exec(message)[1];
|
||||
let messageId = "unknownAtRule";
|
||||
if (message.endsWith("prelude")) {
|
||||
messageId = message.includes("should not")
|
||||
? "invalidExtraPrelude"
|
||||
: "missingPrelude";
|
||||
}
|
||||
return {
|
||||
messageId,
|
||||
data: {
|
||||
name: atRuleName,
|
||||
},
|
||||
};
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidAtRulesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
fixable: "code",
|
||||
docs: {
|
||||
description: "Disallow invalid at-rules",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-at-rules.md",
|
||||
},
|
||||
messages: {
|
||||
unknownAtRule: "Unknown at-rule '@{{name}}' found.",
|
||||
invalidPrelude: "Invalid prelude '{{prelude}}' found for at-rule '@{{name}}'. Expected '{{expected}}'.",
|
||||
unknownDescriptor: "Unknown descriptor '{{descriptor}}' found for at-rule '@{{name}}'.",
|
||||
invalidDescriptor: "Invalid value '{{value}}' for descriptor '{{descriptor}}' found for at-rule '@{{name}}'. Expected {{expected}}.",
|
||||
invalidExtraPrelude: "At-rule '@{{name}}' should not contain a prelude.",
|
||||
missingPrelude: "At-rule '@{{name}}' should contain a prelude.",
|
||||
invalidCharsetSyntax: "Invalid @charset syntax. Expected '@charset \"{{encoding}}\";'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
const lexer = sourceCode.lexer;
|
||||
/**
|
||||
* Validates a `@charset` rule for correct syntax:
|
||||
* - Verifies the rule name is exactly "charset" (case-sensitive)
|
||||
* - Ensures the rule has a prelude
|
||||
* - Validates the prelude matches the expected pattern
|
||||
* @param {AtrulePlain} node The node representing the rule.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateCharsetRule(node) {
|
||||
const { name, prelude, loc } = node;
|
||||
const charsetNameLoc = {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + name.length + 1,
|
||||
},
|
||||
};
|
||||
if (name !== "charset") {
|
||||
context.report({
|
||||
loc: charsetNameLoc,
|
||||
messageId: "unknownAtRule",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([
|
||||
loc.start.offset,
|
||||
loc.start.offset + name.length + 1,
|
||||
], "@charset");
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!prelude) {
|
||||
context.report({
|
||||
loc: charsetNameLoc,
|
||||
messageId: "missingPrelude",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const nodeText = sourceCode.getText(node);
|
||||
const preludeText = sourceCode.getText(prelude);
|
||||
const encoding = preludeText
|
||||
.match(charsetEncodingPattern)?.[1]
|
||||
?.trim();
|
||||
if (!encoding) {
|
||||
context.report({
|
||||
loc: prelude.loc,
|
||||
messageId: "invalidCharsetSyntax",
|
||||
data: { encoding: "<charset>" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!charsetPattern.test(nodeText)) {
|
||||
context.report({
|
||||
loc: prelude.loc,
|
||||
messageId: "invalidCharsetSyntax",
|
||||
data: { encoding },
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, `@charset "${encoding}";`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
Atrule(node) {
|
||||
if (node.name.toLowerCase() === "charset") {
|
||||
validateCharsetRule(node);
|
||||
return;
|
||||
}
|
||||
// checks both name and prelude
|
||||
const { error } = lexer.matchAtrulePrelude(node.name, node.prelude);
|
||||
if (error) {
|
||||
if (isSyntaxMatchError(error)) {
|
||||
context.report({
|
||||
loc: error.loc,
|
||||
messageId: "invalidPrelude",
|
||||
data: {
|
||||
name: node.name,
|
||||
prelude: error.css,
|
||||
expected: error.syntax,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loc = node.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
// add 1 to account for the @ symbol
|
||||
column: loc.start.column + node.name.length + 1,
|
||||
},
|
||||
},
|
||||
...extractMetaDataFromError(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
"AtRule > Block > Declaration"(node) {
|
||||
// skip custom descriptors
|
||||
if (node.property.startsWith("--")) {
|
||||
return;
|
||||
}
|
||||
// get at rule node
|
||||
const atRule = /** @type {AtrulePlain} */ (sourceCode.getParent(sourceCode.getParent(node)));
|
||||
if (nestableAtRules.has(atRule.name.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
const { error } = lexer.matchAtruleDescriptor(atRule.name, node.property, node.value);
|
||||
if (error) {
|
||||
if (isSyntaxMatchError(error)) {
|
||||
context.report({
|
||||
loc: error.loc,
|
||||
messageId: "invalidDescriptor",
|
||||
data: {
|
||||
name: atRule.name,
|
||||
descriptor: node.property,
|
||||
value: error.css,
|
||||
expected: error.syntax,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loc = node.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column + node.property.length,
|
||||
},
|
||||
},
|
||||
messageId: "unknownDescriptor",
|
||||
data: {
|
||||
name: atRule.name,
|
||||
descriptor: node.property,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let emptyGridArea: string;
|
||||
let unevenGridArea: string;
|
||||
let nonRectangularGridArea: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidNamedGridAreasMessageIds;
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidNamedGridAreasMessageIds = "emptyGridArea" | "unevenGridArea" | "nonRectangularGridArea";
|
||||
export type NoInvalidNamedGridAreasRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoInvalidNamedGridAreasMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent invalid named grid areas in CSS grid templates.
|
||||
* @author xbinaryx
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"emptyGridArea" | "unevenGridArea" | "nonRectangularGridArea"} NoInvalidNamedGridAreasMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidNamedGridAreasMessageIds }>} NoInvalidNamedGridAreasRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Regular expression to match null cell tokens (sequences of one or more dots)
|
||||
*/
|
||||
const nullCellToken = /^\.+$/u;
|
||||
/**
|
||||
* Finds non-rectangular grid areas in a 2D grid
|
||||
* @param {string[][]} grid 2D array representing the grid areas
|
||||
* @returns {Array<{name: string, row: number}>} Array of errors found
|
||||
*/
|
||||
function findNonRectangularAreas(grid) {
|
||||
const errors = [];
|
||||
const reported = new Set();
|
||||
const names = [...new Set(grid.flat())].filter(name => !nullCellToken.test(name));
|
||||
for (const name of names) {
|
||||
const indicesByRow = grid.map(row => {
|
||||
const indices = [];
|
||||
let idx = row.indexOf(name);
|
||||
while (idx !== -1) {
|
||||
indices.push(idx);
|
||||
idx = row.indexOf(name, idx + 1);
|
||||
}
|
||||
return indices;
|
||||
});
|
||||
for (let i = 0; i < indicesByRow.length; i++) {
|
||||
for (let j = i + 1; j < indicesByRow.length; j++) {
|
||||
const row1 = indicesByRow[i];
|
||||
const row2 = indicesByRow[j];
|
||||
if (row1.length === 0 || row2.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (row1.length !== row2.length ||
|
||||
!row1.every((val, idx) => val === row2[idx])) {
|
||||
const key = `${name}|${j}`;
|
||||
if (!reported.has(key)) {
|
||||
errors.push({ name, row: j });
|
||||
reported.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
const validProps = new Set(["grid-template-areas", "grid-template", "grid"]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidNamedGridAreasRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow invalid named grid areas",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-named-grid-areas.md",
|
||||
},
|
||||
messages: {
|
||||
emptyGridArea: "Grid area must contain at least one cell token.",
|
||||
unevenGridArea: "Grid area strings must have the same number of cell tokens.",
|
||||
nonRectangularGridArea: "Cell tokens with name '{{name}}' must form a rectangle.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Declaration(node) {
|
||||
const propName = node.property.toLowerCase();
|
||||
if (validProps.has(propName) &&
|
||||
node.value.type === "Value" &&
|
||||
node.value.children.length > 0) {
|
||||
const stringNodes = node.value.children.filter(child => child.type === "String");
|
||||
if (stringNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const grid = [];
|
||||
const emptyNodes = [];
|
||||
const unevenNodes = [];
|
||||
let firstRowLen = null;
|
||||
for (const stringNode of stringNodes) {
|
||||
const trimmedValue = stringNode.value.trim();
|
||||
if (trimmedValue === "") {
|
||||
emptyNodes.push(stringNode);
|
||||
continue;
|
||||
}
|
||||
const row = trimmedValue.split(" ").filter(Boolean);
|
||||
grid.push(row);
|
||||
if (firstRowLen === null) {
|
||||
firstRowLen = row.length;
|
||||
}
|
||||
else if (row.length !== firstRowLen) {
|
||||
unevenNodes.push(stringNode);
|
||||
}
|
||||
}
|
||||
if (emptyNodes.length > 0) {
|
||||
emptyNodes.forEach(emptyNode => context.report({
|
||||
node: emptyNode,
|
||||
messageId: "emptyGridArea",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (unevenNodes.length > 0) {
|
||||
unevenNodes.forEach(unevenNode => context.report({
|
||||
node: unevenNode,
|
||||
messageId: "unevenGridArea",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const nonRectErrors = findNonRectangularAreas(grid);
|
||||
nonRectErrors.forEach(({ name, row }) => {
|
||||
const stringNode = stringNodes[row];
|
||||
context.report({
|
||||
node: stringNode,
|
||||
messageId: "nonRectangularGridArea",
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowUnknownVariables: {
|
||||
type: "boolean";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowUnknownVariables: false;
|
||||
}];
|
||||
namespace messages {
|
||||
let invalidPropertyValue: string;
|
||||
let unknownProperty: string;
|
||||
let unknownVar: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: NoInvalidPropertiesOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: NoInvalidPropertiesMessageIds;
|
||||
}>): {
|
||||
"Rule > Block Declaration"(): void;
|
||||
"Rule > Block Declaration > Value > *:not(Function)"(node: any): void;
|
||||
Function(): void;
|
||||
"Function > *:not(Function)"(node: any): void;
|
||||
"Function:exit"(node: FunctionNodePlain): void;
|
||||
"Rule > Block Declaration:exit"(node: any): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoInvalidPropertiesMessageIds = "invalidPropertyValue" | "unknownProperty" | "unknownVar";
|
||||
export type NoInvalidPropertiesOptions = [{
|
||||
allowUnknownVariables?: boolean;
|
||||
}];
|
||||
export type NoInvalidPropertiesRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: NoInvalidPropertiesOptions;
|
||||
MessageIds: NoInvalidPropertiesMessageIds;
|
||||
}>;
|
||||
import type { FunctionNodePlain } from "@eslint/css-tree";
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* @fileoverview Rule to prevent invalid properties in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { isSyntaxMatchError, isSyntaxReferenceError } from "../util.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @import { ValuePlain, FunctionNodePlain, CssLocationRange } from "@eslint/css-tree";
|
||||
* @typedef {"invalidPropertyValue" | "unknownProperty" | "unknownVar"} NoInvalidPropertiesMessageIds
|
||||
* @typedef {[{allowUnknownVariables?: boolean}]} NoInvalidPropertiesOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: NoInvalidPropertiesOptions, MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Regex to match var() functional notation with optional fallback.
|
||||
*/
|
||||
const varFunctionPattern = /var\(\s*(--[^,\s)]+)\s*(?:,([\s\S]+))?\)/iu;
|
||||
/**
|
||||
* Parses a var() function text and extracts the custom property name and fallback.
|
||||
* @param {string} text The text containing a var() function.
|
||||
* @returns {{ name: string, fallbackText: string | null } | null} The parsed variable name and optional fallback, or null if not a var().
|
||||
*/
|
||||
function parseVarFunction(text) {
|
||||
const match = text.match(varFunctionPattern);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: match[1].trim(),
|
||||
fallbackText: match[2]?.trim(),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Extracts the list of fallback value or variable name used in a `var()` that is used as fallback function.
|
||||
* For example, for `var(--my-color, var(--fallback-color, red));` it will return `["--fallback-color", "red"]`.
|
||||
* @param {string} value The fallback value that is used in `var()`.
|
||||
* @returns {Array<string>} The list of variable names of fallback value.
|
||||
*/
|
||||
function getVarFallbackList(value) {
|
||||
const list = [];
|
||||
let currentValue = value;
|
||||
while (true) {
|
||||
const parsed = parseVarFunction(currentValue);
|
||||
if (!parsed) {
|
||||
break;
|
||||
}
|
||||
list.push(parsed.name);
|
||||
if (!parsed.fallbackText) {
|
||||
break;
|
||||
}
|
||||
// If fallback is not another var(), we're done
|
||||
if (!parsed.fallbackText.toLowerCase().includes("var(")) {
|
||||
list.push(parsed.fallbackText);
|
||||
break;
|
||||
}
|
||||
// Continue parsing from fallback
|
||||
currentValue = parsed.fallbackText;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoInvalidPropertiesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow invalid properties",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-properties.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowUnknownVariables: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowUnknownVariables: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
invalidPropertyValue: "Invalid value '{{value}}' for property '{{property}}'. Expected {{expected}}.",
|
||||
unknownProperty: "Unknown property '{{property}}' found.",
|
||||
unknownVar: "Can't validate with unknown variable '{{var}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const lexer = sourceCode.lexer;
|
||||
/** @type {Map<string,ValuePlain>} */
|
||||
const vars = new Map();
|
||||
/**
|
||||
* @type {Array<{
|
||||
* valueParts: string[],
|
||||
* functionPartsStack: string[][],
|
||||
* valueSegmentLocs: Map<string,CssLocationRange>,
|
||||
* skipValidation: boolean,
|
||||
* hadVarSubstitution: boolean,
|
||||
* resolvedCache: Map<string,string>
|
||||
* }>}
|
||||
*/
|
||||
const declStack = [];
|
||||
const [{ allowUnknownVariables }] = context.options;
|
||||
/**
|
||||
* Iteratively resolves CSS variable references until a value is found.
|
||||
* @param {string} variableName The variable name to resolve
|
||||
* @param {Map<string, string>} cache Cache for memoization within a single resolution scope
|
||||
* @param {Set<string>} [seen] Set of already seen variables to detect cycles
|
||||
* @returns {string|null} The resolved value or null if not found
|
||||
*/
|
||||
function resolveVariable(variableName, cache, seen = new Set()) {
|
||||
/** @type {Array<string>} */
|
||||
const fallbackStack = [];
|
||||
let currentVarName = variableName;
|
||||
/*
|
||||
* Resolves a CSS variable by following its reference chain.
|
||||
*
|
||||
* Phase 1: Follow var() references
|
||||
* - Use `seen` to detect cycles
|
||||
* - Use `cache` for memoization
|
||||
* - If value is concrete: cache and return
|
||||
* - If value is another var(--next, <fallback>):
|
||||
* push fallback to stack and continue with --next
|
||||
* - If variable unknown: proceed to Phase 2
|
||||
*
|
||||
* Phase 2: Try fallback values (if Phase 1 failed)
|
||||
* - Process fallbacks in reverse order (LIFO)
|
||||
* - Resolve each via resolveFallback()
|
||||
* - Return first successful resolution
|
||||
*/
|
||||
while (true) {
|
||||
if (seen.has(currentVarName)) {
|
||||
break;
|
||||
}
|
||||
seen.add(currentVarName);
|
||||
if (cache.has(currentVarName)) {
|
||||
return cache.get(currentVarName);
|
||||
}
|
||||
const valueNode = vars.get(currentVarName);
|
||||
if (!valueNode) {
|
||||
break;
|
||||
}
|
||||
const valueText = sourceCode.getText(valueNode).trim();
|
||||
const parsed = parseVarFunction(valueText);
|
||||
if (!parsed) {
|
||||
cache.set(currentVarName, valueText);
|
||||
return valueText;
|
||||
}
|
||||
if (parsed.fallbackText) {
|
||||
fallbackStack.push(parsed.fallbackText);
|
||||
}
|
||||
currentVarName = parsed.name;
|
||||
}
|
||||
while (fallbackStack.length > 0) {
|
||||
const fallbackText = fallbackStack.pop();
|
||||
// eslint-disable-next-line no-use-before-define -- resolveFallback and resolveVariable are mutually recursive
|
||||
const resolvedFallback = resolveFallback(fallbackText, cache, seen);
|
||||
if (resolvedFallback !== null) {
|
||||
return resolvedFallback;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Resolves a fallback text which can contain nested var() calls.
|
||||
* Returns the first resolvable value or null if none resolve.
|
||||
* @param {string} rawFallbackText The raw fallback text to resolve.
|
||||
* @param {Map<string, string>} cache Cache for memoization within a single resolution scope.
|
||||
* @param {Set<string>} [seen] Set of already seen variables to detect cycles.
|
||||
* @returns {string | null} The resolved fallback value, or null if none can be resolved.
|
||||
*/
|
||||
function resolveFallback(rawFallbackText, cache, seen = new Set()) {
|
||||
const fallbackVarList = getVarFallbackList(rawFallbackText);
|
||||
if (fallbackVarList.length === 0) {
|
||||
return rawFallbackText;
|
||||
}
|
||||
for (const fallbackCandidate of fallbackVarList) {
|
||||
if (fallbackCandidate.startsWith("--")) {
|
||||
const resolved = resolveVariable(fallbackCandidate, cache, seen);
|
||||
if (resolved !== null) {
|
||||
return resolved;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return fallbackCandidate.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Process a var function node and add its resolved value to the value list
|
||||
* @param {Object} varNode The var() function node
|
||||
* @param {string[]} valueList Array to collect processed values
|
||||
* @param {Map<string,CssLocationRange>} valueSegmentLocs Map of rebuilt value segments to their locations
|
||||
* @param {Map<string, string>} resolvedCache Cache for resolved variable values to prevent redundant lookups
|
||||
* @returns {boolean} Whether processing was successful
|
||||
*/
|
||||
function processVarFunction(varNode, valueList, valueSegmentLocs, resolvedCache) {
|
||||
const varValue = vars.get(varNode.children[0].name);
|
||||
if (varValue) {
|
||||
const resolvedValue = resolveVariable(varNode.children[0].name, resolvedCache);
|
||||
if (resolvedValue) {
|
||||
valueList.push(resolvedValue);
|
||||
valueSegmentLocs.set(resolvedValue, varNode.loc);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If the variable is not found and doesn't have a fallback value, report it
|
||||
if (varNode.children.length === 1) {
|
||||
if (!allowUnknownVariables) {
|
||||
context.report({
|
||||
loc: varNode.children[0].loc,
|
||||
messageId: "unknownVar",
|
||||
data: { var: varNode.children[0].name },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Handle fallback values
|
||||
if (varNode.children[2].type !== "Raw") {
|
||||
return true;
|
||||
}
|
||||
const fallbackValue = varNode.children[2].value.trim();
|
||||
const resolvedFallbackValue = resolveFallback(fallbackValue, resolvedCache);
|
||||
if (resolvedFallbackValue) {
|
||||
valueList.push(resolvedFallbackValue);
|
||||
valueSegmentLocs.set(resolvedFallbackValue, varNode.loc);
|
||||
return true;
|
||||
}
|
||||
// No valid fallback found
|
||||
if (!allowUnknownVariables) {
|
||||
context.report({
|
||||
loc: varNode.children[0].loc,
|
||||
messageId: "unknownVar",
|
||||
data: { var: varNode.children[0].name },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return {
|
||||
"Rule > Block Declaration"() {
|
||||
declStack.push({
|
||||
valueParts: [],
|
||||
functionPartsStack: [],
|
||||
valueSegmentLocs: new Map(),
|
||||
skipValidation: false,
|
||||
hadVarSubstitution: false,
|
||||
/**
|
||||
* Cache for resolved variable values within this single declaration.
|
||||
* Prevents re-resolving the same variable and re-walking long `var()` chains.
|
||||
*/
|
||||
resolvedCache: new Map(),
|
||||
});
|
||||
},
|
||||
"Rule > Block Declaration > Value > *:not(Function)"(node) {
|
||||
const state = declStack.at(-1);
|
||||
const text = sourceCode.getText(node).trim();
|
||||
state.valueParts.push(text);
|
||||
state.valueSegmentLocs.set(text, node.loc);
|
||||
},
|
||||
Function() {
|
||||
const state = declStack.at(-1);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
state.functionPartsStack.push([]);
|
||||
},
|
||||
"Function > *:not(Function)"(node) {
|
||||
const state = declStack.at(-1);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const parts = state.functionPartsStack.at(-1);
|
||||
const text = sourceCode.getText(node).trim();
|
||||
parts.push(text);
|
||||
state.valueSegmentLocs.set(text, node.loc);
|
||||
},
|
||||
"Function:exit"(node) {
|
||||
const state = declStack.at(-1);
|
||||
if (!state || state.skipValidation) {
|
||||
return;
|
||||
}
|
||||
const parts = state.functionPartsStack.pop();
|
||||
let result;
|
||||
if (node.name.toLowerCase() === "var") {
|
||||
const resolvedParts = [];
|
||||
const success = processVarFunction(node, resolvedParts, state.valueSegmentLocs, state.resolvedCache);
|
||||
if (!success) {
|
||||
state.skipValidation = true;
|
||||
return;
|
||||
}
|
||||
if (resolvedParts.length === 0) {
|
||||
return;
|
||||
}
|
||||
state.hadVarSubstitution = true;
|
||||
result = resolvedParts[0];
|
||||
}
|
||||
else {
|
||||
result = `${node.name}(${parts.join(" ")})`;
|
||||
}
|
||||
const parentParts = state.functionPartsStack.at(-1);
|
||||
if (parentParts) {
|
||||
parentParts.push(result);
|
||||
}
|
||||
else {
|
||||
state.valueParts.push(result);
|
||||
}
|
||||
},
|
||||
"Rule > Block Declaration:exit"(node) {
|
||||
const state = declStack.pop();
|
||||
if (node.property.startsWith("--")) {
|
||||
// store the custom property name and value to validate later
|
||||
vars.set(node.property, node.value);
|
||||
// don't validate custom properties
|
||||
return;
|
||||
}
|
||||
if (state.skipValidation) {
|
||||
return;
|
||||
}
|
||||
let value = node.value;
|
||||
if (state.hadVarSubstitution) {
|
||||
const valueList = state.valueParts;
|
||||
value =
|
||||
valueList.length > 0
|
||||
? valueList.join(" ")
|
||||
: sourceCode.getText(node.value);
|
||||
}
|
||||
const { error } = lexer.matchProperty(node.property, value);
|
||||
if (error) {
|
||||
// validation failure
|
||||
if (isSyntaxMatchError(error)) {
|
||||
const errorValue = state.hadVarSubstitution &&
|
||||
value.slice(error.mismatchOffset, error.mismatchOffset + error.mismatchLength);
|
||||
context.report({
|
||||
/*
|
||||
* When using variables, check to see if the error
|
||||
* occurred at a location where a variable was replaced.
|
||||
* If so, use that location; otherwise, use the error's
|
||||
* reported location.
|
||||
*/
|
||||
loc: state.hadVarSubstitution
|
||||
? (state.valueSegmentLocs.get(errorValue) ??
|
||||
node.value.loc)
|
||||
: error.loc,
|
||||
messageId: "invalidPropertyValue",
|
||||
data: {
|
||||
property: node.property,
|
||||
/*
|
||||
* When using variables, slice the value to
|
||||
* only include the part that caused the error.
|
||||
* Otherwise, use the full value from the error.
|
||||
*/
|
||||
value: state.hadVarSubstitution
|
||||
? errorValue
|
||||
: error.css,
|
||||
expected: error.syntax,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!allowUnknownVariables ||
|
||||
isSyntaxReferenceError(error)) {
|
||||
// unknown property
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.loc.start,
|
||||
end: {
|
||||
line: node.loc.start.line,
|
||||
column: node.loc.start.column +
|
||||
node.property.length,
|
||||
},
|
||||
},
|
||||
messageId: "unknownProperty",
|
||||
data: {
|
||||
property: node.property,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
namespace messages {
|
||||
let unmatchableSelector: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: [];
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "unmatchableSelector";
|
||||
}>): {
|
||||
AnPlusB(node: import("@eslint/css-tree").AnPlusB): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type NoUnmatchableSelectorsMessageIds = "unmatchableSelector";
|
||||
export type NoUnmatchableSelectorsRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: [];
|
||||
MessageIds: NoUnmatchableSelectorsMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Rule to disallow unmatchable selectors.
|
||||
* @author TKDev7
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"unmatchableSelector"} NoUnmatchableSelectorsMessageIds
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoUnmatchableSelectorsMessageIds }>} NoUnmatchableSelectorsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {NoUnmatchableSelectorsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Disallow unmatchable selectors",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/no-unmatchable-selectors.md",
|
||||
},
|
||||
messages: {
|
||||
unmatchableSelector: "Unexpected unmatchable selector '{{selector}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
return {
|
||||
AnPlusB(node) {
|
||||
// Either node.a or node.b can be null; Number(null) === 0.
|
||||
// This coercion is intentional so that omitted coefficients are treated as 0.
|
||||
const a = Number(node.a);
|
||||
const b = Number(node.b);
|
||||
if (a <= 0 && b <= 0) {
|
||||
const pseudo = sourceCode.getParent(sourceCode.getParent(node));
|
||||
context.report({
|
||||
loc: pseudo.loc,
|
||||
messageId: "unmatchableSelector",
|
||||
data: { selector: sourceCode.getText(pseudo) },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
let hasSuggestions: true;
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowProperties: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowUnits: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowProperties: any[];
|
||||
allowUnits: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let notLogicalProperty: string;
|
||||
let notLogicalValue: string;
|
||||
let notLogicalUnit: string;
|
||||
let replaceWithLogicalProperty: string;
|
||||
let replaceWithLogicalValue: string;
|
||||
let replaceWithLogicalUnit: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: PreferLogicalPropertiesOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: PreferLogicalPropertiesMessageIds;
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
Dimension(node: import("@eslint/css-tree").Dimension): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type PreferLogicalPropertiesMessageIds = "notLogicalProperty" | "notLogicalValue" | "notLogicalUnit" | "replaceWithLogicalProperty" | "replaceWithLogicalValue" | "replaceWithLogicalUnit";
|
||||
export type PreferLogicalPropertiesOptions = [{
|
||||
allowProperties?: string[];
|
||||
allowUnits?: string[];
|
||||
}];
|
||||
export type PreferLogicalPropertiesRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: PreferLogicalPropertiesOptions;
|
||||
MessageIds: PreferLogicalPropertiesMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"notLogicalProperty" | "notLogicalValue" | "notLogicalUnit" | "replaceWithLogicalProperty" | "replaceWithLogicalValue" | "replaceWithLogicalUnit"} PreferLogicalPropertiesMessageIds
|
||||
* @typedef {[{
|
||||
* allowProperties?: string[],
|
||||
* allowUnits?: string[]
|
||||
* }]} PreferLogicalPropertiesOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: PreferLogicalPropertiesOptions, MessageIds: PreferLogicalPropertiesMessageIds }>} PreferLogicalPropertiesRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const propertiesReplacements = new Map([
|
||||
["bottom", "inset-block-end"],
|
||||
["border-bottom", "border-block-end"],
|
||||
["border-bottom-color", "border-block-end-color"],
|
||||
["border-bottom-left-radius", "border-end-start-radius"],
|
||||
["border-bottom-right-radius", "border-end-end-radius"],
|
||||
["border-bottom-style", "border-block-end-style"],
|
||||
["border-bottom-width", "border-block-end-width"],
|
||||
["border-left", "border-inline-start"],
|
||||
["border-left-color", "border-inline-start-color"],
|
||||
["border-left-style", "border-inline-start-style"],
|
||||
["border-left-width", "border-inline-start-width"],
|
||||
["border-right", "border-inline-end"],
|
||||
["border-right-color", "border-inline-end-color"],
|
||||
["border-right-style", "border-inline-end-style"],
|
||||
["border-right-width", "border-inline-end-width"],
|
||||
["border-top", "border-block-start"],
|
||||
["border-top-color", "border-block-start-color"],
|
||||
["border-top-left-radius", "border-start-start-radius"],
|
||||
["border-top-right-radius", "border-start-end-radius"],
|
||||
["border-top-style", "border-block-start-style"],
|
||||
["border-top-width", "border-block-start-width"],
|
||||
["contain-intrinsic-height", "contain-intrinsic-block-size"],
|
||||
["contain-intrinsic-width", "contain-intrinsic-inline-size"],
|
||||
["height", "block-size"],
|
||||
["left", "inset-inline-start"],
|
||||
["margin-bottom", "margin-block-end"],
|
||||
["margin-left", "margin-inline-start"],
|
||||
["margin-right", "margin-inline-end"],
|
||||
["margin-top", "margin-block-start"],
|
||||
["max-height", "max-block-size"],
|
||||
["max-width", "max-inline-size"],
|
||||
["min-height", "min-block-size"],
|
||||
["min-width", "min-inline-size"],
|
||||
["overflow-x", "overflow-inline"],
|
||||
["overflow-y", "overflow-block"],
|
||||
["overscroll-behavior-x", "overscroll-behavior-inline"],
|
||||
["overscroll-behavior-y", "overscroll-behavior-block"],
|
||||
["padding-bottom", "padding-block-end"],
|
||||
["padding-left", "padding-inline-start"],
|
||||
["padding-right", "padding-inline-end"],
|
||||
["padding-top", "padding-block-start"],
|
||||
["right", "inset-inline-end"],
|
||||
["scroll-margin-bottom", "scroll-margin-block-end"],
|
||||
["scroll-margin-left", "scroll-margin-inline-start"],
|
||||
["scroll-margin-right", "scroll-margin-inline-end"],
|
||||
["scroll-margin-top", "scroll-margin-block-start"],
|
||||
["scroll-padding-bottom", "scroll-padding-block-end"],
|
||||
["scroll-padding-left", "scroll-padding-inline-start"],
|
||||
["scroll-padding-right", "scroll-padding-inline-end"],
|
||||
["scroll-padding-top", "scroll-padding-block-start"],
|
||||
["top", "inset-block-start"],
|
||||
["width", "inline-size"],
|
||||
]);
|
||||
const propertyValuesReplacements = new Map([
|
||||
[
|
||||
"text-align",
|
||||
{
|
||||
left: "start",
|
||||
right: "end",
|
||||
},
|
||||
],
|
||||
[
|
||||
"resize",
|
||||
{
|
||||
horizontal: "inline",
|
||||
vertical: "block",
|
||||
},
|
||||
],
|
||||
[
|
||||
"caption-side",
|
||||
{
|
||||
left: "inline-start",
|
||||
right: "inline-end",
|
||||
},
|
||||
],
|
||||
[
|
||||
"box-orient",
|
||||
{
|
||||
horizontal: "inline-axis",
|
||||
vertical: "block-axis",
|
||||
},
|
||||
],
|
||||
[
|
||||
"float",
|
||||
{
|
||||
left: "inline-start",
|
||||
right: "inline-end",
|
||||
},
|
||||
],
|
||||
[
|
||||
"clear",
|
||||
{
|
||||
left: "inline-start",
|
||||
right: "inline-end",
|
||||
},
|
||||
],
|
||||
]);
|
||||
const unitReplacements = new Map([
|
||||
["cqh", "cqb"],
|
||||
["cqw", "cqi"],
|
||||
["dvh", "dvb"],
|
||||
["dvw", "dvi"],
|
||||
["lvh", "lvb"],
|
||||
["lvw", "lvi"],
|
||||
["svh", "svb"],
|
||||
["svw", "svi"],
|
||||
["vh", "vb"],
|
||||
["vw", "vi"],
|
||||
]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {PreferLogicalPropertiesRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
hasSuggestions: true,
|
||||
docs: {
|
||||
description: "Enforce the use of logical properties",
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/prefer-logical-properties.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowProperties: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(propertiesReplacements.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowUnits: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(unitReplacements.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowProperties: [],
|
||||
allowUnits: [],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
notLogicalProperty: "Expected logical property '{{replacement}}' instead of '{{property}}'.",
|
||||
notLogicalValue: "Expected logical value '{{replacement}}' instead of '{{value}}'.",
|
||||
notLogicalUnit: "Expected logical unit '{{replacement}}' instead of '{{unit}}'.",
|
||||
replaceWithLogicalProperty: "Replace '{{property}}' with logical property '{{replacement}}'.",
|
||||
replaceWithLogicalValue: "Replace '{{value}}' with logical value '{{replacement}}'.",
|
||||
replaceWithLogicalUnit: "Replace '{{unit}}' with logical unit '{{replacement}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const [{ allowProperties, allowUnits }] = context.options;
|
||||
return {
|
||||
Declaration(node) {
|
||||
const parent = context.sourceCode.getParent(node);
|
||||
if (parent.type === "SupportsDeclaration") {
|
||||
return;
|
||||
}
|
||||
const propertyReplacement = propertiesReplacements.get(node.property);
|
||||
if (propertyReplacement &&
|
||||
!allowProperties.includes(node.property)) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "notLogicalProperty",
|
||||
data: {
|
||||
property: node.property,
|
||||
replacement: propertyReplacement,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: "replaceWithLogicalProperty",
|
||||
data: {
|
||||
property: node.property,
|
||||
replacement: propertyReplacement,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([
|
||||
node.loc.start.offset,
|
||||
node.loc.start.offset +
|
||||
node.property.length,
|
||||
], propertyReplacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
const valueReplacements = propertyValuesReplacements.get(node.property);
|
||||
if (valueReplacements &&
|
||||
node.value.type === "Value" &&
|
||||
node.value.children[0].type === "Identifier") {
|
||||
const identifier = node.value.children[0];
|
||||
const nodeValue = identifier.name;
|
||||
const valueReplacement = valueReplacements[nodeValue];
|
||||
if (valueReplacement) {
|
||||
context.report({
|
||||
loc: identifier.loc,
|
||||
messageId: "notLogicalValue",
|
||||
data: {
|
||||
value: nodeValue,
|
||||
replacement: valueReplacement,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: "replaceWithLogicalValue",
|
||||
data: {
|
||||
value: nodeValue,
|
||||
replacement: valueReplacement,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(identifier, valueReplacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Dimension(node) {
|
||||
const unitReplacement = unitReplacements.get(node.unit);
|
||||
if (unitReplacement && !allowUnits.includes(node.unit)) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "notLogicalUnit",
|
||||
data: {
|
||||
unit: node.unit,
|
||||
replacement: unitReplacement,
|
||||
},
|
||||
suggest: [
|
||||
{
|
||||
messageId: "replaceWithLogicalUnit",
|
||||
data: {
|
||||
unit: node.unit,
|
||||
replacement: unitReplacement,
|
||||
},
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, node.value + unitReplacement);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "suggestion";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowUnits: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowUnits: string[];
|
||||
}];
|
||||
namespace messages {
|
||||
let allowedFontUnits: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: RelativeFontUnitsOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: "allowedFontUnits";
|
||||
}>): {
|
||||
Declaration(node: import("@eslint/css-tree").DeclarationPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type RelativeFontUnitsMessageIds = "allowedFontUnits";
|
||||
export type RelativeFontUnitsOptions = [{
|
||||
allowUnits?: string[];
|
||||
}];
|
||||
export type RelativeFontUnitsRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: RelativeFontUnitsOptions;
|
||||
MessageIds: RelativeFontUnitsMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @fileoverview Enforce the use of relative units for font size.
|
||||
* @author Tanuj Kanti
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"allowedFontUnits"} RelativeFontUnitsMessageIds
|
||||
* @typedef {[{allowUnits?: string[]}]} RelativeFontUnitsOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: RelativeFontUnitsOptions, MessageIds: RelativeFontUnitsMessageIds}>} RelativeFontUnitsRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
const relativeFontUnits = [
|
||||
"%",
|
||||
"cap",
|
||||
"ch",
|
||||
"em",
|
||||
"ex",
|
||||
"ic",
|
||||
"lh",
|
||||
"rcap",
|
||||
"rch",
|
||||
"rem",
|
||||
"rex",
|
||||
"ric",
|
||||
"rlh",
|
||||
];
|
||||
const disallowedFontSizeKeywords = new Set([
|
||||
"xx-small",
|
||||
"x-small",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"x-large",
|
||||
"xx-large",
|
||||
"xxx-large",
|
||||
"math",
|
||||
]);
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {RelativeFontUnitsRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description: "Enforce the use of relative font units",
|
||||
recommended: false,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/relative-font-units.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowUnits: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: relativeFontUnits,
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowUnits: ["rem"],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
allowedFontUnits: "Use only allowed relative units for 'font-size' - {{allowedFontUnits}}.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const [{ allowUnits: allowedFontUnits }] = context.options;
|
||||
return {
|
||||
Declaration(node) {
|
||||
if (node.property === "font-size") {
|
||||
if (node.value.type === "Value" &&
|
||||
node.value.children.length > 0) {
|
||||
const value = node.value.children[0];
|
||||
if ((value.type === "Dimension" &&
|
||||
!allowedFontUnits.includes(value.unit.toLowerCase())) ||
|
||||
(value.type === "Identifier" &&
|
||||
disallowedFontSizeKeywords.has(value.name.toLowerCase())) ||
|
||||
(value.type === "Percentage" &&
|
||||
!allowedFontUnits.includes("%"))) {
|
||||
context.report({
|
||||
loc: value.loc,
|
||||
messageId: "allowedFontUnits",
|
||||
data: {
|
||||
allowedFontUnits: allowedFontUnits.join(", "),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.property === "font") {
|
||||
if (node.value.type === "Value" &&
|
||||
node.value.children.length > 0) {
|
||||
const value = node.value;
|
||||
const dimensionNode = value.children.find(child => child.type === "Dimension");
|
||||
const identifierNode = value.children.find(child => child.type === "Identifier" &&
|
||||
disallowedFontSizeKeywords.has(child.name.toLowerCase()));
|
||||
const percentageNode = value.children.find((child, index) => {
|
||||
const isPercentage = child.type === "Percentage";
|
||||
const previousNode = value.children[index - 1];
|
||||
const previousNodeIsSlashOperator = previousNode &&
|
||||
previousNode.type === "Operator" &&
|
||||
previousNode.value === "/";
|
||||
return (isPercentage && !previousNodeIsSlashOperator);
|
||||
});
|
||||
let location;
|
||||
let shouldReport = false;
|
||||
const conditions = [
|
||||
{
|
||||
check: !allowedFontUnits.includes("%") &&
|
||||
percentageNode,
|
||||
loc: percentageNode?.loc,
|
||||
},
|
||||
{
|
||||
check: identifierNode,
|
||||
loc: identifierNode?.loc,
|
||||
},
|
||||
{
|
||||
check: dimensionNode &&
|
||||
!allowedFontUnits.includes(dimensionNode.unit.toLowerCase()),
|
||||
loc: dimensionNode?.loc,
|
||||
},
|
||||
];
|
||||
for (const condition of conditions) {
|
||||
if (condition.check) {
|
||||
shouldReport = true;
|
||||
location = condition.loc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldReport) {
|
||||
context.report({
|
||||
loc: location,
|
||||
messageId: "allowedFontUnits",
|
||||
data: {
|
||||
allowedFontUnits: allowedFontUnits.join(", "),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
maxIds: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxClasses: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxTypes: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxAttributes: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxPseudoClasses: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxUniversals: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxCompounds: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
maxCombinators: {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
};
|
||||
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;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
maxIds: number;
|
||||
maxClasses: number;
|
||||
maxTypes: number;
|
||||
maxAttributes: number;
|
||||
maxPseudoClasses: number;
|
||||
maxUniversals: number;
|
||||
maxCompounds: number;
|
||||
maxCombinators: number;
|
||||
disallowCombinators: any[];
|
||||
disallowPseudoClasses: any[];
|
||||
disallowPseudoElements: any[];
|
||||
disallowAttributes: any[];
|
||||
disallowAttributeMatchers: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let maxSelectors: string;
|
||||
let disallowedSelectors: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: SelectorComplexityOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: SelectorComplexityMessageIds;
|
||||
}>): {
|
||||
Selector(node: import("@eslint/css-tree").SelectorPlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type SelectorComplexityMessageIds = "maxSelectors" | "disallowedSelectors";
|
||||
export type SelectorComplexityOptions = [{
|
||||
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[];
|
||||
}];
|
||||
export type SelectorComplexityRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: SelectorComplexityOptions;
|
||||
MessageIds: SelectorComplexityMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* @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",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let recommended: boolean;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
available: {
|
||||
anyOf: ({
|
||||
enum: string[];
|
||||
type?: undefined;
|
||||
minimum?: undefined;
|
||||
maximum?: undefined;
|
||||
} | {
|
||||
type: "integer";
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
enum?: undefined;
|
||||
})[];
|
||||
};
|
||||
allowAtRules: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowFunctions: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowMediaConditions: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowProperties: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowPropertyValues: {
|
||||
type: "object";
|
||||
properties: {
|
||||
[k: string]: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
};
|
||||
allowSelectors: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
allowUnits: {
|
||||
type: "array";
|
||||
items: {
|
||||
enum: string[];
|
||||
};
|
||||
uniqueItems: true;
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
available: "widely";
|
||||
allowAtRules: any[];
|
||||
allowFunctions: any[];
|
||||
allowMediaConditions: any[];
|
||||
allowProperties: any[];
|
||||
allowPropertyValues: {};
|
||||
allowSelectors: any[];
|
||||
allowUnits: any[];
|
||||
}];
|
||||
namespace messages {
|
||||
let notBaselineProperty: string;
|
||||
let notBaselinePropertyValue: string;
|
||||
let notBaselineAtRule: string;
|
||||
let notBaselineFunction: string;
|
||||
let notBaselineMediaCondition: string;
|
||||
let notBaselineSelector: string;
|
||||
let notBaselineUnit: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: UseBaselineOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: UseBaselineMessageIds;
|
||||
}>): {
|
||||
"Atrule[name=/^supports$/i]"(): void;
|
||||
"Atrule[name=/^supports$/i] > AtrulePrelude > Condition"(node: any): void;
|
||||
"Rule > Block > Declaration"(node: any): void;
|
||||
"Atrule[name=/^supports$/i]:exit"(): void;
|
||||
"Atrule[name=/^media$/i] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"(node: any): void;
|
||||
Atrule(node: import("@eslint/css-tree").AtrulePlain): void;
|
||||
"PseudoClassSelector,PseudoElementSelector"(node: any): void;
|
||||
NestingSelector(node: import("@eslint/css-tree").NestingSelector): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type UseBaselineMessageIds = "notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineFunction" | "notBaselineMediaCondition" | "notBaselineSelector" | "notBaselineUnit";
|
||||
export type UseBaselineOptions = [{
|
||||
available?: "widely" | "newly" | number;
|
||||
allowAtRules?: string[];
|
||||
allowFunctions?: string[];
|
||||
allowMediaConditions?: string[];
|
||||
allowProperties?: string[];
|
||||
allowPropertyValues?: {
|
||||
[property: string]: string[];
|
||||
};
|
||||
allowSelectors?: string[];
|
||||
allowUnits?: string[];
|
||||
}];
|
||||
export type UseBaselineRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: UseBaselineOptions;
|
||||
MessageIds: UseBaselineMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+868
@@ -0,0 +1,868 @@
|
||||
/**
|
||||
* @fileoverview Rule to enforce the use of baseline features.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
import { BASELINE_HIGH, BASELINE_LOW, properties, propertyValues, atRules, mediaConditions, functions, units, selectors, } from "../data/baseline-data.js";
|
||||
import { namedColors } from "../data/colors.js";
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @import { Identifier, FunctionNodePlain, Dimension } from "@eslint/css-tree"
|
||||
* @typedef {"notBaselineProperty" | "notBaselinePropertyValue" | "notBaselineAtRule" | "notBaselineFunction" | "notBaselineMediaCondition" | "notBaselineSelector" | "notBaselineUnit"} UseBaselineMessageIds
|
||||
* @typedef {[{
|
||||
* available?: "widely" | "newly" | number,
|
||||
* allowAtRules?: string[],
|
||||
* allowFunctions?: string[],
|
||||
* allowMediaConditions?: string[],
|
||||
* allowProperties?: string[],
|
||||
* allowPropertyValues?: { [property: string]: string[] },
|
||||
* allowSelectors?: string[],
|
||||
* allowUnits?: string[]
|
||||
* }]} UseBaselineOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: UseBaselineOptions, MessageIds: UseBaselineMessageIds }>} UseBaselineRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* Represents a property that is supported via `@supports`.
|
||||
*/
|
||||
class SupportedProperty {
|
||||
/**
|
||||
* The name of the property.
|
||||
* @type {string}
|
||||
*/
|
||||
name;
|
||||
/**
|
||||
* Supported identifier values.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#identifiers = new Set();
|
||||
/**
|
||||
* Supported units.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#units = new Set();
|
||||
/**
|
||||
* Supported function types.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#functions = new Set();
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {string} name The name of the property.
|
||||
*/
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
/**
|
||||
* Adds an identifier to the list of supported identifiers.
|
||||
* @param {string} identifier The identifier to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addIdentifier(identifier) {
|
||||
this.#identifiers.add(identifier);
|
||||
}
|
||||
/**
|
||||
* Determines if an identifier is supported.
|
||||
* @param {string} identifier The identifier to check.
|
||||
* @returns {boolean} `true` if the identifier is supported, `false` if not.
|
||||
*/
|
||||
hasIdentifier(identifier) {
|
||||
return this.#identifiers.has(identifier);
|
||||
}
|
||||
/**
|
||||
* Determines if any identifiers are supported.
|
||||
* @returns {boolean} `true` if any identifiers are supported, `false` if not.
|
||||
*/
|
||||
hasIdentifiers() {
|
||||
return this.#identifiers.size > 0;
|
||||
}
|
||||
/**
|
||||
* Adds a unit to the list of supported units.
|
||||
* @param {string} unit The unit to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addUnit(unit) {
|
||||
this.#units.add(unit);
|
||||
}
|
||||
/**
|
||||
* Determines if a unit is supported.
|
||||
* @param {string} unit The unit to check.
|
||||
* @returns {boolean} `true` if the unit is supported, `false` if not.
|
||||
*/
|
||||
hasUnit(unit) {
|
||||
return this.#units.has(unit);
|
||||
}
|
||||
/**
|
||||
* Determines if any units are supported.
|
||||
* @returns {boolean} `true` if any units are supported, `false` if not.
|
||||
*/
|
||||
hasUnits() {
|
||||
return this.#units.size > 0;
|
||||
}
|
||||
/**
|
||||
* Adds a function to the list of supported functions.
|
||||
* @param {string} func The function to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addFunction(func) {
|
||||
this.#functions.add(func);
|
||||
}
|
||||
/**
|
||||
* Determines if a function is supported.
|
||||
* @param {string} func The function to check.
|
||||
* @returns {boolean} `true` if the function is supported, `false` if not.
|
||||
*/
|
||||
hasFunction(func) {
|
||||
return this.#functions.has(func);
|
||||
}
|
||||
/**
|
||||
* Determines if any functions are supported.
|
||||
* @returns {boolean} `true` if any functions are supported, `false` if not.
|
||||
*/
|
||||
hasFunctions() {
|
||||
return this.#functions.size > 0;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents an `@supports` rule and everything it enables.
|
||||
*/
|
||||
class SupportsRule {
|
||||
/**
|
||||
* The properties supported by this rule.
|
||||
* @type {Map<string, SupportedProperty>}
|
||||
*/
|
||||
#properties = new Map();
|
||||
/**
|
||||
* The selectors supported by this rule.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
#selectors = new Set();
|
||||
/**
|
||||
* Adds a property to the rule.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {SupportedProperty} The supported property object.
|
||||
*/
|
||||
addProperty(property) {
|
||||
if (this.#properties.has(property)) {
|
||||
return this.#properties.get(property);
|
||||
}
|
||||
const supportedProperty = new SupportedProperty(property);
|
||||
this.#properties.set(property, supportedProperty);
|
||||
return supportedProperty;
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a property.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if the property is supported, `false` if not.
|
||||
*/
|
||||
hasProperty(property) {
|
||||
return this.#properties.has(property);
|
||||
}
|
||||
/**
|
||||
* Gets the supported property.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {SupportedProperty} The supported property.
|
||||
*/
|
||||
getProperty(property) {
|
||||
return this.#properties.get(property);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a property value.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} identifier The identifier to check.
|
||||
* @returns {boolean} `true` if the property value is supported, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifier(property, identifier) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasIdentifier(identifier);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports any property values.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any property values are supported, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifiers(property) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasIdentifiers();
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a function.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} func The function to check.
|
||||
* @returns {boolean} `true` if the function is supported, `false` if not.
|
||||
*/
|
||||
hasFunction(property, func) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasFunction(func);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports any functions.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any functions are supported, `false` if not.
|
||||
*/
|
||||
hasFunctions(property) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasFunctions();
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a unit.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} unit The unit to check.
|
||||
* @returns {boolean} `true` if the unit is supported, `false` if not.
|
||||
*/
|
||||
hasPropertyUnit(property, unit) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasUnit(unit);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports any units.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any units are supported, `false` if not.
|
||||
*/
|
||||
hasPropertyUnits(property) {
|
||||
const supportedProperty = this.#properties.get(property);
|
||||
if (!supportedProperty) {
|
||||
return false;
|
||||
}
|
||||
return supportedProperty.hasUnits();
|
||||
}
|
||||
/**
|
||||
* Adds a selector to the rule.
|
||||
* @param {string} selector The name of the selector.
|
||||
* @returns {void}
|
||||
*/
|
||||
addSelector(selector) {
|
||||
this.#selectors.add(selector);
|
||||
}
|
||||
/**
|
||||
* Determines if the rule supports a selector.
|
||||
* @param {string} selector The name of the selector.
|
||||
* @returns {boolean} `true` if the selector is supported, `false` if not.
|
||||
*/
|
||||
hasSelector(selector) {
|
||||
return this.#selectors.has(selector);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents a collection of supports rules.
|
||||
*/
|
||||
class SupportsRules {
|
||||
/**
|
||||
* A collection of supports rules.
|
||||
* @type {Array<SupportsRule>}
|
||||
*/
|
||||
#rules = [];
|
||||
/**
|
||||
* Adds a rule to the collection.
|
||||
* @param {SupportsRule} rule The rule to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
push(rule) {
|
||||
this.#rules.push(rule);
|
||||
}
|
||||
/**
|
||||
* Removes the last rule from the collection.
|
||||
* @returns {SupportsRule} The last rule in the collection.
|
||||
*/
|
||||
pop() {
|
||||
return this.#rules.pop();
|
||||
}
|
||||
/**
|
||||
* Retrieves the last rule in the collection.
|
||||
* @returns {SupportsRule} The last rule in the collection.
|
||||
*/
|
||||
last() {
|
||||
return this.#rules.at(-1);
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a property.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any rule supports the property, `false` if not.
|
||||
*/
|
||||
hasProperty(property) {
|
||||
return this.#rules.some(rule => rule.hasProperty(property));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a property identifier.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} identifier The identifier to check.
|
||||
* @returns {boolean} `true` if any rule supports the property value, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifier(property, identifier) {
|
||||
return this.#rules.some(rule => rule.hasPropertyIdentifier(property, identifier));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports any property identifiers.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any rule supports the property values, `false` if not.
|
||||
*/
|
||||
hasPropertyIdentifiers(property) {
|
||||
return this.#rules.some(rule => rule.hasPropertyIdentifiers(property));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a function.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} func The function to check.
|
||||
* @returns {boolean} `true` if any rule supports the function, `false` if not.
|
||||
*/
|
||||
hasPropertyFunction(property, func) {
|
||||
return this.#rules.some(rule => rule.hasFunction(property, func));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports any functions.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {boolean} `true` if any rule supports the functions, `false` if not.
|
||||
*/
|
||||
hasPropertyFunctions(property) {
|
||||
return this.#rules.some(rule => rule.hasFunctions(property));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a unit.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {string} unit The unit to check.
|
||||
* @returns {boolean} `true` if any rule supports the unit, `false` if not.
|
||||
*/
|
||||
hasPropertyUnit(property, unit) {
|
||||
return this.#rules.some(rule => rule.hasPropertyUnit(property, unit));
|
||||
}
|
||||
/**
|
||||
* Determines if any rule supports a selector.
|
||||
* @param {string} selector The name of the selector.
|
||||
* @returns {boolean} `true` if any rule supports the selector, `false` if not.
|
||||
*/
|
||||
hasSelector(selector) {
|
||||
return this.#rules.some(rule => rule.hasSelector(selector));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Represents the required availability of a feature.
|
||||
*/
|
||||
class BaselineAvailability {
|
||||
/**
|
||||
* The preferred Baseline year.
|
||||
* @type {number}
|
||||
*/
|
||||
#baselineYear = undefined;
|
||||
/**
|
||||
* The preferred Baseline status.
|
||||
* @type {number}
|
||||
*/
|
||||
#baselineStatus = undefined;
|
||||
/**
|
||||
* @param {string | number} availability The required level of feature availability.
|
||||
*/
|
||||
constructor(availability) {
|
||||
this.availability = availability;
|
||||
if (typeof availability === "number") {
|
||||
this.#baselineYear = availability;
|
||||
}
|
||||
else {
|
||||
this.#baselineStatus =
|
||||
availability === "widely" ? BASELINE_HIGH : BASELINE_LOW;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines whether a feature meets the required availability.
|
||||
* @param {Object} encodedStatus A feature's encoded baseline status and year.
|
||||
* @returns {boolean} `true` if the feature is supported, `false` if not.
|
||||
*/
|
||||
isSupported(encodedStatus) {
|
||||
if (!encodedStatus) {
|
||||
// if we don't know the status, assume it's supported
|
||||
return true;
|
||||
}
|
||||
const parts = encodedStatus.split(":");
|
||||
const status = Number(parts[0]);
|
||||
const year = Number(parts[1] || NaN);
|
||||
if (this.#baselineYear) {
|
||||
return year <= this.#baselineYear;
|
||||
}
|
||||
return status >= this.#baselineStatus;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {UseBaselineRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Enforce the use of baseline features",
|
||||
recommended: true,
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/use-baseline.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
available: {
|
||||
anyOf: [
|
||||
{
|
||||
enum: ["widely", "newly"],
|
||||
},
|
||||
{
|
||||
// baseline year
|
||||
type: "integer",
|
||||
minimum: 2000,
|
||||
maximum: new Date().getFullYear(),
|
||||
},
|
||||
],
|
||||
},
|
||||
allowAtRules: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(atRules.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowFunctions: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(functions.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowMediaConditions: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(mediaConditions.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowProperties: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(properties.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowPropertyValues: {
|
||||
type: "object",
|
||||
properties: Object.fromEntries(Array.from(propertyValues.entries()).map(([prop, valuesMap]) => [
|
||||
prop,
|
||||
{
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(valuesMap.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
])),
|
||||
additionalProperties: false,
|
||||
},
|
||||
allowSelectors: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(selectors.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
allowUnits: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: Array.from(units.keys()),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
available: "widely",
|
||||
allowAtRules: [],
|
||||
allowFunctions: [],
|
||||
allowMediaConditions: [],
|
||||
allowProperties: [],
|
||||
allowPropertyValues: {},
|
||||
allowSelectors: [],
|
||||
allowUnits: [],
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
notBaselineProperty: "Property '{{property}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselinePropertyValue: "Value '{{value}}' of property '{{property}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineAtRule: "At-rule '@{{atRule}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineFunction: "Function '{{function}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineMediaCondition: "Media condition '{{condition}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineSelector: "Selector '{{selector}}' is not a {{availability}} available baseline feature.",
|
||||
notBaselineUnit: "Unit '{{unit}}' is not a {{availability}} available baseline feature.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const baselineAvailability = new BaselineAvailability(context.options[0].available);
|
||||
const supportsRules = new SupportsRules();
|
||||
const allowAtRules = new Set(context.options[0].allowAtRules);
|
||||
const allowProperties = new Set(context.options[0].allowProperties);
|
||||
const allowSelectors = new Set(context.options[0].allowSelectors);
|
||||
const allowFunctions = new Set(context.options[0].allowFunctions);
|
||||
const allowMediaConditions = new Set(context.options[0].allowMediaConditions);
|
||||
const allowUnits = new Set(context.options[0].allowUnits);
|
||||
const allowPropertyValuesMap = new Map();
|
||||
for (const [prop, values] of Object.entries(context.options[0].allowPropertyValues)) {
|
||||
allowPropertyValuesMap.set(prop, new Set(values));
|
||||
}
|
||||
/**
|
||||
* Checks a property value identifier to see if it's a baseline feature.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {Identifier} child The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkPropertyValueIdentifier(property, child) {
|
||||
// named colors are always valid
|
||||
if (namedColors.has(child.name)) {
|
||||
return;
|
||||
}
|
||||
const allowedValues = allowPropertyValuesMap.get(property);
|
||||
if (allowedValues?.has(child.name)) {
|
||||
return;
|
||||
}
|
||||
const possiblePropertyValues = propertyValues.get(property);
|
||||
// if we don't know of any possible property values, just skip it
|
||||
if (!possiblePropertyValues) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = possiblePropertyValues.get(child.name);
|
||||
// if we don't know of any possible property values, just skip it
|
||||
if (featureStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: child.loc,
|
||||
messageId: "notBaselinePropertyValue",
|
||||
data: {
|
||||
property,
|
||||
value: child.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks a property value function to see if it's a baseline feature.
|
||||
* @param {FunctionNodePlain} child The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkPropertyValueFunction(child) {
|
||||
if (allowFunctions.has(child.name)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = functions.get(child.name);
|
||||
// if we don't know of any possible property values, just skip it
|
||||
if (featureStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: child.loc,
|
||||
messageId: "notBaselineFunction",
|
||||
data: {
|
||||
function: child.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks a property value unit to see if it's a baseline feature.
|
||||
* @param {string} property The name of the property.
|
||||
* @param {Dimension} child The node to check.
|
||||
* @returns {void}
|
||||
*/
|
||||
function checkPropertyValueUnit(property, child) {
|
||||
if (allowUnits.has(child.unit)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = units.get(child.unit);
|
||||
// if we don't know of this unit, just skip it
|
||||
if (featureStatus === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: child.loc,
|
||||
messageId: "notBaselineUnit",
|
||||
data: {
|
||||
unit: child.unit,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
"Atrule[name=/^supports$/i]"() {
|
||||
supportsRules.push(new SupportsRule());
|
||||
},
|
||||
"Atrule[name=/^supports$/i] > AtrulePrelude > Condition"(node) {
|
||||
const supportsRule = supportsRules.last();
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const conditionChild = node.children[i];
|
||||
// if a SupportsDeclaration is preceded by "not" then we don't consider it
|
||||
if (conditionChild.type === "Identifier" &&
|
||||
conditionChild.name === "not") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// save the supported properties and values for this at-rule
|
||||
if (conditionChild.type === "SupportsDeclaration") {
|
||||
const { declaration } = conditionChild;
|
||||
const property = declaration.property;
|
||||
const supportedProperty = supportsRule.addProperty(property);
|
||||
declaration.value.children.forEach(child => {
|
||||
if (child.type === "Identifier") {
|
||||
supportedProperty.addIdentifier(child.name);
|
||||
return;
|
||||
}
|
||||
if (child.type === "Dimension") {
|
||||
supportedProperty.addUnit(child.unit);
|
||||
return;
|
||||
}
|
||||
if (child.type === "Function") {
|
||||
supportedProperty.addFunction(child.name);
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (conditionChild.type === "FeatureFunction" &&
|
||||
conditionChild.feature === "selector") {
|
||||
for (const selectorChild of conditionChild.value
|
||||
.children) {
|
||||
supportsRule.addSelector(selectorChild.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rule > Block > Declaration"(node) {
|
||||
const property = node.property;
|
||||
// ignore unknown properties - no-invalid-properties already catches this
|
||||
if (!properties.has(property)) {
|
||||
return;
|
||||
}
|
||||
if (allowProperties.has(property)) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Step 1: Check that the property is in the baseline.
|
||||
*
|
||||
* If the property has been tested in a @supports rule, we don't need to
|
||||
* check it because it won't be applied if the browser doesn't support it.
|
||||
*/
|
||||
if (!supportsRules.hasProperty(property)) {
|
||||
const featureStatus = properties.get(property);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
context.report({
|
||||
loc: {
|
||||
start: node.loc.start,
|
||||
end: {
|
||||
line: node.loc.start.line,
|
||||
column: node.loc.start.column +
|
||||
node.property.length,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineProperty",
|
||||
data: {
|
||||
property,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
/*
|
||||
* If the property isn't in baseline, then we don't go
|
||||
* on to check the values. If the property itself isn't
|
||||
* in baseline then chances are the values aren't too,
|
||||
* and there's no need to report multiple errors for the
|
||||
* same property.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* With tolerant parsing, it's possible that the value is `Raw`
|
||||
* and therefore doesn't have children. If that's the case then
|
||||
* we just exit.
|
||||
*/
|
||||
if (!node.value?.children) {
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Step 2: Check that the property values are in the baseline.
|
||||
*/
|
||||
for (const child of node.value.children) {
|
||||
if (child.type === "Identifier") {
|
||||
// if the property value has been tested in a @supports rule, don't check it
|
||||
if (!supportsRules.hasPropertyIdentifier(property, child.name)) {
|
||||
checkPropertyValueIdentifier(property, child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (child.type === "Dimension") {
|
||||
if (!supportsRules.hasPropertyUnit(property, child.unit)) {
|
||||
checkPropertyValueUnit(property, child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (child.type === "Function") {
|
||||
if (!supportsRules.hasPropertyFunction(property, child.name)) {
|
||||
checkPropertyValueFunction(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Atrule[name=/^supports$/i]:exit"() {
|
||||
supportsRules.pop();
|
||||
},
|
||||
"Atrule[name=/^media$/i] > AtrulePrelude > MediaQueryList > MediaQuery > Condition"(node) {
|
||||
for (const child of node.children) {
|
||||
// ignore unknown media conditions - no-invalid-at-rules already catches this
|
||||
if (!mediaConditions.has(child.name)) {
|
||||
continue;
|
||||
}
|
||||
if (child.type !== "Feature") {
|
||||
continue;
|
||||
}
|
||||
if (allowMediaConditions.has(child.name)) {
|
||||
continue;
|
||||
}
|
||||
const featureStatus = mediaConditions.get(child.name);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
const loc = child.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: {
|
||||
line: loc.start.line,
|
||||
// add 1 to account for the @ symbol
|
||||
column: loc.start.column + 1,
|
||||
},
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column:
|
||||
// add 1 to account for the @ symbol
|
||||
loc.start.column +
|
||||
child.name.length +
|
||||
1,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineMediaCondition",
|
||||
data: {
|
||||
condition: child.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Atrule(node) {
|
||||
// ignore unknown at-rules - no-invalid-at-rules already catches this
|
||||
const atRuleName = node.name.toLowerCase();
|
||||
if (!atRules.has(atRuleName)) {
|
||||
return;
|
||||
}
|
||||
if (allowAtRules.has(atRuleName)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = atRules.get(atRuleName);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
const loc = node.loc;
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
// add 1 to account for the @ symbol
|
||||
column: loc.start.column + node.name.length + 1,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineAtRule",
|
||||
data: {
|
||||
atRule: node.name,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
"PseudoClassSelector,PseudoElementSelector"(node) {
|
||||
const selector = node.name;
|
||||
if (!selectors.has(selector)) {
|
||||
return;
|
||||
}
|
||||
if (allowSelectors.has(selector)) {
|
||||
return;
|
||||
}
|
||||
// if the selector has been tested in a @supports rule, don't check it
|
||||
if (supportsRules.hasSelector(selector)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = selectors.get(selector);
|
||||
if (!baselineAvailability.isSupported(featureStatus)) {
|
||||
const loc = node.loc;
|
||||
// some selectors are prefixed with the : or :: symbols
|
||||
let prefixSymbolLength = 0;
|
||||
if (node.type.startsWith("PseudoClass")) {
|
||||
prefixSymbolLength = 1;
|
||||
}
|
||||
else if (node.type.startsWith("PseudoElement")) {
|
||||
prefixSymbolLength = 2;
|
||||
}
|
||||
context.report({
|
||||
loc: {
|
||||
start: loc.start,
|
||||
end: {
|
||||
line: loc.start.line,
|
||||
column: loc.start.column +
|
||||
selector.length +
|
||||
prefixSymbolLength,
|
||||
},
|
||||
},
|
||||
messageId: "notBaselineSelector",
|
||||
data: {
|
||||
selector,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
NestingSelector(node) {
|
||||
// NestingSelector implies CSS nesting
|
||||
const selector = "nesting";
|
||||
if (allowSelectors.has(selector)) {
|
||||
return;
|
||||
}
|
||||
const featureStatus = selectors.get(selector);
|
||||
if (baselineAvailability.isSupported(featureStatus)) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "notBaselineSelector",
|
||||
data: {
|
||||
selector,
|
||||
availability: baselineAvailability.availability,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
declare namespace _default {
|
||||
namespace meta {
|
||||
let type: "problem";
|
||||
namespace docs {
|
||||
let description: string;
|
||||
let url: string;
|
||||
}
|
||||
let schema: {
|
||||
type: "object";
|
||||
properties: {
|
||||
allowUnnamedLayers: {
|
||||
type: "boolean";
|
||||
};
|
||||
requireImportLayers: {
|
||||
type: "boolean";
|
||||
};
|
||||
layerNamePattern: {
|
||||
type: "string";
|
||||
};
|
||||
};
|
||||
additionalProperties: false;
|
||||
}[];
|
||||
let defaultOptions: [{
|
||||
allowUnnamedLayers: false;
|
||||
requireImportLayers: true;
|
||||
layerNamePattern: string;
|
||||
}];
|
||||
namespace messages {
|
||||
let missingLayer: string;
|
||||
let missingLayerName: string;
|
||||
let missingImportLayer: string;
|
||||
let layerNameMismatch: string;
|
||||
}
|
||||
}
|
||||
function create(context: import("@eslint/core").RuleContext<{
|
||||
LangOptions: import("../index.js").CSSLanguageOptions;
|
||||
Code: import("../index.js").CSSSourceCode;
|
||||
RuleOptions: UseLayersOptions;
|
||||
Node: import("@eslint/css-tree").CssNodePlain;
|
||||
MessageIds: UseLayersMessageIds;
|
||||
}>): {
|
||||
"Atrule[name=/^import$/i]"(node: any): void;
|
||||
Layer(node: import("@eslint/css-tree").Layer): void;
|
||||
"Atrule[name=/^layer$/i]"(node: any): void;
|
||||
"Atrule[name=/^layer$/i]:exit"(): void;
|
||||
Rule(node: import("@eslint/css-tree").RulePlain): void;
|
||||
};
|
||||
}
|
||||
export default _default;
|
||||
export type UseLayersMessageIds = "missingLayer" | "missingLayerName" | "missingImportLayer" | "layerNameMismatch";
|
||||
export type UseLayersOptions = [{
|
||||
allowUnnamedLayers?: boolean;
|
||||
requireImportLayers?: boolean;
|
||||
layerNamePattern?: string;
|
||||
}];
|
||||
export type UseLayersRuleDefinition = CSSRuleDefinition<{
|
||||
RuleOptions: UseLayersOptions;
|
||||
MessageIds: UseLayersMessageIds;
|
||||
}>;
|
||||
import type { CSSRuleDefinition } from "../types.js";
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @fileoverview Rule to require layers in CSS.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Type Definitions
|
||||
//-----------------------------------------------------------------------------
|
||||
/**
|
||||
* @import { CSSRuleDefinition } from "../types.js"
|
||||
* @typedef {"missingLayer" | "missingLayerName" | "missingImportLayer" | "layerNameMismatch"} UseLayersMessageIds
|
||||
* @typedef {[{
|
||||
* allowUnnamedLayers?: boolean,
|
||||
* requireImportLayers?: boolean,
|
||||
* layerNamePattern?: string
|
||||
* }]} UseLayersOptions
|
||||
* @typedef {CSSRuleDefinition<{ RuleOptions: UseLayersOptions, MessageIds: UseLayersMessageIds }>} UseLayersRuleDefinition
|
||||
*/
|
||||
//-----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//-----------------------------------------------------------------------------
|
||||
export default /** @satisfies {UseLayersRuleDefinition} */ ({
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Require use of layers",
|
||||
url: "https://github.com/eslint/css/blob/main/docs/rules/use-layers.md",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowUnnamedLayers: {
|
||||
type: "boolean",
|
||||
},
|
||||
requireImportLayers: {
|
||||
type: "boolean",
|
||||
},
|
||||
layerNamePattern: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
allowUnnamedLayers: false,
|
||||
requireImportLayers: true,
|
||||
layerNamePattern: "",
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
missingLayer: "Expected rule to be within a layer.",
|
||||
missingLayerName: "Expected layer to have a name.",
|
||||
missingImportLayer: "Expected import to be within a layer.",
|
||||
layerNameMismatch: "Expected layer name '{{ name }}' to match pattern '{{pattern}}'.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
let layerDepth = 0;
|
||||
const options = context.options[0];
|
||||
const layerNameRegex = options.layerNamePattern
|
||||
? new RegExp(options.layerNamePattern, "u")
|
||||
: null;
|
||||
return {
|
||||
"Atrule[name=/^import$/i]"(node) {
|
||||
// layer, if present, must always be the second child of the prelude
|
||||
const secondChild = node.prelude?.children[1];
|
||||
const layerNode = secondChild?.name === "layer" ? secondChild : null;
|
||||
if (options.requireImportLayers && !layerNode) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "missingImportLayer",
|
||||
});
|
||||
}
|
||||
if (layerNode) {
|
||||
const isLayerFunction = layerNode.type === "Function";
|
||||
if (!options.allowUnnamedLayers && !isLayerFunction) {
|
||||
context.report({
|
||||
loc: layerNode.loc,
|
||||
messageId: "missingLayerName",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Layer(node) {
|
||||
if (!layerNameRegex) {
|
||||
return;
|
||||
}
|
||||
const parts = node.name.split(".");
|
||||
let currentPos = 0;
|
||||
parts.forEach((part, index) => {
|
||||
if (!layerNameRegex.test(part)) {
|
||||
const startColumn = node.loc.start.column + currentPos;
|
||||
const endColumn = startColumn + part.length;
|
||||
context.report({
|
||||
loc: {
|
||||
start: {
|
||||
line: node.loc.start.line,
|
||||
column: startColumn,
|
||||
},
|
||||
end: {
|
||||
line: node.loc.start.line,
|
||||
column: endColumn,
|
||||
},
|
||||
},
|
||||
messageId: "layerNameMismatch",
|
||||
data: {
|
||||
name: part,
|
||||
pattern: options.layerNamePattern,
|
||||
},
|
||||
});
|
||||
}
|
||||
currentPos += part.length;
|
||||
// add 1 to account for the . symbol
|
||||
if (index < parts.length - 1) {
|
||||
currentPos += 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
"Atrule[name=/^layer$/i]"(node) {
|
||||
layerDepth++;
|
||||
if (!options.allowUnnamedLayers && !node.prelude) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "missingLayerName",
|
||||
});
|
||||
}
|
||||
},
|
||||
"Atrule[name=/^layer$/i]:exit"() {
|
||||
layerDepth--;
|
||||
},
|
||||
Rule(node) {
|
||||
if (layerDepth > 0) {
|
||||
return;
|
||||
}
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "missingLayer",
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user