diff --git a/lib/rules/classnames-order.js b/lib/rules/classnames-order.js index d3782d78..03fa0473 100644 --- a/lib/rules/classnames-order.js +++ b/lib/rules/classnames-order.js @@ -8,7 +8,7 @@ const docsUrl = require('../util/docsUrl'); const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const removeDuplicatesFromClassnamesAndWhitespaces = require('../util/removeDuplicatesFromClassnamesAndWhitespaces'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); const order = require('../util/prettier/order'); const createContextFallback = require('tailwindcss/lib/lib/setupContextUtils').createContext; @@ -68,12 +68,14 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); const twConfig = getOption(context, 'config'); const classRegex = getOption(context, 'classRegex'); const removeDuplicates = getOption(context, 'removeDuplicates'); + const classDeclarationRegex = getOption(context, 'declarationRegex'); + const skipClassDeclaration = getOption(context, 'skipClassDeclaration'); const mergedConfig = customConfig.resolve(twConfig); const contextFallback = // Set the created contextFallback in the cache if it does not exist yet. @@ -92,7 +94,7 @@ module.exports = { * @param {ASTNode} arg The child node of node * @returns {void} */ - const sortNodeArgumentValue = (node, arg = null) => { + const sortNodeArgumentValue = (node, arg = null, calleeKey = false) => { let originalClassNamesValue = null; let start = null; let end = null; @@ -114,34 +116,33 @@ module.exports = { return; case 'TemplateLiteral': arg.expressions.forEach((exp) => { - sortNodeArgumentValue(node, exp); + sortNodeArgumentValue(node, exp, calleeKey); }); arg.quasis.forEach((quasis) => { - sortNodeArgumentValue(node, quasis); + sortNodeArgumentValue(node, quasis, calleeKey); }); return; case 'ConditionalExpression': - sortNodeArgumentValue(node, arg.consequent); - sortNodeArgumentValue(node, arg.alternate); + sortNodeArgumentValue(node, arg.consequent, calleeKey); + sortNodeArgumentValue(node, arg.alternate, calleeKey); return; case 'LogicalExpression': - sortNodeArgumentValue(node, arg.right); + sortNodeArgumentValue(node, arg.right, calleeKey); return; case 'ArrayExpression': arg.elements.forEach((el) => { - sortNodeArgumentValue(node, el); + sortNodeArgumentValue(node, el, calleeKey); }); return; case 'ObjectExpression': - const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames'; const isVue = node.key && node.key.type === 'VDirectiveKey'; arg.properties.forEach((prop) => { - const propVal = isUsedByClassNamesPlugin || isVue ? prop.key : prop.value; - sortNodeArgumentValue(node, propVal); + const propVal = calleeKey || isVue ? prop.key : prop.value; + sortNodeArgumentValue(node, propVal, calleeKey); }); return; case 'Property': - sortNodeArgumentValue(node, arg.key); + sortNodeArgumentValue(node, arg.key, calleeKey); break; case 'Literal': originalClassNamesValue = arg.value; @@ -220,20 +221,31 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } node.arguments.forEach((arg) => { - sortNodeArgumentValue(node, arg); + sortNodeArgumentValue(node, arg, calleeType === 'key'); }); }; + const variableDeclaratorVistor = function (node) { + if (!astUtil.isVariableDeclaration(node, classDeclarationRegex) || skipClassDeclaration) { + return; + } + if (!astUtil.isValidDeclaratorValue(node)) { + return; + } + sortNodeArgumentValue(node, node.init); + }; + const scriptVisitor = { JSXAttribute: attributeVisitor, TextAttribute: attributeVisitor, CallExpression: callExpressionVisitor, + VariableDeclarator: variableDeclaratorVistor, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return; @@ -255,12 +267,12 @@ module.exports = { case astUtil.isVLiteralValue(node): sortNodeArgumentValue(node, null); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): node.value.expression.elements.forEach((arg) => { sortNodeArgumentValue(node, arg); }); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { sortNodeArgumentValue(node, prop); }); diff --git a/lib/rules/enforces-negative-arbitrary-values.js b/lib/rules/enforces-negative-arbitrary-values.js index 2ef01f30..d7d5b228 100644 --- a/lib/rules/enforces-negative-arbitrary-values.js +++ b/lib/rules/enforces-negative-arbitrary-values.js @@ -8,7 +8,7 @@ const docsUrl = require('../util/docsUrl'); const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const groupUtil = require('../util/groupMethods'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); //------------------------------------------------------------------------------ @@ -60,11 +60,13 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); const twConfig = getOption(context, 'config'); const classRegex = getOption(context, 'classRegex'); + const classDeclarationRegex = getOption(context, 'declarationRegex'); + const skipClassDeclaration = getOption(context, 'skipClassDeclaration'); const mergedConfig = customConfig.resolve(twConfig); @@ -78,7 +80,7 @@ module.exports = { * @param {ASTNode} arg The child node of node * @returns {void} */ - const parseForNegativeArbitraryClassNames = (node, arg = null) => { + const parseForNegativeArbitraryClassNames = (node, arg = null, calleeKey = false) => { let originalClassNamesValue = null; if (arg === null) { originalClassNamesValue = astUtil.extractValueFromNode(node); @@ -88,34 +90,33 @@ module.exports = { return; case 'TemplateLiteral': arg.expressions.forEach((exp) => { - parseForNegativeArbitraryClassNames(node, exp); + parseForNegativeArbitraryClassNames(node, exp, calleeKey); }); arg.quasis.forEach((quasis) => { - parseForNegativeArbitraryClassNames(node, quasis); + parseForNegativeArbitraryClassNames(node, quasis, calleeKey); }); return; case 'ConditionalExpression': - parseForNegativeArbitraryClassNames(node, arg.consequent); - parseForNegativeArbitraryClassNames(node, arg.alternate); + parseForNegativeArbitraryClassNames(node, arg.consequent, calleeKey); + parseForNegativeArbitraryClassNames(node, arg.alternate, calleeKey); return; case 'LogicalExpression': - parseForNegativeArbitraryClassNames(node, arg.right); + parseForNegativeArbitraryClassNames(node, arg.right, calleeKey); return; case 'ArrayExpression': arg.elements.forEach((el) => { - parseForNegativeArbitraryClassNames(node, el); + parseForNegativeArbitraryClassNames(node, el, calleeKey); }); return; case 'ObjectExpression': - const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames'; const isVue = node.key && node.key.type === 'VDirectiveKey'; arg.properties.forEach((prop) => { - const propVal = isUsedByClassNamesPlugin || isVue ? prop.key : prop.value; - parseForNegativeArbitraryClassNames(node, propVal); + const propVal = calleeKey || isVue ? prop.key : prop.value; + parseForNegativeArbitraryClassNames(node, propVal, calleeKey); }); return; case 'Property': - parseForNegativeArbitraryClassNames(node, arg.key); + parseForNegativeArbitraryClassNames(node, arg.key, calleeKey); return; case 'Literal': originalClassNamesValue = arg.value; @@ -165,19 +166,30 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } node.arguments.forEach((arg) => { - parseForNegativeArbitraryClassNames(node, arg); + parseForNegativeArbitraryClassNames(node, arg, calleeType === 'key'); }); }; + const variableDeclaratorVistor = function (node) { + if (!astUtil.isVariableDeclaration(node, classDeclarationRegex) || skipClassDeclaration) { + return; + } + if (!astUtil.isValidDeclaratorValue(node)) { + return; + } + parseForNegativeArbitraryClassNames(node, node.init); + }; + const scriptVisitor = { JSXAttribute: attributeVisitor, TextAttribute: attributeVisitor, CallExpression: callExpressionVisitor, + VariableDeclarator: variableDeclaratorVistor, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return; @@ -199,12 +211,12 @@ module.exports = { case astUtil.isVLiteralValue(node): parseForNegativeArbitraryClassNames(node); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): node.value.expression.elements.forEach((arg) => { parseForNegativeArbitraryClassNames(node, arg); }); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { parseForNegativeArbitraryClassNames(node, prop); }); diff --git a/lib/rules/enforces-shorthand.js b/lib/rules/enforces-shorthand.js index 9e40ba3b..5270d259 100644 --- a/lib/rules/enforces-shorthand.js +++ b/lib/rules/enforces-shorthand.js @@ -9,7 +9,7 @@ const defaultGroups = require('../config/groups').groups; const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const groupUtil = require('../util/groupMethods'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); //------------------------------------------------------------------------------ @@ -61,11 +61,13 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); const twConfig = getOption(context, 'config'); const classRegex = getOption(context, 'classRegex'); + const classDeclarationRegex = getOption(context, 'declarationRegex'); + const skipClassDeclaration = getOption(context, 'skipClassDeclaration'); const mergedConfig = customConfig.resolve(twConfig); @@ -125,7 +127,7 @@ module.exports = { * @param {ASTNode} arg The child node of node * @returns {void} */ - const parseForShorthandCandidates = (node, arg = null) => { + const parseForShorthandCandidates = (node, arg = null, calleeKey = false) => { let originalClassNamesValue = null; let start = null; let end = null; @@ -148,34 +150,33 @@ module.exports = { return; case 'TemplateLiteral': arg.expressions.forEach((exp) => { - parseForShorthandCandidates(node, exp); + parseForShorthandCandidates(node, exp, calleeKey); }); arg.quasis.forEach((quasis) => { - parseForShorthandCandidates(node, quasis); + parseForShorthandCandidates(node, quasis, calleeKey); }); return; case 'ConditionalExpression': - parseForShorthandCandidates(node, arg.consequent); - parseForShorthandCandidates(node, arg.alternate); + parseForShorthandCandidates(node, arg.consequent, calleeKey); + parseForShorthandCandidates(node, arg.alternate, calleeKey); return; case 'LogicalExpression': - parseForShorthandCandidates(node, arg.right); + parseForShorthandCandidates(node, arg.right, calleeKey); return; case 'ArrayExpression': arg.elements.forEach((el) => { - parseForShorthandCandidates(node, el); + parseForShorthandCandidates(node, el, calleeKey); }); return; case 'ObjectExpression': - const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames'; const isVue = node.key && node.key.type === 'VDirectiveKey'; arg.properties.forEach((prop) => { - const propVal = isUsedByClassNamesPlugin || isVue ? prop.key : prop.value; - parseForShorthandCandidates(node, propVal); + const propVal = calleeKey || isVue ? prop.key : prop.value; + parseForShorthandCandidates(node, propVal, calleeKey); }); return; case 'Property': - parseForShorthandCandidates(node, arg.key); + parseForShorthandCandidates(node, arg.key, calleeKey); return; case 'Literal': @@ -443,20 +444,31 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } node.arguments.forEach((arg) => { - parseForShorthandCandidates(node, arg); + parseForShorthandCandidates(node, arg, calleeType === 'key'); }); }; + const variableDeclaratorVistor = function (node) { + if (!astUtil.isVariableDeclaration(node, classDeclarationRegex) || skipClassDeclaration) { + return; + } + if (!astUtil.isValidDeclaratorValue(node)) { + return; + } + parseForShorthandCandidates(node, node.init); + }; + const scriptVisitor = { JSXAttribute: attributeVisitor, TextAttribute: attributeVisitor, CallExpression: callExpressionVisitor, + VariableDeclarator: variableDeclaratorVistor, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return; @@ -479,12 +491,12 @@ module.exports = { case astUtil.isVLiteralValue(node): parseForShorthandCandidates(node); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): node.value.expression.elements.forEach((arg) => { parseForShorthandCandidates(node, arg); }); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { parseForShorthandCandidates(node, prop); }); diff --git a/lib/rules/migration-from-tailwind-2.js b/lib/rules/migration-from-tailwind-2.js index 434d1023..d063bae5 100644 --- a/lib/rules/migration-from-tailwind-2.js +++ b/lib/rules/migration-from-tailwind-2.js @@ -8,7 +8,7 @@ const docsUrl = require('../util/docsUrl'); const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const groupUtil = require('../util/groupMethods'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); //------------------------------------------------------------------------------ @@ -66,7 +66,7 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); const twConfig = getOption(context, 'config'); @@ -276,8 +276,8 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } node.arguments.forEach((arg) => { @@ -310,12 +310,12 @@ module.exports = { case astUtil.isVLiteralValue(node): parseForObsoleteClassNames(node); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): node.value.expression.elements.forEach((arg) => { parseForObsoleteClassNames(node, arg); }); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { parseForObsoleteClassNames(node, prop); }); diff --git a/lib/rules/no-arbitrary-value.js b/lib/rules/no-arbitrary-value.js index eac5e25a..c50d2e78 100644 --- a/lib/rules/no-arbitrary-value.js +++ b/lib/rules/no-arbitrary-value.js @@ -8,7 +8,7 @@ const docsUrl = require('../util/docsUrl'); const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const groupUtil = require('../util/groupMethods'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); //------------------------------------------------------------------------------ @@ -60,11 +60,13 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); const twConfig = getOption(context, 'config'); const classRegex = getOption(context, 'classRegex'); + const classDeclarationRegex = getOption(context, 'declarationRegex'); + const skipClassDeclaration = getOption(context, 'skipClassDeclaration'); const mergedConfig = customConfig.resolve(twConfig); @@ -78,7 +80,7 @@ module.exports = { * @param {ASTNode} arg The child node of node * @returns {void} */ - const parseForArbitraryValues = (node, arg = null) => { + const parseForArbitraryValues = (node, arg = null, calleeKey = false) => { let originalClassNamesValue = null; if (arg === null) { originalClassNamesValue = astUtil.extractValueFromNode(node); @@ -88,34 +90,33 @@ module.exports = { return; case 'TemplateLiteral': arg.expressions.forEach((exp) => { - parseForArbitraryValues(node, exp); + parseForArbitraryValues(node, exp, calleeKey); }); arg.quasis.forEach((quasis) => { - parseForArbitraryValues(node, quasis); + parseForArbitraryValues(node, quasis, calleeKey); }); return; case 'ConditionalExpression': - parseForArbitraryValues(node, arg.consequent); - parseForArbitraryValues(node, arg.alternate); + parseForArbitraryValues(node, arg.consequent, calleeKey); + parseForArbitraryValues(node, arg.alternate, calleeKey); return; case 'LogicalExpression': - parseForArbitraryValues(node, arg.right); + parseForArbitraryValues(node, arg.right, calleeKey); return; case 'ArrayExpression': arg.elements.forEach((el) => { - parseForArbitraryValues(node, el); + parseForArbitraryValues(node, el, calleeKey); }); return; case 'ObjectExpression': - const isUsedByClassNamesPlugin = node.callee && node.callee.name === 'classnames'; const isVue = node.key && node.key.type === 'VDirectiveKey'; arg.properties.forEach((prop) => { - const propVal = isUsedByClassNamesPlugin || isVue ? prop.key : prop.value; - parseForArbitraryValues(node, propVal); + const propVal = calleeKey || isVue ? prop.key : prop.value; + parseForArbitraryValues(node, propVal, calleeKey); }); return; case 'Property': - parseForArbitraryValues(node, arg.key); + parseForArbitraryValues(node, arg.key, calleeKey); return; case 'Literal': originalClassNamesValue = arg.value; @@ -165,19 +166,30 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } node.arguments.forEach((arg) => { - parseForArbitraryValues(node, arg); + parseForArbitraryValues(node, arg, calleeType === 'key'); }); }; + const variableDeclaratorVistor = function (node) { + if (!astUtil.isVariableDeclaration(node, classDeclarationRegex) || skipClassDeclaration) { + return; + } + if (!astUtil.isValidDeclaratorValue(node)) { + return; + } + parseForArbitraryValues(node, node.init); + }; + const scriptVisitor = { JSXAttribute: attributeVisitor, TextAttribute: attributeVisitor, CallExpression: callExpressionVisitor, + VariableDeclarator: variableDeclaratorVistor, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return; @@ -199,12 +211,12 @@ module.exports = { case astUtil.isVLiteralValue(node): parseForArbitraryValues(node, null); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): node.value.expression.elements.forEach((arg) => { parseForArbitraryValues(node, arg); }); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { parseForArbitraryValues(node, prop); }); diff --git a/lib/rules/no-contradicting-classname.js b/lib/rules/no-contradicting-classname.js index 188f3d7b..59d2a83f 100644 --- a/lib/rules/no-contradicting-classname.js +++ b/lib/rules/no-contradicting-classname.js @@ -9,7 +9,7 @@ const defaultGroups = require('../config/groups').groups; const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const groupUtil = require('../util/groupMethods'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); //------------------------------------------------------------------------------ @@ -61,12 +61,14 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const ignoredKeys = getOption(context, 'ignoredKeys'); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); const twConfig = getOption(context, 'config'); const classRegex = getOption(context, 'classRegex'); + const classDeclarationRegex = getOption(context, 'declarationRegex'); + const skipClassDeclaration = getOption(context, 'skipClassDeclaration'); const mergedConfig = customConfig.resolve(twConfig); @@ -178,8 +180,8 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } const allClassnamesForNode = []; @@ -193,15 +195,26 @@ module.exports = { } }; node.arguments.forEach((arg) => { - astUtil.parseNodeRecursive(node, arg, pushClasses, true, false, ignoredKeys); + astUtil.parseNodeRecursive(node, arg, pushClasses, true, false, ignoredKeys, calleeType === 'key'); }); parseForContradictingClassNames(allClassnamesForNode, node); }; + const variableDeclaratorVistor = function (node) { + if (!astUtil.isVariableDeclaration(node, classDeclarationRegex) || skipClassDeclaration) { + return; + } + if (!astUtil.isValidDeclaratorValue(node)) { + return; + } + astUtil.parseNodeRecursive(node, node.init, parseForContradictingClassNames, true, false, ignoredKeys); + }; + const scriptVisitor = { JSXAttribute: attributeVisitor, TextAttribute: attributeVisitor, CallExpression: callExpressionVisitor, + VariableDeclarator: variableDeclaratorVistor, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return; @@ -235,7 +248,7 @@ module.exports = { case astUtil.isVLiteralValue(node): astUtil.parseNodeRecursive(node, null, parseForContradictingClassNames, true, false, ignoredKeys); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): const allClassnamesForNode = []; const pushClasses = (classNames, targetNode) => { if (targetNode === null) { @@ -251,7 +264,7 @@ module.exports = { }); parseForContradictingClassNames(allClassnamesForNode, node); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { astUtil.parseNodeRecursive(node, prop, parseForContradictingClassNames, false, false, ignoredKeys); }); diff --git a/lib/rules/no-custom-classname.js b/lib/rules/no-custom-classname.js index aceb171a..48ad78d8 100644 --- a/lib/rules/no-custom-classname.js +++ b/lib/rules/no-custom-classname.js @@ -9,7 +9,7 @@ const defaultGroups = require('../config/groups').groups; const customConfig = require('../util/customConfig'); const astUtil = require('../util/ast'); const groupUtil = require('../util/groupMethods'); -const getOption = require('../util/settings'); +const { getOption, normalizeCallees } = require('../util/settings'); const parserUtil = require('../util/parser'); const getClassnamesFromCSS = require('../util/cssFiles'); const createContextFallback = require('tailwindcss/lib/lib/setupContextUtils').createContext; @@ -49,7 +49,7 @@ module.exports = { type: 'object', properties: { callees: { - type: 'array', + type: ['array', 'object'], items: { type: 'string', minLength: 0 }, uniqueItems: true, }, @@ -87,7 +87,7 @@ module.exports = { }, create: function (context) { - const callees = getOption(context, 'callees'); + const callees = normalizeCallees(getOption(context, 'callees')); const ignoredKeys = getOption(context, 'ignoredKeys'); const skipClassAttribute = getOption(context, 'skipClassAttribute'); const tags = getOption(context, 'tags'); @@ -96,6 +96,8 @@ module.exports = { const cssFilesRefreshRate = getOption(context, 'cssFilesRefreshRate'); const whitelist = getOption(context, 'whitelist'); const classRegex = getOption(context, 'classRegex'); + const classDeclarationRegex = getOption(context, 'declarationRegex'); + const skipClassDeclaration = getOption(context, 'skipClassDeclaration'); const mergedConfig = customConfig.resolve(twConfig); const contextFallback = // Set the created contextFallback in the cache if it does not exist yet. @@ -168,19 +170,30 @@ module.exports = { }; const callExpressionVisitor = function (node) { - const calleeStr = astUtil.calleeToString(node.callee); - if (callees.findIndex((name) => calleeStr === name) === -1) { + const calleeType = callees[astUtil.calleeToString(node.callee)]; + if (!calleeType) { return; } node.arguments.forEach((arg) => { - astUtil.parseNodeRecursive(node, arg, parseForCustomClassNames, false, false, ignoredKeys); + astUtil.parseNodeRecursive(node, arg, parseForCustomClassNames, false, false, ignoredKeys, calleeType === 'key'); }); }; + const variableDeclaratorVistor = function (node) { + if (!astUtil.isVariableDeclaration(node, classDeclarationRegex) || skipClassDeclaration) { + return; + } + if (!astUtil.isValidDeclaratorValue(node)) { + return; + } + astUtil.parseNodeRecursive(node, node.init, parseForCustomClassNames, false, false, ignoredKeys); + }; + const scriptVisitor = { JSXAttribute: attributeVisitor, TextAttribute: attributeVisitor, CallExpression: callExpressionVisitor, + VariableDeclarator: variableDeclaratorVistor, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return; @@ -202,12 +215,12 @@ module.exports = { case astUtil.isVLiteralValue(node): astUtil.parseNodeRecursive(node, null, parseForCustomClassNames, false, false, ignoredKeys); break; - case astUtil.isArrayExpression(node): + case astUtil.isVArrayExpression(node): node.value.expression.elements.forEach((arg) => { astUtil.parseNodeRecursive(node, arg, parseForCustomClassNames, false, false, ignoredKeys); }); break; - case astUtil.isObjectExpression(node): + case astUtil.isVObjectExpression(node): node.value.expression.properties.forEach((prop) => { astUtil.parseNodeRecursive(node, prop, parseForCustomClassNames, false, false, ignoredKeys); }); diff --git a/lib/util/ast.js b/lib/util/ast.js index e420ef18..ce9af372 100644 --- a/lib/util/ast.js +++ b/lib/util/ast.js @@ -42,6 +42,28 @@ function isClassAttribute(node, classRegex) { return new RegExp(classRegex).test(name); } +/** + * Find out if node is a variable declaration + * + * @param {ASTNode} node The AST node being checked + * @param {String} classDeclarationRegex Regex to test the attribute that is being checked against + * @returns {Boolean} + */ +function isVariableDeclaration(node, classDeclarationRegex) { + let name; + switch (node.type) { + case 'VariableDeclarator': + name = node.id.name; + break; + default: + name = ''; + } + if (!name) { + return false; + } + return new RegExp(classDeclarationRegex).test(name); +} + /** * Find out if node is `class` * @@ -71,6 +93,64 @@ function isVueClassAttribute(node, classRegex) { } } +/** + * Find out if a node is just simple text + * + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} + */ +function isLiteralValue(node) { + return node && node.type === 'Literal'; +} + +/** + * Find out if a node is an ArrayExpression + * + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} + */ +function isArrayExpression(node) { + return node && node.type === 'ArrayExpression'; +} + +/** + * Find out if a node is an ObjectExpression + * + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} + */ +function isObjectExpression(node) { + return node && node.type === 'ObjectExpression'; +} + +/** + * Find out if a node is an ConditionalExpression + * + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} + */ +function isConditionalExpression(node) { + return node && node.type === 'ConditionalExpression'; +} + +/** + * Find out if node's value attribute is just simple text + * + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} + */ +function isValidDeclaratorValue(node) { + switch (true) { + case isLiteralValue(node.init): // Simple string + case isArrayExpression(node.init): // ['tw-unknown-class'] + case isObjectExpression(node.init): // {'tw-unknown-class': true} + case isConditionalExpression(node.init): // bool ? 'tw-unknown-class' : 'tw-unknown-class' + return true; + default: + return false; + } +} + /** * Find out if node's value attribute is just simple text * @@ -87,7 +167,7 @@ function isVLiteralValue(node) { * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ -function isArrayExpression(node) { +function isVArrayExpression(node) { return node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ArrayExpression'; } @@ -97,7 +177,7 @@ function isArrayExpression(node) { * @param {ASTNode} node The AST node being checked * @returns {Boolean} */ -function isObjectExpression(node) { +function isVObjectExpression(node) { return node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ObjectExpression'; } @@ -110,10 +190,9 @@ function isObjectExpression(node) { function isVueValidAttributeValue(node) { switch (true) { case isVLiteralValue(node): // Simple string - case isArrayExpression(node): // ['tw-unknown-class'] - case isObjectExpression(node): // {'tw-unknown-class': true} + case isVArrayExpression(node): // ['tw-unknown-class'] + case isVObjectExpression(node): // {'tw-unknown-class': true} return true; - break; default: return false; } @@ -245,7 +324,7 @@ function extractClassnamesFromValue(classStr) { * @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 = []) { +function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, isolate = false, ignoredKeys = [], calleeKey = false) { // TODO allow vue non litteral let originalClassNamesValue; let classNames; @@ -267,28 +346,26 @@ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, is switch (childNode.type) { case 'TemplateLiteral': childNode.expressions.forEach((exp) => { - parseNodeRecursive(rootNode, exp, cb, skipConditional, forceIsolation, ignoredKeys); + parseNodeRecursive(rootNode, exp, cb, skipConditional, forceIsolation, ignoredKeys, calleeKey); }); childNode.quasis.forEach((quasis) => { - parseNodeRecursive(rootNode, quasis, cb, skipConditional, isolate, ignoredKeys); + parseNodeRecursive(rootNode, quasis, cb, skipConditional, isolate, ignoredKeys, calleeKey); }); return; case 'ConditionalExpression': - parseNodeRecursive(rootNode, childNode.consequent, cb, skipConditional, forceIsolation, ignoredKeys); - parseNodeRecursive(rootNode, childNode.alternate, cb, skipConditional, forceIsolation, ignoredKeys); + parseNodeRecursive(rootNode, childNode.consequent, cb, skipConditional, forceIsolation, ignoredKeys, calleeKey); + parseNodeRecursive(rootNode, childNode.alternate, cb, skipConditional, forceIsolation, ignoredKeys, calleeKey); return; case 'LogicalExpression': - parseNodeRecursive(rootNode, childNode.right, cb, skipConditional, forceIsolation, ignoredKeys); + parseNodeRecursive(rootNode, childNode.right, cb, skipConditional, forceIsolation, ignoredKeys, calleeKey); return; case 'ArrayExpression': childNode.elements.forEach((el) => { - parseNodeRecursive(rootNode, el, cb, skipConditional, forceIsolation, ignoredKeys); + parseNodeRecursive(rootNode, el, cb, skipConditional, forceIsolation, ignoredKeys, calleeKey); }); return; case 'ObjectExpression': childNode.properties.forEach((prop) => { - const isUsedByClassNamesPlugin = rootNode.callee && rootNode.callee.name === 'classnames'; - if (prop.type === 'SpreadElement') { // Ignore spread elements return; @@ -301,16 +378,17 @@ function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, is parseNodeRecursive( rootNode, - isUsedByClassNamesPlugin ? prop.key : prop.value, + calleeKey ? prop.key : prop.value, cb, skipConditional, forceIsolation, - ignoredKeys + ignoredKeys, + calleeKey ); }); return; case 'Property': - parseNodeRecursive(rootNode, childNode.key, cb, skipConditional, forceIsolation, ignoredKeys); + parseNodeRecursive(rootNode, childNode.key, cb, skipConditional, forceIsolation, ignoredKeys, calleeKey); return; case 'Literal': trim = true; @@ -361,9 +439,15 @@ module.exports = { extractValueFromNode, extractClassnamesFromValue, isClassAttribute, - isLiteralAttributeValue, + isVariableDeclaration, + isLiteralValue, isArrayExpression, isObjectExpression, + isConditionalExpression, + isLiteralAttributeValue, + isValidDeclaratorValue, + isVArrayExpression, + isVObjectExpression, isValidJSXAttribute, isValidVueAttribute, isVLiteralValue, diff --git a/lib/util/settings.js b/lib/util/settings.js index 6674463b..7ec60c9a 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -14,7 +14,7 @@ function getOption(context, name) { // Fallback to defaults switch (name) { case 'callees': - return ['classnames', 'clsx', 'ctl', 'cva', 'tv']; + return { classnames: 'key', clsx: 'key', ctl: 'value', cva: 'value', tv: 'value' }; case 'ignoredKeys': return ['compoundVariants', 'defaultVariants']; case 'classRegex': @@ -33,7 +33,34 @@ function getOption(context, name) { return []; case 'whitelist': return []; + case 'declarationRegex': + return '^(classes|classNames?|.*Styles)$'; + case 'skipClassDeclaration': + return false; + } +} + +function normalizeCallees(callees) { + const defaultCallees = getOption({ options: [{}] }, 'callees'); + if (Array.isArray(callees)) { + return callees.reduce((acc, callee) => { + if (typeof callee === 'string') { + acc[callee] = defaultCallees[callee] ?? 'value'; + } else { + throw new Error(`Invalid callee: ${callee}`); + } + return acc; + }, {}); } + Object.keys(callees).forEach((callee) => { + if (callees[callee] !== 'key' && callees[callee] !== 'value') { + throw new Error(`Invalid callee: ${callee}`); + } + }); + return callees; } -module.exports = getOption; +module.exports = { + getOption, + normalizeCallees +};