UNPKG

eslint-plugin-tailwindcss

Version:
387 lines (361 loc) 11.7 kB
/** * @fileoverview Utility functions for AST */ 'use strict'; const { separatorRegEx } = require('./regex'); // context.parserPath // /.../eslint-plugin-tailwindcss/node_modules/espree/espree.js // /.../eslint-plugin-tailwindcss/node_modules/@angular-eslint/template-parser/dist/index.js const removeDuplicatesFromArray = require('./removeDuplicatesFromArray'); function calleeToString(calleeNode) { if (calleeNode.type === 'Identifier') { return calleeNode.name; } if (calleeNode.type === 'MemberExpression') { return `${calleeNode.object.name}.${calleeNode.property.name}`; } } /** * Find out if node is `class` or `className` * * @param {ASTNode} node The AST node being checked * @param {String} classRegex Regex to test the attribute that is being checked against * @returns {Boolean} */ function isClassAttribute(node, classRegex) { if (!node.name) { return false; } let name = ''; switch (node.type) { case 'TextAttribute': name = node.name; break; case 'JSXAttribute': if (node.name.type === 'JSXNamespacedName') { const ns = node.name.namespace.name || ''; name = (ns.length ? ns + ':' : '') + node.name.name.name; } else { name = node.name.name; } break; default: name = node.name.name; } return new RegExp(classRegex).test(name); } /** * Find out if node is `class` * * @param {ASTNode} node The AST node being checked * @param {String} classRegex Regex to test the attribute that is being checked against * @returns {Boolean} */ function isVueClassAttribute(node, classRegex) { const re = new RegExp(classRegex); let name = ''; switch (true) { case node.key && node.key.name && re.test(node.key.name): // class="vue-classes-as-litteral" return true; case node.key && node.key.name && node.key.name.name && node.key.argument && node.key.argument.name && /^bind$/.test(node.key.name.name) && re.test(node.key.argument.name): // v-bind:class="vue-classes-as-bind" // :class="vue-classes-as-bind" return true; default: return false; } } /** * Find out if node's value attribute is just simple text * * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ function isVLiteralValue(node) { return node.value && node.value.type === 'VLiteral'; } /** * Find out if node's value attribute is an ArrayExpression * * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ function isArrayExpression(node) { return node.value && node.value.type === 'VExpressionContainer' && node.value.expression && node.value.expression.type === 'ArrayExpression'; } /** * Find out if node's value attribute is an ObjectExpression * * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ function isObjectExpression(node) { return node.value && node.value.type === 'VExpressionContainer' && node.value.expression && node.value.expression.type === 'ObjectExpression'; } /** * Find out if node's value attribute is just simple text * * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ function isVueValidAttributeValue(node) { switch (true) { case isVLiteralValue(node): // Simple string case isArrayExpression(node): // ['tw-unknown-class'] case isObjectExpression(node): // {'tw-unknown-class': true} return true; default: return false; } } /** * Find out if node's value attribute is just simple text * * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ function isLiteralAttributeValue(node) { if (node.type === 'TextAttribute' && node.name === 'class' && typeof node.value === 'string') { return true; } if (node.value) { switch (node.value.type) { case 'Literal': // No support for dynamic or conditional... return !/\{|\?|\}/.test(node.value.value); case 'JSXExpressionContainer': // className={"..."} return node.value.expression.type === 'Literal'; } } return false; } /** * Find out if the node is a valid candidate for our rules * * @param {ASTNode} node The AST node being checked * @param {String} classRegex Regex to test the attribute that is being checked against * @returns {Boolean} */ function isValidJSXAttribute(node, classRegex) { if (!isClassAttribute(node, classRegex)) { // Only run for class[Name] attributes return false; } if (!isLiteralAttributeValue(node)) { // No support for dynamic or conditional classnames return false; } return true; } /** * Find out if the node is a valid candidate for our rules * * @param {ASTNode} node The AST node being checked * @param {String} classRegex Regex to test the attribute that is being checked against * @returns {Boolean} */ function isValidVueAttribute(node, classRegex) { if (!isVueClassAttribute(node, classRegex)) { // Only run for class attributes return false; } if (!isVueValidAttributeValue(node)) { // No support for dynamic or conditional classnames return false; } return true; } function extractRangeFromNode(node) { if (node.type === 'TextAttribute' && node.name === 'class') { return [node.valueSpan.fullStart.offset, node.valueSpan.end.offset]; } if (node.value === undefined) { return [0, 0]; } switch (node.value.type) { case 'JSXExpressionContainer': return node.value.expression.range; default: return node.value.range; } } function extractValueFromNode(node) { if (node.type === 'TextAttribute' && node.name === 'class') { return node.value; } if (node.value === undefined) { return node.value; } switch (node.value.type) { case 'JSXExpressionContainer': return node.value.expression.value; case 'VExpressionContainer': switch (node.value.expression.type) { case 'ArrayExpression': return node.value.expression.elements; case 'ObjectExpression': return node.value.expression.properties; } return node.value.expression.value; default: return node.value.value; } } function extractClassnamesFromValue(classStr) { if (typeof classStr !== 'string') { return { classNames: [], whitespaces: [], headSpace: false, tailSpace: false }; } let parts = classStr.split(separatorRegEx); if (parts[0] === '') { parts.shift(); } if (parts[parts.length - 1] === '') { parts.pop(); } let headSpace = separatorRegEx.test(parts[0]); let tailSpace = separatorRegEx.test(parts[parts.length - 1]); const isClass = (_, i) => (headSpace ? i % 2 !== 0 : i % 2 === 0); const isNotClass = (_, i) => (headSpace ? i % 2 === 0 : i % 2 !== 0); let classNames = parts.filter(isClass); let whitespaces = parts.filter(isNotClass); return { classNames: classNames, whitespaces: whitespaces, headSpace: headSpace, tailSpace: tailSpace }; } /** * Inspect and parse an abstract syntax node and run a callback function * * @param {ASTNode} rootNode The current root node being parsed by eslint * @param {ASTNode} childNode The AST node child argument being checked * @param {Function} cb The callback function * @param {Boolean} skipConditional Optional, indicate distinct parsing for conditional nodes * @param {Boolean} isolate Optional, set internally to isolate parsing and validation on conditional children * @param {Array} ignoredKeys Optional, set object keys which should not be parsed e.g. for `cva` * @returns {void} */ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, isolate = false, ignoredKeys = []) { // TODO allow vue non litteral let originalClassNamesValue; let classNames; if (childNode === null) { originalClassNamesValue = extractValueFromNode(rootNode); ({ classNames } = extractClassnamesFromValue(originalClassNamesValue)); classNames = removeDuplicatesFromArray(classNames); if (classNames.length === 0) { // Don't run for empty className return; } cb(classNames, rootNode); } else if (childNode === undefined) { // Ignore invalid child candidates (probably inside complex TemplateLiteral) return; } else { const forceIsolation = skipConditional ? true : isolate; let trim = false; switch (childNode.type) { case 'TemplateLiteral': childNode.expressions.forEach((exp) => { parseNodeRecursive(rootNode, exp, cb, skipConditional, forceIsolation, ignoredKeys); }); childNode.quasis.forEach((quasis) => { parseNodeRecursive(rootNode, quasis, cb, skipConditional, isolate, ignoredKeys); }); return; case 'ConditionalExpression': parseNodeRecursive(rootNode, childNode.consequent, cb, skipConditional, forceIsolation, ignoredKeys); parseNodeRecursive(rootNode, childNode.alternate, cb, skipConditional, forceIsolation, ignoredKeys); return; case 'LogicalExpression': parseNodeRecursive(rootNode, childNode.right, cb, skipConditional, forceIsolation, ignoredKeys); return; case 'ArrayExpression': childNode.elements.forEach((el) => { parseNodeRecursive(rootNode, el, cb, skipConditional, forceIsolation, ignoredKeys); }); return; case 'ObjectExpression': childNode.properties.forEach((prop) => { const isUsedByClassNamesPlugin = (rootNode.callee && rootNode.callee.name === 'classnames') || rootNode.type === 'VAttribute'; if (prop.type === 'SpreadElement') { // Ignore spread elements return; } if (prop.key.type === 'Identifier' && ignoredKeys.includes(prop.key.name)) { // Ignore specific keys defined in settings return; } parseNodeRecursive( rootNode, isUsedByClassNamesPlugin ? prop.key : prop.value, cb, skipConditional, forceIsolation, ignoredKeys ); }); return; case 'Property': parseNodeRecursive(rootNode, childNode.key, cb, skipConditional, forceIsolation, ignoredKeys); return; case 'Literal': trim = true; originalClassNamesValue = childNode.value; break; case 'TemplateElement': originalClassNamesValue = childNode.value.raw; break; } ({ classNames } = extractClassnamesFromValue(originalClassNamesValue)); classNames = removeDuplicatesFromArray(classNames); if (classNames.length === 0) { // Don't run for empty className return; } const targetNode = isolate ? null : rootNode; cb(classNames, targetNode); } } function getTemplateElementPrefix(text, raw) { const idx = text.indexOf(raw); if (idx === 0) { return ''; } return text.split(raw).shift(); } function getTemplateElementSuffix(text, raw) { if (text.indexOf(raw) === -1) { return ''; } return text.split(raw).pop(); } function getTemplateElementBody(text, prefix, suffix) { let arr = text.split(prefix); arr.shift(); let body = arr.join(prefix); arr = body.split(suffix); arr.pop(); return arr.join(suffix); } module.exports = { calleeToString, extractRangeFromNode, extractValueFromNode, extractClassnamesFromValue, isClassAttribute, isLiteralAttributeValue, isArrayExpression, isObjectExpression, isValidJSXAttribute, isValidVueAttribute, isVLiteralValue, parseNodeRecursive, getTemplateElementPrefix, getTemplateElementSuffix, getTemplateElementBody, };