diff --git a/packages/eslint-plugin/lib/rules/class-order.js b/packages/eslint-plugin/lib/rules/class-order.js index dd82636cb..ce9c56c1f 100644 --- a/packages/eslint-plugin/lib/rules/class-order.js +++ b/packages/eslint-plugin/lib/rules/class-order.js @@ -8,18 +8,10 @@ // Modified from https://github.com/francoismassart/eslint-plugin-tailwindcss const astUtil = require('../util/ast') -const removeDuplicatesFromClassnamesAndWhitespaces = require('../util/removeDuplicatesFromClassnamesAndWhitespaces') const getOption = require('../util/settings') -const parserUtil = require('../util/parser') const { reorderForReadableClasses } = require('@master/css') -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ - -// Predefine message for use in context.report conditional. -// messageId will still be usable in tests. const INVALID_CLASSNAMES_ORDER_MSG = 'No consistent class order followed.' module.exports = { @@ -43,19 +35,10 @@ module.exports = { items: { type: 'string', minLength: 0 }, uniqueItems: true, }, - ignoredKeys: { - type: 'array', - items: { type: 'string', minLength: 0 }, - uniqueItems: true, - }, config: { // returned from `loadConfig()` utility type: ['string', 'object'], }, - removeDuplicates: { - // default: true, - type: 'boolean', - }, tags: { type: 'array', items: { type: 'string', minLength: 0 }, @@ -68,20 +51,10 @@ module.exports = { create: function (context) { const callees = getOption(context, 'callees') - const skipClassAttribute = getOption(context, 'skipClassAttribute') const tags = getOption(context, 'tags') const masterCssConfig = getOption(context, 'config') const classRegex = getOption(context, 'classRegex') - //---------------------------------------------------------------------- - // Helpers - //---------------------------------------------------------------------- - /** - * Recursive function crawling into child nodes - * @param {ASTNode} node The root node of the current parsing - * @param {ASTNode} arg The child node of node - * @returns {void} - */ const sortNodeArgumentValue = (node, arg = null) => { let originalClassNamesValue = null let start = null @@ -145,7 +118,7 @@ module.exports = { } start = arg.range[0] end = arg.range[1] - break; + break case 'TemplateElement': originalClassNamesValue = arg.value.raw if (originalClassNamesValue === '') { @@ -176,9 +149,9 @@ module.exports = { let orderedClassNames = reorderForReadableClasses(classNames, masterCssConfig) .filter(eachOrderedClassName => classNames.includes(eachOrderedClassName)) - orderedClassNames = orderedClassNames.concat(classNames.filter(x => !orderedClassNames.includes(x))) - - removeDuplicatesFromClassnamesAndWhitespaces(orderedClassNames, whitespaces, headSpace, tailSpace) + orderedClassNames = orderedClassNames + .concat(classNames.filter(x => !orderedClassNames.includes(x))) + .filter(x => x.trim() !== '') // Generates the validated/sorted attribute value let validatedClassNamesValue = '' @@ -203,21 +176,6 @@ module.exports = { } } - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- - - const attributeVisitor = function (node) { - if (!astUtil.isClassAttribute(node, classRegex) || skipClassAttribute) { - return - } - if (astUtil.isLiteralAttributeValue(node)) { - sortNodeArgumentValue(node) - } else if (node.value && node.value.type === 'JSXExpressionContainer') { - sortNodeArgumentValue(node, node.value.expression) - } - } - const callExpressionVisitor = function (node) { const calleeStr = astUtil.calleeToString(node.callee) if (callees.findIndex((name) => calleeStr === name) === -1) { @@ -230,53 +188,51 @@ module.exports = { } const scriptVisitor = { - JSXAttribute: attributeVisitor, - SvelteAttribute: function (node) { - if (!node.key?.name) return - if (!new RegExp(classRegex).test(node.key.name) || skipClassAttribute) { - return + CallExpression: callExpressionVisitor, + JSXAttribute: function (node) { + if (!node.name || !new RegExp(classRegex).test(node.name.name)) return + if (node.value && node.value.type === 'Literal') { + sortNodeArgumentValue(node) + } else if (node.value && node.value.type === 'JSXExpressionContainer') { + sortNodeArgumentValue(node, node.value.expression) } + }, + SvelteAttribute: function (node) { + if (!node.key?.name || !new RegExp(classRegex).test(node.key.name)) return for (const eachValue of node.value) { sortNodeArgumentValue(node, eachValue) } }, - TextAttribute: attributeVisitor, - CallExpression: callExpressionVisitor, + TextAttribute: function (node) { + if (!node.name || !new RegExp(classRegex).test(node.name)) return + sortNodeArgumentValue(node) + }, TaggedTemplateExpression: function (node) { if (!tags.includes(node.tag.name)) { return } - sortNodeArgumentValue(node, node.quasi) }, } - const templateVisitor = { + const templateBodyVisitor = { CallExpression: callExpressionVisitor, - /* - Tagged templates inside data bindings - https://github.com/vuejs/vue/issues/9721 - */ VAttribute: function (node) { - switch (true) { - case !astUtil.isValidVueAttribute(node, classRegex): - return - case astUtil.isVLiteralValue(node): - sortNodeArgumentValue(node, null) - break - case astUtil.isArrayExpression(node): - node.value.expression.elements.forEach((arg) => { - sortNodeArgumentValue(node, arg) - }) - break - case astUtil.isObjectExpression(node): - node.value.expression.properties.forEach((prop) => { - sortNodeArgumentValue(node, prop) - }) - break + if (node.value && node.value.type === 'VLiteral') { + sortNodeArgumentValue(node) + } else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ArrayExpression') { + node.value.expression.elements.forEach((arg) => { + sortNodeArgumentValue(node, arg) + }) + } else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ObjectExpression') { + sortNodeArgumentValue(node, prop) } }, } - return parserUtil.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor) + if (context.parserServices == null || context.parserServices.defineTemplateBodyVisitor == null) { + return scriptVisitor + } else { + return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor) + } }, } diff --git a/packages/eslint-plugin/lib/rules/illegal-class-checks.js b/packages/eslint-plugin/lib/rules/illegal-class-checks.js new file mode 100644 index 000000000..60a3d6acb --- /dev/null +++ b/packages/eslint-plugin/lib/rules/illegal-class-checks.js @@ -0,0 +1,151 @@ +/* eslint-disable no-case-declarations */ +/** + * @fileoverview Check the validity of classes with your configuration + * @author Miles + */ +'use strict' + +// Modified from https://github.com/francoismassart/eslint-plugin-tailwindcss + +const astUtil = require('../util/ast') +const getOption = require('../util/settings') + +const { reportErrors } = require('@master/css-validator') + +const ILLEGAL_CLASSNAME_MSG = '{{message}}' + +module.exports = { + meta: { + docs: { + description: 'Check the validity of classes with your configuration', + category: 'Stylistic Issues', + recommended: false, + url: 'https://beta.css.master.co/docs/code-linting#check-the-validity-of-classes-with-your-configuration', + }, + messages: { + illegalClassname: ILLEGAL_CLASSNAME_MSG, + }, + fixable: null, + schema: [ + { + type: 'object', + properties: { + callees: { + type: 'array', + items: { type: 'string', minLength: 0 }, + uniqueItems: true, + }, + ignoredKeys: { + type: 'array', + items: { type: 'string', minLength: 0 }, + uniqueItems: true, + }, + config: { + // returned from `loadConfig()` utility + type: ['string', 'object'], + }, + tags: { + type: 'array', + items: { type: 'string', minLength: 0 }, + uniqueItems: true, + }, + }, + }, + ], + }, + + create: function (context) { + const callees = getOption(context, 'callees') + const tags = getOption(context, 'tags') + const masterCssConfig = getOption(context, 'config') + const classRegex = getOption(context, 'classRegex') + const ignoredKeys = getOption(context, 'ignoredKeys') + + const checkNodeArgumentValue = (node, arg = null) => { + astUtil.parseNodeRecursive( + node, + arg, + (classNames, node) => { + for (const className of classNames) { + const errors = reportErrors(className, {config: masterCssConfig}) + if (errors.length > 0) { + + for (const error of errors) { + context.report({ + node, + messageId: 'illegalClassname', + data: { + message: error.message, + } + }) + } + } + + } + }, + false, + false, + ignoredKeys + ) + } + + const callExpressionVisitor = function (node) { + const calleeStr = astUtil.calleeToString(node.callee) + if (callees.findIndex((name) => calleeStr === name) === -1) { + return + } + + node.arguments.forEach((arg) => { + checkNodeArgumentValue(node, arg) + }) + } + + const scriptVisitor = { + CallExpression: callExpressionVisitor, + JSXAttribute: function (node) { + if (!node.name || !new RegExp(classRegex).test(node.name.name)) return + if (node.value && node.value.type === 'Literal') { + checkNodeArgumentValue(node) + } else if (node.value && node.value.type === 'JSXExpressionContainer') { + checkNodeArgumentValue(node, node.value.expression) + } + }, + SvelteAttribute: function (node) { + if (!node.key?.name || !new RegExp(classRegex).test(node.key.name)) return + for (const eachValue of node.value) { + checkNodeArgumentValue(node, eachValue) + } + }, + TextAttribute: function (node) { + if (!node.name || !new RegExp(classRegex).test(node.name)) return + checkNodeArgumentValue(node) + }, + TaggedTemplateExpression: function (node) { + if (!tags.includes(node.tag.name)) { + return + } + checkNodeArgumentValue(node, node.quasi) + }, + } + const templateBodyVisitor = { + CallExpression: callExpressionVisitor, + VAttribute: function (node) { + if (node.value && node.value.type === 'VLiteral') { + checkNodeArgumentValue(node) + } else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ArrayExpression') { + node.value.expression.elements.forEach((arg) => { + checkNodeArgumentValue(node, arg) + }) + } else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ObjectExpression') { + checkNodeArgumentValue(node, prop) + } + }, + } + + if (context.parserServices == null || context.parserServices.defineTemplateBodyVisitor == null) { + return scriptVisitor + } else { + return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor) + } + }, +} diff --git a/packages/eslint-plugin/lib/util/ast.js b/packages/eslint-plugin/lib/util/ast.js index 6fa1490c3..2717e4fb0 100644 --- a/packages/eslint-plugin/lib/util/ast.js +++ b/packages/eslint-plugin/lib/util/ast.js @@ -4,232 +4,70 @@ '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') +const separatorRegEx = /([\t\n\f\r ]+)/ 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 - 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.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.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': - return true - case 'JSXExpressionContainer': - // className={"..."} - return node.value.expression.type === 'Literal' + if (calleeNode.type === 'Identifier') { + return calleeNode.name + } + if (calleeNode.type === 'MemberExpression') { + return `${calleeNode.object.name}.${calleeNode.property.name}` } - } - 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] - } - switch (node.value.type) { - case 'JSXExpressionContainer': - return node.value.expression.range - default: - return node.value.range - } + if (node.type === 'TextAttribute' && node.name === 'class') { + return [node.valueSpan.fullStart.offset, node.valueSpan.end.offset] + } + 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 - } - 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 - } + if (node.type === 'TextAttribute' && node.name === 'class') { + 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 } + 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 * @@ -242,129 +80,122 @@ function extractClassnamesFromValue(classStr) { * @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' - - if (prop.type === 'SpreadElement') { - // Ignore spread elements - return - } - - if (prop.key.type === 'Identifier' && ignoredKeys.includes(prop.key.name)) { - // Ignore specific keys defined in settings + // TODO allow vue non litteral + let originalClassNamesValue + let classNames + if (childNode === null) { + originalClassNamesValue = extractValueFromNode(rootNode); + ({ classNames } = extractClassnamesFromValue(originalClassNamesValue)) + classNames = [... new Set(classNames)] + if (classNames.length === 0) { + // Don't run for empty className return - } - - parseNodeRecursive( - rootNode, - isUsedByClassNamesPlugin ? prop.key : prop.value, - cb, - skipConditional, - forceIsolation, - ignoredKeys - ) - }) - return - case 'Property': - parseNodeRecursive(rootNode, childNode.key, cb, skipConditional, forceIsolation, ignoredKeys) + } + cb(classNames, rootNode) + } else if (childNode === undefined) { + // Ignore invalid child candidates (probably inside complex TemplateLiteral) 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 + } 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' + + 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) } - 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() + 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() + 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) + 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, + calleeToString, + extractRangeFromNode, + extractValueFromNode, + extractClassnamesFromValue, + getTemplateElementPrefix, + getTemplateElementSuffix, + getTemplateElementBody, + parseNodeRecursive } diff --git a/packages/eslint-plugin/lib/util/parser.js b/packages/eslint-plugin/lib/util/parser.js deleted file mode 100644 index 95e401255..000000000 --- a/packages/eslint-plugin/lib/util/parser.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @see parserServices https://eslint.org/docs/developer-guide/working-with-rules#the-context-object - * @param {Object} context - * @param {Function} templateBodyVisitor - * @param {Function} scriptVisitor - * @returns - */ -function defineTemplateBodyVisitor(context, templateBodyVisitor, scriptVisitor) { - if (context.parserServices == null || context.parserServices.defineTemplateBodyVisitor == null) { - // Default parser - return scriptVisitor - } - - // Using "vue-eslint-parser" requires this setup - // @see https://eslint.org/docs/developer-guide/working-with-rules#the-context-object - return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor) -} - -module.exports = { - defineTemplateBodyVisitor, -} diff --git a/packages/eslint-plugin/lib/util/regex.js b/packages/eslint-plugin/lib/util/regex.js deleted file mode 100644 index b0b1a8110..000000000 --- a/packages/eslint-plugin/lib/util/regex.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Escapes a string to be used in a regular expression - * Copied from https://stackoverflow.com/a/3561711. - * @param {string} input - * @returns {string} - */ -function escapeRegex(input) { - return input.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') -} - -const separatorRegEx = /([\t\n\f\r ]+)/ - -module.exports = { - escapeRegex, - separatorRegEx, -} diff --git a/packages/eslint-plugin/lib/util/removeDuplicatesFromArray.js b/packages/eslint-plugin/lib/util/removeDuplicatesFromArray.js deleted file mode 100644 index 09b58765d..000000000 --- a/packages/eslint-plugin/lib/util/removeDuplicatesFromArray.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -function removeDuplicatesFromArray(arr) { - return [...new Set(arr)] -} - -module.exports = removeDuplicatesFromArray diff --git a/packages/eslint-plugin/lib/util/removeDuplicatesFromClassnamesAndWhitespaces.js b/packages/eslint-plugin/lib/util/removeDuplicatesFromClassnamesAndWhitespaces.js deleted file mode 100644 index ae3ce3858..000000000 --- a/packages/eslint-plugin/lib/util/removeDuplicatesFromClassnamesAndWhitespaces.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -function removeDuplicatesFromClassnamesAndWhitespaces(orderedClassNames, whitespaces, headSpace, tailSpace) { - let previous = orderedClassNames[0] - const offset = (!headSpace && !tailSpace) || tailSpace ? -1 : 0 - for (let i = 1; i < orderedClassNames.length; i++) { - const cls = orderedClassNames[i] - // This function assumes that the list of classNames is ordered, so just comparing to the previous className is enough - if (cls === previous) { - orderedClassNames.splice(i, 1) - whitespaces.splice(i + offset, 1) - i-- - } - previous = cls - } -} - -module.exports = removeDuplicatesFromClassnamesAndWhitespaces diff --git a/packages/eslint-plugin/lib/util/settings.js b/packages/eslint-plugin/lib/util/settings.js index 026fde8e6..1aad167ef 100644 --- a/packages/eslint-plugin/lib/util/settings.js +++ b/packages/eslint-plugin/lib/util/settings.js @@ -21,18 +21,8 @@ function getOption(context, name) { return '^class(Name)?$' case 'config': return exploreConfig('master.css.*', {}) - case 'cssFiles': - return ['**/*.css', '!**/node_modules', '!**/.*', '!**/dist', '!**/build'] - case 'cssFilesRefreshRate': - return 5_000 - case 'removeDuplicates': - return true - case 'skipClassAttribute': - return false case 'tags': return [] - case 'whitelist': - return [] } } diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 82f90e834..568df9330 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@master/css": "workspace:^", + "@master/css-validator": "workspace:^", "explore-config": "^2.2.10", "requireindex": "^1.2.0" }, diff --git a/packages/eslint-plugin/tests/illegal-class-checks/core.js b/packages/eslint-plugin/tests/illegal-class-checks/core.js new file mode 100644 index 000000000..afa9b6279 --- /dev/null +++ b/packages/eslint-plugin/tests/illegal-class-checks/core.js @@ -0,0 +1,39 @@ +/** + * @fileoverview Use a consistent orders for the Master CSS classnames, based on property then on variants + * @author Miles + */ +'use strict' + +// Modified from https://github.com/francoismassart/eslint-plugin-tailwindcss + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +var rule = require('../../lib/rules/illegal-class-checks') +var RuleTester = require('eslint').RuleTester +var parserOptions = { + ecmaVersion: 2019, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +} + +var ruleTester = new RuleTester({ parserOptions }) +ruleTester.run('illegal', rule, { + valid: [ + { + code: `