Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add(ESLint): Illegal class checks #285

Merged
merged 3 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 32 additions & 76 deletions packages/eslint-plugin/lib/rules/class-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 },
Expand All @@ -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
Expand Down Expand Up @@ -145,7 +118,7 @@ module.exports = {
}
start = arg.range[0]
end = arg.range[1]
break;
break
case 'TemplateElement':
originalClassNamesValue = arg.value.raw
if (originalClassNamesValue === '') {
Expand Down Expand Up @@ -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 = ''
Expand All @@ -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) {
Expand All @@ -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)
}
},
}
151 changes: 151 additions & 0 deletions packages/eslint-plugin/lib/rules/illegal-class-checks.js
Original file line number Diff line number Diff line change
@@ -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)
}
},
}
Loading
Loading