UNPKG

@ec0lint/plugin-css

Version:

ec0lint plugin that provides rules to verify CSS definition objects

490 lines (489 loc) 20 kB
"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;