/** * @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", }); }, }; }, });