@eslint/css
Version:
CSS linting plugin for ESLint
1,981 lines (1,745 loc) • 131 kB
JavaScript
/**
* @import { CssNode, CssNodePlain, Comment, Lexer, StyleSheetPlain, SyntaxConfig, SyntaxMatchError, ValuePlain, FunctionNodePlain, CssLocationRange, AtrulePlain, Identifier } from "@eslint/css-tree"
* @import { SourceRange, SourceLocation, FileProblem, DirectiveType, RulesConfig, Language, OkParseResult, ParseResult, File, FileError } from "@eslint/core"
* @import { CSSSyntaxElement, CSSRuleDefinition } from "./types.cjs"
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var cssTree = require('@eslint/css-tree');
var pluginKit = require('@eslint/plugin-kit');
/**
* @fileoverview Visitor keys for the CSS Tree AST.
* @author Nicholas C. Zakas
*/
const visitorKeys = {
AnPlusB: [],
Atrule: ["prelude", "block"],
AtrulePrelude: ["children"],
AttributeSelector: ["name", "value"],
Block: ["children"],
Brackets: ["children"],
CDC: [],
CDO: [],
ClassSelector: [],
Combinator: [],
Comment: [],
Condition: ["children"],
Declaration: ["value"],
DeclarationList: ["children"],
Dimension: [],
Feature: ["value"],
FeatureFunction: ["value"],
FeatureRange: ["left", "middle", "right"],
Function: ["children"],
GeneralEnclosed: ["children"],
Hash: [],
IdSelector: [],
Identifier: [],
Layer: [],
LayerList: ["children"],
MediaQuery: ["condition"],
MediaQueryList: ["children"],
NestingSelector: [],
Nth: ["nth", "selector"],
Number: [],
Operator: [],
Parentheses: ["children"],
Percentage: [],
PseudoClassSelector: ["children"],
PseudoElementSelector: ["children"],
Ratio: ["left", "right"],
Raw: [],
Rule: ["prelude", "block"],
Scope: ["root", "limit"],
Selector: ["children"],
SelectorList: ["children"],
String: [],
StyleSheet: ["children"],
SupportsDeclaration: ["declaration"],
TypeSelector: [],
UnicodeRange: [],
Url: [],
Value: ["children"],
WhiteSpace: [],
};
/**
* @fileoverview The CSSSourceCode class.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
/**
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const commentParser = new pluginKit.ConfigCommentParser();
const INLINE_CONFIG =
/^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u;
/**
* A class to represent a step in the traversal process.
*/
class CSSTraversalStep extends pluginKit.VisitNodeStep {
/**
* The target of the step.
* @type {CssNode}
*/
target = undefined;
/**
* Creates a new instance.
* @param {Object} options The options for the step.
* @param {CssNode} options.target The target of the step.
* @param {1|2} options.phase The phase of the step.
* @param {Array<any>} options.args The arguments of the step.
*/
constructor({ target, phase, args }) {
super({ target, phase, args });
this.target = target;
}
}
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* CSS Source Code Object.
* @extends {TextSourceCodeBase<{LangOptions: CSSLanguageOptions, RootNode: StyleSheetPlain, SyntaxElementWithLoc: CSSSyntaxElement, ConfigNode: Comment}>}
*/
class CSSSourceCode extends pluginKit.TextSourceCodeBase {
/**
* Cached traversal steps.
* @type {Array<CSSTraversalStep>|undefined}
*/
#steps;
/**
* Cache of parent nodes.
* @type {WeakMap<CssNodePlain, CssNodePlain>}
*/
#parents = new WeakMap();
/**
* Collection of inline configuration comments.
* @type {Array<Comment>}
*/
#inlineConfigComments;
/**
* The AST of the source code.
* @type {StyleSheetPlain}
*/
ast = undefined;
/**
* The comment node in the source code.
* @type {Array<Comment>|undefined}
*/
comments;
/**
* The lexer for this instance.
* @type {Lexer}
*/
lexer;
/**
* Creates a new instance.
* @param {Object} options The options for the instance.
* @param {string} options.text The source code text.
* @param {StyleSheetPlain} options.ast The root AST node.
* @param {Array<Comment>} options.comments The comment nodes in the source code.
* @param {Lexer} options.lexer The lexer used to parse the source code.
*/
constructor({ text, ast, comments, lexer }) {
super({ text, ast });
this.ast = ast;
this.comments = comments;
this.lexer = lexer;
}
/**
* Returns the range of the given node.
* @param {CssNodePlain} node The node to get the range of.
* @returns {SourceRange} The range of the node.
* @override
*/
getRange(node) {
return [node.loc.start.offset, node.loc.end.offset];
}
/**
* Returns an array of all inline configuration nodes found in the
* source code.
* @returns {Array<Comment>} An array of all inline configuration nodes.
*/
getInlineConfigNodes() {
if (!this.#inlineConfigComments) {
this.#inlineConfigComments = this.comments.filter(comment =>
INLINE_CONFIG.test(comment.value),
);
}
return this.#inlineConfigComments;
}
/**
* Returns directives that enable or disable rules along with any problems
* encountered while parsing the directives.
* @returns {{problems:Array<FileProblem>,directives:Array<Directive>}} Information
* that ESLint needs to further process the directives.
*/
getDisableDirectives() {
const problems = [];
const directives = [];
this.getInlineConfigNodes().forEach(comment => {
const { label, value, justification } =
commentParser.parseDirective(comment.value);
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
if (
label === "eslint-disable-line" &&
comment.loc.start.line !== comment.loc.end.line
) {
const message = `${label} comment should not span multiple lines.`;
problems.push({
ruleId: null,
message,
loc: comment.loc,
});
return;
}
switch (label) {
case "eslint-disable":
case "eslint-enable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = label.slice("eslint-".length);
directives.push(
new pluginKit.Directive({
type: /** @type {DirectiveType} */ (directiveType),
node: comment,
value,
justification,
}),
);
}
// no default
}
});
return { problems, directives };
}
/**
* Returns inline rule configurations along with any problems
* encountered while parsing the configurations.
* @returns {{problems:Array<FileProblem>,configs:Array<{config:{rules:RulesConfig},loc:SourceLocation}>}} Information
* that ESLint needs to further process the rule configurations.
*/
applyInlineConfig() {
const problems = [];
const configs = [];
this.getInlineConfigNodes().forEach(comment => {
const { label, value } = commentParser.parseDirective(
comment.value,
);
if (label === "eslint") {
const parseResult = commentParser.parseJSONLikeConfig(value);
if (parseResult.ok) {
configs.push({
config: {
rules: parseResult.config,
},
loc: comment.loc,
});
} else {
problems.push({
ruleId: null,
message:
/** @type {{ok: false, error: { message: string }}} */ (
parseResult
).error.message,
loc: comment.loc,
});
}
}
});
return {
configs,
problems,
};
}
/**
* Returns the parent of the given node.
* @param {CssNodePlain} node The node to get the parent of.
* @returns {CssNodePlain|undefined} The parent of the node.
*/
getParent(node) {
return this.#parents.get(node);
}
/**
* Traverse the source code and return the steps that were taken.
* @returns {Iterable<CSSTraversalStep>} The steps that were taken while traversing the source code.
*/
traverse() {
// Because the AST doesn't mutate, we can cache the steps
if (this.#steps) {
return this.#steps.values();
}
/** @type {Array<CSSTraversalStep>} */
const steps = (this.#steps = []);
// Note: We can't use `walk` from `css-tree` because it uses `CssNode` instead of `CssNodePlain`
const visit = (node, parent) => {
// first set the parent
this.#parents.set(node, parent);
// then add the step
steps.push(
new CSSTraversalStep({
target: node,
phase: 1,
args: [node, parent],
}),
);
// then visit the children
for (const key of visitorKeys[node.type] || []) {
const child = node[key];
if (child) {
if (Array.isArray(child)) {
child.forEach(grandchild => {
visit(grandchild, node);
});
} else {
visit(child, node);
}
}
}
// then add the exit step
steps.push(
new CSSTraversalStep({
target: node,
phase: 2,
args: [node, parent],
}),
);
};
visit(this.ast);
return steps;
}
}
/**
* @filedescription The CSSLanguage class.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
/**
*/
/** @typedef {OkParseResult<StyleSheetPlain> & { comments: Comment[], lexer: Lexer }} CSSOkParseResult */
/** @typedef {ParseResult<StyleSheetPlain>} CSSParseResult */
/**
* @typedef {Object} CSSLanguageOptions
* @property {boolean} [tolerant] Whether to be tolerant of recoverable parsing errors.
* @property {SyntaxConfig} [customSyntax] Custom syntax to use for parsing.
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const blockOpenerTokenTypes = new Map([
[cssTree.tokenTypes.Function, ")"],
[cssTree.tokenTypes.LeftCurlyBracket, "}"],
[cssTree.tokenTypes.LeftParenthesis, ")"],
[cssTree.tokenTypes.LeftSquareBracket, "]"],
]);
const blockCloserTokenTypes = new Map([
[cssTree.tokenTypes.RightCurlyBracket, "{"],
[cssTree.tokenTypes.RightParenthesis, "("],
[cssTree.tokenTypes.RightSquareBracket, "["],
]);
//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------
/**
* CSS Language Object
* @implements {Language<{ LangOptions: CSSLanguageOptions; Code: CSSSourceCode; RootNode: StyleSheetPlain; Node: CssNodePlain}>}
*/
class CSSLanguage {
/**
* The type of file to read.
* @type {"text"}
*/
fileType = "text";
/**
* The line number at which the parser starts counting.
* @type {0|1}
*/
lineStart = 1;
/**
* The column number at which the parser starts counting.
* @type {0|1}
*/
columnStart = 1;
/**
* The name of the key that holds the type of the node.
* @type {string}
*/
nodeTypeKey = "type";
/**
* The visitor keys for the CSSTree AST.
* @type {Record<string, string[]>}
*/
visitorKeys = visitorKeys;
/**
* The default language options.
* @type {CSSLanguageOptions}
*/
defaultLanguageOptions = {
tolerant: false,
};
/**
* Validates the language options.
* @param {CSSLanguageOptions} languageOptions The language options to validate.
* @throws {Error} When the language options are invalid.
*/
validateLanguageOptions(languageOptions) {
if (
"tolerant" in languageOptions &&
typeof languageOptions.tolerant !== "boolean"
) {
throw new TypeError(
"Expected a boolean value for 'tolerant' option.",
);
}
if ("customSyntax" in languageOptions) {
if (
typeof languageOptions.customSyntax !== "object" ||
languageOptions.customSyntax === null
) {
throw new TypeError(
"Expected an object value for 'customSyntax' option.",
);
}
}
}
/**
* Parses the given file into an AST.
* @param {File} file The virtual file to parse.
* @param {Object} [context] The parsing context.
* @param {CSSLanguageOptions} [context.languageOptions] The language options to use for parsing.
* @returns {CSSParseResult} The result of parsing.
*/
parse(file, { languageOptions = {} } = {}) {
// Note: BOM already removed
const text = /** @type {string} */ (file.body);
/** @type {Comment[]} */
const comments = [];
/** @type {FileError[]} */
const errors = [];
const { tolerant } = languageOptions;
const { parse, lexer } = languageOptions.customSyntax
? cssTree.fork(languageOptions.customSyntax)
: { parse: cssTree.parse, lexer: cssTree.lexer };
/*
* Check for parsing errors first. If there's a parsing error, nothing
* else can happen. However, a parsing error does not throw an error
* from this method - it's just considered a fatal error message, a
* problem that ESLint identified just like any other.
*/
try {
const root = cssTree.toPlainObject(
parse(text, {
filename: file.path,
positions: true,
onComment(value, loc) {
comments.push({
type: "Comment",
value,
loc,
});
},
onParseError(error) {
if (!tolerant) {
errors.push(error);
}
},
onToken(type, start, end, index) {
if (tolerant) {
return;
}
switch (type) {
// these already generate errors
case cssTree.tokenTypes.BadString:
case cssTree.tokenTypes.BadUrl:
break;
default:
/* eslint-disable new-cap -- This is a valid call */
if (this.isBlockOpenerTokenType(type)) {
if (
this.getBlockTokenPairIndex(index) ===
-1
) {
const loc = this.getRangeLocation(
start,
end,
);
errors.push(
parse.SyntaxError(
`Missing closing ${blockOpenerTokenTypes.get(type)}`,
text,
start,
loc.start.line,
loc.start.column,
),
);
}
} else if (this.isBlockCloserTokenType(type)) {
if (
this.getBlockTokenPairIndex(index) ===
-1
) {
const loc = this.getRangeLocation(
start,
end,
);
errors.push(
parse.SyntaxError(
`Missing opening ${blockCloserTokenTypes.get(type)}`,
text,
start,
loc.start.line,
loc.start.column,
),
);
}
}
/* eslint-enable new-cap -- This is a valid call */
}
},
}),
);
if (errors.length) {
return {
ok: false,
errors,
};
}
return {
ok: true,
ast: /** @type {StyleSheetPlain} */ (root),
comments,
lexer,
};
} catch (ex) {
return {
ok: false,
errors: [ex],
};
}
}
/**
* Creates a new `CSSSourceCode` object from the given information.
* @param {File} file The virtual file to create a `CSSSourceCode` object from.
* @param {CSSOkParseResult} parseResult The result returned from `parse()`.
* @returns {CSSSourceCode} The new `CSSSourceCode` object.
*/
createSourceCode(file, parseResult) {
return new CSSSourceCode({
text: /** @type {string} */ (file.body),
ast: parseResult.ast,
comments: parseResult.comments,
lexer: parseResult.lexer,
});
}
}
/**
* @fileoverview Rule to prevent empty blocks in CSS.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"emptyBlock"} NoEmptyBlocksMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoEmptyBlocksMessageIds }>} NoEmptyBlocksRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoEmptyBlocksRuleDefinition} */
var noEmptyBlocks = {
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",
});
}
},
};
},
};
/**
* @fileoverview Rule to prevent duplicate imports in CSS.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"duplicateImport"} NoDuplicateKeysMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoDuplicateKeysMessageIds }>} NoDuplicateImportsRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule
//-----------------------------------------------------------------------------
/**
* @type {NoDuplicateImportsRuleDefinition}
*/
var noDuplicateImports = {
meta: {
type: "problem",
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}}.",
},
},
create(context) {
const imports = new Set();
return {
"Atrule[name=import]"(node) {
const url = node.prelude.children[0].value;
if (imports.has(url)) {
context.report({
loc: node.loc,
messageId: "duplicateImport",
data: { url },
});
} else {
imports.add(url);
}
},
};
},
};
/**
* @fileoverview Utility functions for ESLint CSS plugin.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Determines if an error is a syntax match error.
* @param {Object} error The error object to check.
* @returns {error is SyntaxMatchError} True if the error is a syntax match error, false if not.
*/
function isSyntaxMatchError(error) {
return typeof error.syntax === "string";
}
/**
* Finds the line and column offsets for a given offset in a string.
* @param {string} text The text to search.
* @param {number} offset The offset to find.
* @returns {{lineOffset:number,columnOffset:number}} The location of the offset.
*/
function findOffsets(text, offset) {
let lineOffset = 0;
let columnOffset = 0;
for (let i = 0; i < offset; i++) {
if (text[i] === "\n") {
lineOffset++;
columnOffset = 0;
} else {
columnOffset++;
}
}
return {
lineOffset,
columnOffset,
};
}
/**
* @fileoverview Rule to disallow `!important` flags.
* @author thecalamiity
* @author Yann Bertrand
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"unexpectedImportant"} NoImportantMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoImportantMessageIds }>} NoImportantRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoImportantRuleDefinition} */
var noImportant = {
meta: {
type: "problem",
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.",
},
},
create(context) {
const importantPattern = /!(\s|\/\*.*?\*\/)*important/iu;
return {
Declaration(node) {
if (node.important) {
const declarationText = context.sourceCode.getText(node);
const importantMatch =
importantPattern.exec(declarationText);
const {
lineOffset: startLineOffset,
columnOffset: startColumnOffset,
} = findOffsets(declarationText, importantMatch.index);
const {
lineOffset: endLineOffset,
columnOffset: endColumnOffset,
} = findOffsets(
declarationText,
importantMatch.index + importantMatch[0].length,
);
const nodeStartLine = node.loc.start.line;
const nodeStartColumn = node.loc.start.column;
const startLine = nodeStartLine + startLineOffset;
const endLine = nodeStartLine + endLineOffset;
const startColumn =
(startLine === nodeStartLine ? nodeStartColumn : 1) +
startColumnOffset;
const endColumn =
(endLine === nodeStartLine ? nodeStartColumn : 1) +
endColumnOffset;
context.report({
loc: {
start: {
line: startLine,
column: startColumn,
},
end: {
line: endLine,
column: endColumn,
},
},
messageId: "unexpectedImportant",
});
}
},
};
},
};
/**
* @fileoverview Rule to prevent invalid properties in CSS.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"invalidPropertyValue" | "unknownProperty" | "unknownVar"} NoInvalidPropertiesMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidPropertiesMessageIds }>} NoInvalidPropertiesRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* Replaces all instances of a regex pattern with a replacement and tracks the offsets
* @param {string} text The text to perform replacements on
* @param {string} varName The regex pattern string to search for
* @param {string} replaceValue The string to replace with
* @returns {{text: string, offsets: Array<number>}} The updated text and array of offsets
* where replacements occurred
*/
function replaceWithOffsets(text, varName, replaceValue) {
const offsets = [];
let result = "";
let lastIndex = 0;
const regex = new RegExp(`var\\(\\s*${varName}\\s*\\)`, "gu");
let match;
while ((match = regex.exec(text)) !== null) {
result += text.slice(lastIndex, match.index);
/*
* We need the offset of the replacement after other replacements have
* been made, so we push the current length of the result before appending
* the replacement value.
*/
offsets.push(result.length);
result += replaceValue;
lastIndex = match.index + match[0].length;
}
result += text.slice(lastIndex);
return { text: result, offsets };
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {NoInvalidPropertiesRuleDefinition} */
var noInvalidProperties = {
meta: {
type: "problem",
docs: {
description: "Disallow invalid properties",
recommended: true,
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-properties.md",
},
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();
/**
* We need to track this as a stack because we can have nested
* rules that use the `var()` function, and we need to
* ensure that we validate the innermost rule first.
* @type {Array<Map<string,FunctionNodePlain>>}
*/
const replacements = [];
return {
"Rule > Block > Declaration"() {
replacements.push(new Map());
},
"Function[name=var]"(node) {
const map = replacements.at(-1);
if (!map) {
return;
}
/*
* Store the custom property name and the function node
* so can use these to validate the value later.
*/
const name = node.children[0].name;
map.set(name, node);
},
"Rule > Block > Declaration:exit"(node) {
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;
}
const varsFound = replacements.pop();
/** @type {Map<number,CssLocationRange>} */
const varsFoundLocs = new Map();
const usingVars = varsFound?.size > 0;
let value = node.value;
if (usingVars) {
// need to use a text version of the value here
value = sourceCode.getText(node.value);
let offsets;
// replace any custom properties with their values
for (const [name, func] of varsFound) {
const varValue = vars.get(name);
if (varValue) {
({ text: value, offsets } = replaceWithOffsets(
value,
name,
sourceCode.getText(varValue).trim(),
));
/*
* Store the offsets of the replacements so we can
* report the correct location of any validation error.
*/
offsets.forEach(offset => {
varsFoundLocs.set(offset, func.loc);
});
} else {
context.report({
loc: func.children[0].loc,
messageId: "unknownVar",
data: {
var: name,
},
});
return;
}
}
}
const { error } = lexer.matchProperty(node.property, value);
if (error) {
// validation failure
if (isSyntaxMatchError(error)) {
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: usingVars
? (varsFoundLocs.get(error.mismatchOffset) ??
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: usingVars
? value.slice(
error.mismatchOffset,
error.mismatchOffset +
error.mismatchLength,
)
: error.css,
expected: error.syntax,
},
});
return;
}
// 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,
},
});
}
},
};
},
};
/**
* @fileoverview Rule to prevent the use of unknown at-rules in CSS.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"unknownAtRule" | "invalidPrelude" | "unknownDescriptor" | "invalidDescriptor" | "invalidExtraPrelude" | "missingPrelude"} NoInvalidAtRulesMessageIds
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidAtRulesMessageIds }>} NoInvalidAtRulesRuleDefinition
*/
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
* 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
//-----------------------------------------------------------------------------
/** @type {NoInvalidAtRulesRuleDefinition} */
var noInvalidAtRules = {
meta: {
type: "problem",
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.",
},
},
create(context) {
const { sourceCode } = context;
const lexer = sourceCode.lexer;
return {
Atrule(node) {
// 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))
);
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,
},
});
}
},
};
},
};
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"notLogicalProperty" | "notLogicalValue" | "notLogicalUnit"} 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
//-----------------------------------------------------------------------------
/** @type {PreferLogicalPropertiesRuleDefinition} */
var preferLogicalProperties = {
meta: {
type: "problem",
fixable: "code",
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: {
type: "string",
},
},
allowUnits: {
type: "array",
items: {
type: "string",
},
},
},
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}}'.",
},
},
create(context) {
return {
Declaration(node) {
const parent = context.sourceCode.getParent(node);
if (parent.type === "SupportsDeclaration") {
return;
}
const allowProperties = context.options[0].allowProperties;
if (
propertiesReplacements.get(node.property) &&
!allowProperties.includes(node.property)
) {
context.report({
loc: node.loc,
messageId: "notLogicalProperty",
data: {
property: node.property,
replacement: propertiesReplacements.get(
node.property,
),
},
});
}
if (
propertyValuesReplacements.get(node.property) &&
node.value.type === "Value" &&
node.value.children[0].type === "Identifier"
) {
const nodeValue = node.value.children[0].name;
if (
propertyValuesReplacements.get(node.property)[nodeValue]
) {
const replacement = propertyValuesReplacements.get(
node.property,
)[nodeValue];
if (replacement) {
context.report({
loc: node.value.children[0].loc,
messageId: "notLogicalValue",
data: {
value: nodeValue,
replacement,
},
});
}
}
}
},
Dimension(node) {
const allowUnits = context.options[0].allowUnits;
if (
unitReplacements.get(node.unit) &&
!allowUnits.includes(node.unit)
) {
context.report({
loc: node.loc,
messageId: "notLogicalUnit",
data: {
unit: node.unit,
replacement: unitReplacements.get(node.unit),
},
});
}
},
};
},
};
/**
* @fileoverview Enforce the use of relative units for font size.
* @author Tanuj Kanti
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @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 fontSizeIdentifiers = new Set([
"xx-small",
"x-small",
"small",
"medium",
"large",
"x-large",
"xx-large",
"xxx-large",
"smaller",
"larger",
"math",
"inherit",
"initial",
"revert",
"revert-layer",
"unset",
]);
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {RelativeFontUnitsRuleDefinition} */
var relativeFontUnits$1 = {
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,
},
},
},
},
],
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)) ||
value.type === "Identifier" ||
(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" &&
fontSizeIdentifiers.has(child.name),
);
const percentageNode = value.children.find(
child => child.type === "Percentage",
);
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,
),
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(", "),
},
});
}
}
}
},
};
},
};
/**
* @fileoverview Rule to require layers in CSS.
* @author Nicholas C. Zakas
*/
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
* @typedef {"missingLayer" | "missingLayerName" | "missingImportLayer" | "layerNameMismatch"} UseLayersMessageIds
* @typedef {[{
* allowUnnamedLayers?: boolean,
* requireImportLayers?: boolean,
* layerNamePattern?: string
* }]} UseLayersOptions
* @typedef {CSSRuleDefinition<{ RuleOptions: UseLayersOptions, MessageIds: UseLayersMessageIds }>} UseLayersRuleDefinition
*/
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
/** @type {UseLayersRuleDefinition} */
var useLayers = {
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]"(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]"(node) {
layerDepth++;
if (!options.allowUnnamedLayers && !node.prelude) {
context.report({
loc: node.loc,
messageId: "missingLayerName",
});
}
},
"Atrule[name=layer]:exit"() {
layerDepth--;
},
Rule(node) {
if (layerDepth > 0) {
return;
}
context.report({
loc: node.loc,
messageId: "missingLayer",
});
},
};
},
};
/**
* @fileoverview CSS features extracted from the web-features package.
* @author tools/generate-baseline.js
*
* THIS FILE IS AUTOGENERATED. DO NOT MODIFY DIRECTLY.
*/
const BASELINE_HIGH = 10;
const BASELINE_LOW = 5;
const properties = new Map([
["accent-color", "0:"],
["alignment-baseline", "0:"],
["all", "10:2020"],
["anchor-name", "0:"],
["anchor-scope", "0:"],
["position-anchor", "0:"],
["position-area", "0:"],
["position-try", "0:"],
["position-try-fallbacks", "0:"],
["position-try-order", "0:"],
["position-visibility", "0:"],
["animation-composition", "5:2023"],
["animation", "10:2015"],
["animation-delay", "10:2015"],
["animation-direction", "10:2015"],
["animation-duration", "10:2015"],
["animation-fill-mode", "10:2015"],
["animation-iteration-count", "10:2015"],
["animation-name", "10:2015"],
["animation-play-state", "10:2015"],
["animation-timing-function", "10:2015"],
["appearance", "10:2022"],
["aspect-ratio", "10:2021"],
["backdrop-filter", "5:2024"],
["background", "10:2015"],
["background-attachment", "10:2015"],
["background-blend-mode", "10:2020"],
["background-clip", "10:2015"],
["background-color", "10:2015"],
["background-image", "10:2015"],
["background-origin", "10:2015"],
["background-position", "10:2015"],
["background-position-x", "10:2016"],
["background-position-y", "10:2016"],
["background-repeat", "10:2015"],
["background-size", "10:2015"],
["baseline-shift", "0:"],
["baseline-source", "0:"],
["border-image", "10:2015"],
["border-image-outset", "10:2015"],
["border-image-repeat", "10:2016"],
["border-image-slice", "10:2015"],
["border-image-source", "10:2015"],
["border-image-width", "10:2015"],
["border-bottom-left-radius", "10:2015"],
["border-bottom-right-radius", "10:2015"],
["border-radius", "10:2015"],
["border-top-left-radius", "10:2015"],
["border-top-right-radius", "10:2015"],
["border", "10:2015"],
["border-bottom", "10:2015"],
["border-bottom-color", "10:2015"],
["border-bottom-style", "10:2015"],
["border-bottom-width", "10:2015"],
["border-color", "10:2015"],
["border-left", "10:2015"],
["border-left-color", "10:2015"],
["border-left-style", "10:2015"],
["border-left-width", "10:2015"],
["border-right", "10:2015"],
["border-right-color", "10:2015"],
["border-right-style", "10:2015"],
["border-right-width", "10:2015"],
["border-style", "10:2015"],
["border-top", "10:2015"],
["border-top-color", "10:2015"],
["border-top-style", "10:2015"],
["border-top-width", "10:2015"],
["border-width", "10:2015"],
["box-decoration-break", "0:"],
["box-shadow", "10:2015"],
["box-sizing", "10:2015"],
["caret-color", "10:2020"],
["clip", "0:"],
["clip-path", "10:2020"],
["color", "10:2015"],
["color-adjust", "0:"],
["color-scheme", "10:2022"],
["column-fill", "10:2017"],
["column-span", "10:2020"],
["contain", "10:2022"],
["contain-intrinsic-block-size", "5:2023"],
["contain-intrinsic-height", "5:2023"],
["contain-intrinsic-inline-size", "5:2023"],
["contain-intrinsic-size", "5:2023"],
["contain-intrinsic-width", "5:2023"],
["container", "5:2023"],
["container-name", "5:2023"],
["container-type", "5:2023"],
["content", "10:2015"],
["content-visibility", "5:2024"],
["counter-set", "5:2023"],
["counter-increment", "10:2015"],
["counter-reset", "10:2015"],
["custom-property", "10:2017"],
["display", "10:2015"],
["dominant-baseline", "10:2020"],
["field-sizing", "0:"],
["filter", "10:2016"],
["align-content