eslint-plugin-css-custom-properties
Version:
lint css custom properties in javascript strings
194 lines (175 loc) • 4.66 kB
JavaScript
/**
* @fileoverview disallow usage of unknown css custom property strings
*/
;
//------------------------------------------------------------------------------
// 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;
}