@ec0lint/plugin-css
Version:
ec0lint plugin that provides rules to verify CSS definition objects
490 lines (489 loc) • 20 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compositingVisitors = exports.defineCSSVisitor = void 0;
const path_1 = __importDefault(require("path"));
const ast_utils_1 = require("./ast-utils");
const regexp_1 = require("./regexp");
const extract_calls_references_1 = require("./extract-calls-references");
const postcss_value_parser_1 = __importDefault(require("postcss-value-parser"));
function normalizeDefineFunctions(settings) {
if (typeof settings !== "object" || settings == null) {
return {};
}
const defines = {};
for (const moduleName of Object.keys(settings)) {
const moduleSettings = settings[moduleName];
if (typeof moduleSettings !== "object") {
continue;
}
const paths = [];
if (Array.isArray(moduleSettings)) {
paths.push(...moduleSettings.map(normalizePaths));
}
defines[moduleName] = paths;
}
return defines;
function normalizePaths(val) {
if (Array.isArray(val)) {
return val.map((s) => String(s));
}
return [String(val)];
}
}
const cssComments = new WeakMap();
function getCSSComments(context) {
let tokens = cssComments.get(context);
if (tokens) {
return tokens;
}
const sourceCode = context.getSourceCode();
tokens = sourceCode
.getAllComments()
.filter((comment) => /@css(?:\b|$)/u.test(comment.value));
cssComments.set(context, tokens);
return tokens;
}
const cssRules = new WeakMap();
function defineCSSVisitor(context, rule) {
const programNode = context.getSourceCode().ast;
let visitor;
let rules = cssRules.get(programNode);
if (!rules) {
rules = [];
cssRules.set(programNode, rules);
visitor = buildCSSVisitor(context, rules, () => {
cssRules.delete(programNode);
});
}
else {
visitor = {};
}
rules.push(rule);
return visitor;
}
exports.defineCSSVisitor = defineCSSVisitor;
function buildCSSVisitor(context, rules, programExit) {
var _a;
const verifiedObjects = [];
const markedObjects = [];
function verifyCSSObject(baseCtx) {
verifiedObjects.push(baseCtx.define);
if (baseCtx.define.type === "ArrayExpression") {
verifiedObjects.push(...baseCtx.define.elements);
}
const ctx = buildCSSObjectContext(baseCtx);
visitCSS(ctx, createVisitorFromRules(rules, ctx));
}
function visitCSS(ctx, visitor) {
var _a, _b;
(_a = visitor.onRoot) === null || _a === void 0 ? void 0 : _a.call(visitor, ctx);
if (ctx.define.type === "ObjectExpression") {
visitObject(ctx.define);
}
else if (ctx.define.type === "ArrayExpression") {
visitArray(ctx.define);
}
(_b = visitor["onRoot:exit"]) === null || _b === void 0 ? void 0 : _b.call(visitor, ctx);
function visitArray(array) {
for (const element of array.elements) {
if (!element) {
continue;
}
if (element.type === "SpreadElement") {
const target = resolveDefineExpression(element.argument, ctx);
if (target.type === "ArrayExpression") {
visitArray(target);
}
continue;
}
const target = resolveDefineExpression(element, ctx);
if (target.type === "ObjectExpression") {
visitObject(target);
}
}
}
function visitObject(object) {
var _a, _b, _c;
if (ctx.on === "jsx-style" || ctx.on === "vue-style") {
if (visitor.onProperty) {
for (const prop of object.properties) {
if (prop.type === "Property") {
visitor.onProperty(buildPropertyContext(ctx, prop));
}
else if (prop.type === "SpreadElement") {
if (prop.argument.type === "Identifier") {
const target = resolveDefineExpression(prop.argument, ctx);
if (target.type === "ObjectExpression") {
visitObject(target);
}
}
}
}
}
}
else if (ctx.on === "define-function" || ctx.on === "mark") {
if (visitor.onProperty ||
visitor.onRule ||
visitor["onRule:exit"]) {
for (const prop of object.properties) {
if (prop.type === "Property") {
const value = resolveDefineExpression(prop.value, ctx);
if (value.type === "ObjectExpression") {
const rule = buildRuleContext(ctx, prop);
(_a = visitor.onRule) === null || _a === void 0 ? void 0 : _a.call(visitor, rule);
visitObject(value);
(_b = visitor["onRule:exit"]) === null || _b === void 0 ? void 0 : _b.call(visitor, rule);
}
else if (value.type === "Literal" ||
value.type === "TemplateLiteral") {
(_c = visitor.onProperty) === null || _c === void 0 ? void 0 : _c.call(visitor, buildPropertyContext(ctx, prop));
}
}
else if (prop.type === "SpreadElement") {
if (prop.argument.type === "Identifier") {
const target = resolveDefineExpression(prop.argument, ctx);
if (target.type === "ObjectExpression") {
visitObject(target);
}
}
}
}
}
}
else {
const neverCtx = ctx;
throw new Error(`Unknown context. ${neverCtx}`);
}
}
}
const settingsTarget = ((_a = context.settings.css) === null || _a === void 0 ? void 0 : _a.target) || {};
const attributes = ["style", ...(settingsTarget.attributes || [])].map(regexp_1.toRegExp);
const defineFunctions = Object.assign(Object.assign({}, normalizeDefineFunctions(settingsTarget.defineFunctions)), { "styled-components": [
["default", "/^\\w+$/u"],
["default", "/^\\w+$/u", "attrs()"],
["default()"],
["default()", "attrs()"],
["default", "/^\\w+$/u", "withConfig()"],
["default", "/^\\w+$/u", "withConfig()", "attrs()"],
["default", "/^\\w+$/u", "attrs()", "withConfig()"],
["default()", "withConfig()"],
["default()", "withConfig()", "attrs()"],
["default()", "attrs()", "withConfig()"],
["css"],
["keyframes"],
["createGlobalStyle"],
] });
let scopeStack = {
upper: null,
node: context.getSourceCode().ast,
};
const defineStyleArgumentFunctions = new Map();
return compositingVisitors({
Program(node) {
for (const body of node.body) {
if (body.type !== "ImportDeclaration") {
continue;
}
const moduleDefineFunctions = defineFunctions[String(body.source.value)];
if (!moduleDefineFunctions) {
continue;
}
for (const callReference of (0, extract_calls_references_1.extractCallReferences)(body, moduleDefineFunctions, context)) {
for (const argument of callReference.node.arguments) {
if (argument.type === "SpreadElement") {
continue;
}
const defineStyleArgument = {
argument,
callReference,
};
const target = resolveDefineExpression(defineStyleArgument.argument, null);
if (target.type === "ObjectExpression") {
verifyCSSObject({
define: target,
scope: defineStyleArgument.argument,
on: "define-function",
defineStyleArgument,
});
}
else if (target.type === "FunctionExpression" ||
target.type === "ArrowFunctionExpression") {
defineStyleArgumentFunctions.set(target, defineStyleArgument);
}
}
}
}
},
":function, PropertyDefinition"(node) {
const defineStyleArgument = defineStyleArgumentFunctions.get(node);
scopeStack = {
upper: scopeStack,
node,
defineStyleArgumentFunction: defineStyleArgument,
};
if (node.type === "ArrowFunctionExpression" &&
node.expression &&
node.body.type !== "BlockStatement" &&
defineStyleArgument) {
const target = resolveDefineExpression(node.body, null);
if (target.type === "ObjectExpression") {
verifyCSSObject({
define: target,
scope: defineStyleArgument.argument,
on: "define-function",
defineStyleArgument,
});
}
}
},
ReturnStatement(node) {
if (scopeStack &&
scopeStack.defineStyleArgumentFunction &&
node.argument) {
const target = resolveDefineExpression(node.argument, null);
if (target.type === "ObjectExpression") {
const defineStyleArgument = scopeStack.defineStyleArgumentFunction;
verifyCSSObject({
define: target,
scope: defineStyleArgument.argument,
on: "define-function",
defineStyleArgument,
});
}
}
},
":function, PropertyDefinition:exit"() {
scopeStack = scopeStack && scopeStack.upper;
},
[`JSXAttribute > JSXExpressionContainer.value > .expression`](node) {
var _a;
const jsxAttr = (0, ast_utils_1.getParent)((0, ast_utils_1.getParent)(node));
const attrName = (_a = jsxAttr === null || jsxAttr === void 0 ? void 0 : jsxAttr.name) === null || _a === void 0 ? void 0 : _a.name;
if (!attrName || !attributes.some((r) => r.test(attrName))) {
return;
}
const target = resolveDefineExpression(node, null);
if (target.type === "ObjectExpression" ||
target.type === "ArrayExpression") {
verifyCSSObject({
define: target,
scope: node,
on: "jsx-style",
});
}
},
ObjectExpression(node) {
if (getCSSComments(context).some((comment) => comment.loc.end.line === node.loc.start.line - 1)) {
markedObjects.push(node);
}
},
"Program:exit"(node) {
const set = new Set(verifiedObjects);
for (const objNode of markedObjects) {
if (set.has(objNode)) {
continue;
}
verifyCSSObject({
define: objNode,
scope: objNode,
on: "mark",
});
}
programExit(node);
},
}, defineTemplateBodyVisitor(context, {
[`VAttribute[directive=true][key.name.name='bind'] > VExpressionContainer.value > :matches(ObjectExpression,ArrayExpression).expression`](node) {
var _a, _b;
const vBindAttr = (0, ast_utils_1.getParent)((0, ast_utils_1.getParent)(node));
const attrName = (_b = (_a = vBindAttr === null || vBindAttr === void 0 ? void 0 : vBindAttr.key) === null || _a === void 0 ? void 0 : _a.argument) === null || _b === void 0 ? void 0 : _b.name;
if (!attrName || !attributes.some((r) => r.test(attrName))) {
return;
}
verifyCSSObject({
define: node,
scope: node,
on: "vue-style",
});
},
}), {});
function buildCSSObjectContext(baseCtx) {
return Object.assign(Object.assign({}, baseCtx), { isFixable(targetNode) {
const scopeRange = baseCtx.scope.range;
if (scopeRange[0] <= baseCtx.define.range[0] &&
baseCtx.define.range[1] <= scopeRange[1]) {
if (!targetNode) {
return true;
}
const targetRange = targetNode.range;
return (scopeRange[0] <= targetRange[0] &&
targetRange[1] <= scopeRange[1]);
}
return false;
} });
}
function resolveDefineExpression(node, ctx) {
if (ctx && ctx.on === "vue-style") {
return node;
}
if (node.type === "Identifier") {
return (0, ast_utils_1.findExpression)(context, node) || node;
}
return node;
}
function buildPropertyContext(ctx, node) {
return {
getName() {
const { key } = node;
if (key.type === "PrivateIdentifier") {
return null;
}
if (!node.computed && key.type !== "Literal") {
if (key.type === "Identifier") {
return {
name: key.name,
expression: key,
directExpression: key,
};
}
return null;
}
const val = resolveExpression(ctx, key);
return (val && {
name: String(val.value),
expression: val.expression,
directExpression: val.directExpression,
});
},
getValue() {
const value = node.value;
const val = resolveExpression(ctx, value);
let parsed;
return (val && {
value: val.value,
expression: val.expression,
directExpression: val.directExpression,
get parsed() {
if (parsed) {
return parsed;
}
return (parsed = (0, postcss_value_parser_1.default)(String(val.value)));
},
});
},
};
}
function buildRuleContext(ctx, node) {
return {
getSelector() {
const { key } = node;
if (key.type === "PrivateIdentifier") {
return null;
}
if (!node.computed && key.type !== "Literal") {
if (key.type === "Identifier") {
return {
selector: key.name,
expression: key,
directExpression: key,
};
}
return null;
}
const val = resolveExpression(ctx, key);
return (val && {
selector: String(val.value),
expression: val.expression,
directExpression: val.directExpression,
});
},
};
}
function resolveExpression(ctx, node) {
if (node.type === "Literal") {
if (typeof node.value === "string" ||
typeof node.value === "number") {
return {
value: node.value,
expression: node,
directExpression: node,
};
}
return null;
}
if ((0, ast_utils_1.isStaticTemplateLiteral)(node)) {
return {
value: node.quasis[0].value.cooked,
expression: node,
directExpression: node,
};
}
if (ctx.on === "vue-style") {
return null;
}
const name = (0, ast_utils_1.getStaticValue)(context, node);
if (name != null &&
(typeof name.value === "string" || typeof name.value === "number")) {
return {
value: name.value,
expression: node,
directExpression: null,
};
}
return null;
}
}
function createVisitorFromRules(rules, context) {
const handlers = {};
for (const rule of rules) {
if (rule.createVisitor) {
const visitor = rule.createVisitor(context);
for (const key of Object.keys(visitor)) {
const orig = handlers[key];
if (orig) {
handlers[key] = (...args) => {
orig(...args);
visitor[key](...args);
};
}
else {
handlers[key] = visitor[key];
}
}
}
}
return handlers;
}
function defineTemplateBodyVisitor(context, templateBodyVisitor) {
if (context.parserServices.defineTemplateBodyVisitor == null) {
const filename = context.getFilename();
if (path_1.default.extname(filename) === ".vue") {
context.report({
loc: { line: 1, column: 0 },
message: "Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.",
});
}
return {};
}
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, {}, {});
}
function compositingVisitors(visitor, ...visitors) {
for (const v of visitors) {
for (const key in v) {
const orig = visitor[key];
if (orig) {
visitor[key] = (...args) => {
orig(...args);
v[key](...args);
};
}
else {
visitor[key] = v[key];
}
}
}
return visitor;
}
exports.compositingVisitors = compositingVisitors;