UNPKG

eslint-plugin-css-custom-properties

Version:

lint css custom properties in javascript strings

194 lines (175 loc) 4.66 kB
/** * @fileoverview disallow usage of unknown css custom property strings */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ const DEFAULT_OPTIONS = { allow: [], allowUnlessPrefixed: [], allowUnknownWithPrefix: [], allowUnknownWithFallback: true, }; /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "problem", docs: { description: "disallow usage of unknown css custom property strings", recommended: false, url: null, // URL to the documentation page for this rule }, fixable: null, // Or `code` or `whitespace` schema: [ { type: "object", properties: { allowUnknownWithFallback: { type: "boolean" }, allowUnknownWithPrefix: { type: "array", items: { type: "string", }, }, allowUnlessPrefixed: { type: "array", items: { type: "string", }, }, allow: { type: "array", items: { type: "string", }, }, }, additionalProperties: false, }, ], messages: [], }, create({ report, options: [userOptions] = [] }) { /** * @type {{ allow: string[]; allowUnlessPrefixed: string[]; allowUnknownWithPrefix: string[]; allowUnknownWithFallback: boolean }} */ const options = { ...DEFAULT_OPTIONS, ...userOptions, }; const allow = options.allow.map((v) => v.replace(/^--/, "")); const { allowUnknownWithFallback, allowUnlessPrefixed, allowUnknownWithPrefix, } = options; /** * * @param {string} stringLiteral * @param {import('eslint').AST.SourceLocation} loc */ function reportUnknown(stringLiteral, loc) { for (const { name, index, fallback } of findCustomPropertyDeclarations( stringLiteral )) { if (fallback && allowUnknownWithFallback) { continue; } if (!allowUnlessPrefixed.every((prefix) => name.startsWith(prefix))) { continue; } if (allowUnknownWithPrefix.some((prefix) => name.startsWith(prefix))) { continue; } if (!allow.includes(name)) { const start = addIndex(loc.start, index, stringLiteral); report({ loc: { start, end: { ...start, column: start.column + 2 + name.length, }, }, message: `Unknown custom property name '--${name}'`, }); } } } return { TemplateElement(node) { const { value, loc: originLoc } = node; if (!originLoc) { /* ¯\_(ツ)_/¯ */ return; } let loc = { ...originLoc }; for (const line of value.raw.split("\n")) { reportUnknown(line.replace(/\\n/g, "__"), loc); loc = { start: { column: -1, line: loc.start.line + 1 }, end: loc.end, }; } }, Literal(node) { const { value, loc } = node; if (typeof value !== "string" || !loc) { /* ¯\_(ツ)_/¯ */ return; } reportUnknown(value, loc); }, }; }, }; /** * * @param {import('eslint').AST.SourceLocation['end']} pos * @param {number} index * @param {string} source * * @return {import('eslint').AST.SourceLocation['end']} */ function addIndex(pos, index, source) { const newPos = { ...pos }; for (let i = 0; i < index; i++) { if (source[i] === "\r") { newPos.line += 1; newPos.column = 0; } else { newPos.column += 1; } } return newPos; } /** * @param {string} value * @param {number} indexOffset */ function findCustomPropertyDeclarations(value, indexOffset = 0) { /** * @type {{ name: string, fallback: boolean, index: number }[]} */ const properties = []; const matches = value .replace(/\n/g, "__") .matchAll(/var\(--([^)|,]+)(, ?.*)?\)/gm); for (const match of matches) { properties.push({ name: match[1], index: indexOffset + (match.index || 0) + 5, fallback: Boolean(match[2]), }); if (match[2]) { properties.push( ...findCustomPropertyDeclarations( match[2], indexOffset + (match.index || 0) + 6 + match[1].length ) ); } } return properties; }