@master/eslint-plugin-css 2.0.0-beta.185 → 2.0.0-beta.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +0 -14
- package/lib/rules/class-order.js +41 -121
- package/lib/rules/class-validation.js +123 -0
- package/lib/utils/ast.js +218 -0
- package/lib/utils/resolve-context.js +21 -0
- package/package.json +1 -1
- package/tests/frameworks/react.js +55 -0
- package/tests/{class-order → frameworks}/svelte.js +10 -11
- package/tests/{class-order → frameworks}/vue.js +10 -11
- package/tests/order.js +66 -0
- package/tests/validation.js +69 -0
- package/lib/util/ast.js +0 -370
- package/lib/util/parser.js +0 -21
- package/lib/util/regex.js +0 -16
- package/lib/util/removeDuplicatesFromArray.js +0 -7
- package/lib/util/removeDuplicatesFromClassnamesAndWhitespaces.js +0 -18
- package/lib/util/settings.js +0 -39
- package/tests/class-order/core.js +0 -112
package/lib/index.js
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Rules enforcing best practices while using Master CSS
|
|
3
|
-
* @author Miles
|
|
4
|
-
*/
|
|
5
1
|
'use strict'
|
|
6
2
|
|
|
7
|
-
//------------------------------------------------------------------------------
|
|
8
|
-
// Requirements
|
|
9
|
-
//------------------------------------------------------------------------------
|
|
10
|
-
|
|
11
3
|
const requireIndex = require('requireindex')
|
|
12
4
|
|
|
13
|
-
//------------------------------------------------------------------------------
|
|
14
|
-
// Plugin Definition
|
|
15
|
-
//------------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// import all rules in lib/rules
|
|
19
5
|
module.exports.rules = requireIndex(__dirname + '/rules')
|
package/lib/rules/class-order.js
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
/* eslint-disable no-case-declarations */
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview Use a consistent orders for the Master CSS classnames, based on property then on variants
|
|
4
|
-
* @author Miles
|
|
5
|
-
*/
|
|
6
1
|
'use strict'
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const astUtil = require('../util/ast')
|
|
11
|
-
const removeDuplicatesFromClassnamesAndWhitespaces = require('../util/removeDuplicatesFromClassnamesAndWhitespaces')
|
|
12
|
-
const getOption = require('../util/settings')
|
|
13
|
-
const parserUtil = require('../util/parser')
|
|
14
|
-
|
|
3
|
+
const astUtil = require('../utils/ast')
|
|
4
|
+
const resolveContext = require('../utils/resolve-context')
|
|
15
5
|
const { reorderForReadableClasses } = require('@master/css')
|
|
16
6
|
|
|
17
|
-
//------------------------------------------------------------------------------
|
|
18
|
-
// Rule Definition
|
|
19
|
-
//------------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
// Predefine message for use in context.report conditional.
|
|
22
|
-
// messageId will still be usable in tests.
|
|
23
|
-
const INVALID_CLASSNAMES_ORDER_MSG = 'No consistent class order followed.'
|
|
24
|
-
|
|
25
7
|
module.exports = {
|
|
26
8
|
meta: {
|
|
27
9
|
docs: {
|
|
@@ -31,57 +13,12 @@ module.exports = {
|
|
|
31
13
|
url: 'https://beta.css.master.co/docs/code-linting#enforce-a-consistent-and-logical-order-of-classes',
|
|
32
14
|
},
|
|
33
15
|
messages: {
|
|
34
|
-
|
|
16
|
+
invalidClassOrder: 'No consistent class order followed.',
|
|
35
17
|
},
|
|
36
|
-
fixable: 'code'
|
|
37
|
-
schema: [
|
|
38
|
-
{
|
|
39
|
-
type: 'object',
|
|
40
|
-
properties: {
|
|
41
|
-
callees: {
|
|
42
|
-
type: 'array',
|
|
43
|
-
items: { type: 'string', minLength: 0 },
|
|
44
|
-
uniqueItems: true,
|
|
45
|
-
},
|
|
46
|
-
ignoredKeys: {
|
|
47
|
-
type: 'array',
|
|
48
|
-
items: { type: 'string', minLength: 0 },
|
|
49
|
-
uniqueItems: true,
|
|
50
|
-
},
|
|
51
|
-
config: {
|
|
52
|
-
// returned from `loadConfig()` utility
|
|
53
|
-
type: ['string', 'object'],
|
|
54
|
-
},
|
|
55
|
-
removeDuplicates: {
|
|
56
|
-
// default: true,
|
|
57
|
-
type: 'boolean',
|
|
58
|
-
},
|
|
59
|
-
tags: {
|
|
60
|
-
type: 'array',
|
|
61
|
-
items: { type: 'string', minLength: 0 },
|
|
62
|
-
uniqueItems: true,
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
],
|
|
18
|
+
fixable: 'code'
|
|
67
19
|
},
|
|
68
|
-
|
|
69
20
|
create: function (context) {
|
|
70
|
-
const
|
|
71
|
-
const skipClassAttribute = getOption(context, 'skipClassAttribute')
|
|
72
|
-
const tags = getOption(context, 'tags')
|
|
73
|
-
const masterCssConfig = getOption(context, 'config')
|
|
74
|
-
const classRegex = getOption(context, 'classRegex')
|
|
75
|
-
|
|
76
|
-
//----------------------------------------------------------------------
|
|
77
|
-
// Helpers
|
|
78
|
-
//----------------------------------------------------------------------
|
|
79
|
-
/**
|
|
80
|
-
* Recursive function crawling into child nodes
|
|
81
|
-
* @param {ASTNode} node The root node of the current parsing
|
|
82
|
-
* @param {ASTNode} arg The child node of node
|
|
83
|
-
* @returns {void}
|
|
84
|
-
*/
|
|
21
|
+
const { options, settings, config } = resolveContext(context)
|
|
85
22
|
const sortNodeArgumentValue = (node, arg = null) => {
|
|
86
23
|
let originalClassNamesValue = null
|
|
87
24
|
let start = null
|
|
@@ -145,7 +82,7 @@ module.exports = {
|
|
|
145
82
|
}
|
|
146
83
|
start = arg.range[0]
|
|
147
84
|
end = arg.range[1]
|
|
148
|
-
break
|
|
85
|
+
break
|
|
149
86
|
case 'TemplateElement':
|
|
150
87
|
originalClassNamesValue = arg.value.raw
|
|
151
88
|
if (originalClassNamesValue === '') {
|
|
@@ -173,12 +110,12 @@ module.exports = {
|
|
|
173
110
|
return
|
|
174
111
|
}
|
|
175
112
|
|
|
176
|
-
let orderedClassNames = reorderForReadableClasses(classNames,
|
|
113
|
+
let orderedClassNames = reorderForReadableClasses(classNames, config)
|
|
177
114
|
.filter(eachOrderedClassName => classNames.includes(eachOrderedClassName))
|
|
178
115
|
|
|
179
|
-
orderedClassNames =
|
|
180
|
-
|
|
181
|
-
|
|
116
|
+
orderedClassNames = classNames.filter(x => !orderedClassNames.includes(x))
|
|
117
|
+
.concat(orderedClassNames)
|
|
118
|
+
.filter(x => x.trim() !== '')
|
|
182
119
|
|
|
183
120
|
// Generates the validated/sorted attribute value
|
|
184
121
|
let validatedClassNamesValue = ''
|
|
@@ -195,7 +132,7 @@ module.exports = {
|
|
|
195
132
|
validatedClassNamesValue = prefix + validatedClassNamesValue + suffix
|
|
196
133
|
context.report({
|
|
197
134
|
node: node,
|
|
198
|
-
messageId: '
|
|
135
|
+
messageId: 'invalidClassOrder',
|
|
199
136
|
fix: function (fixer) {
|
|
200
137
|
return fixer.replaceTextRange([start, end], validatedClassNamesValue)
|
|
201
138
|
},
|
|
@@ -203,24 +140,9 @@ module.exports = {
|
|
|
203
140
|
}
|
|
204
141
|
}
|
|
205
142
|
|
|
206
|
-
//----------------------------------------------------------------------
|
|
207
|
-
// Public
|
|
208
|
-
//----------------------------------------------------------------------
|
|
209
|
-
|
|
210
|
-
const attributeVisitor = function (node) {
|
|
211
|
-
if (!astUtil.isClassAttribute(node, classRegex) || skipClassAttribute) {
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
if (astUtil.isLiteralAttributeValue(node)) {
|
|
215
|
-
sortNodeArgumentValue(node)
|
|
216
|
-
} else if (node.value && node.value.type === 'JSXExpressionContainer') {
|
|
217
|
-
sortNodeArgumentValue(node, node.value.expression)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
143
|
const callExpressionVisitor = function (node) {
|
|
222
144
|
const calleeStr = astUtil.calleeToString(node.callee)
|
|
223
|
-
if (callees.findIndex((name) => calleeStr === name) === -1) {
|
|
145
|
+
if (settings.callees.findIndex((name) => calleeStr === name) === -1) {
|
|
224
146
|
return
|
|
225
147
|
}
|
|
226
148
|
|
|
@@ -230,53 +152,51 @@ module.exports = {
|
|
|
230
152
|
}
|
|
231
153
|
|
|
232
154
|
const scriptVisitor = {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!node.
|
|
236
|
-
if (
|
|
237
|
-
|
|
155
|
+
CallExpression: callExpressionVisitor,
|
|
156
|
+
JSXAttribute: function (node) {
|
|
157
|
+
if (!node.name || !new RegExp(settings.classMatching).test(node.name.name)) return
|
|
158
|
+
if (node.value && node.value.type === 'Literal') {
|
|
159
|
+
sortNodeArgumentValue(node)
|
|
160
|
+
} else if (node.value && node.value.type === 'JSXExpressionContainer') {
|
|
161
|
+
sortNodeArgumentValue(node, node.value.expression)
|
|
238
162
|
}
|
|
163
|
+
},
|
|
164
|
+
SvelteAttribute: function (node) {
|
|
165
|
+
if (!node.key?.name || !new RegExp(settings.classMatching).test(node.key.name)) return
|
|
239
166
|
for (const eachValue of node.value) {
|
|
240
167
|
sortNodeArgumentValue(node, eachValue)
|
|
241
168
|
}
|
|
242
169
|
},
|
|
243
|
-
TextAttribute:
|
|
244
|
-
|
|
170
|
+
TextAttribute: function (node) {
|
|
171
|
+
if (!node.name || !new RegExp(settings.classMatching).test(node.name)) return
|
|
172
|
+
sortNodeArgumentValue(node)
|
|
173
|
+
},
|
|
245
174
|
TaggedTemplateExpression: function (node) {
|
|
246
|
-
if (!tags.includes(node.tag.name)) {
|
|
175
|
+
if (!settings.tags.includes(node.tag.name)) {
|
|
247
176
|
return
|
|
248
177
|
}
|
|
249
|
-
|
|
250
178
|
sortNodeArgumentValue(node, node.quasi)
|
|
251
179
|
},
|
|
252
180
|
}
|
|
253
|
-
const
|
|
181
|
+
const templateBodyVisitor = {
|
|
254
182
|
CallExpression: callExpressionVisitor,
|
|
255
|
-
/*
|
|
256
|
-
Tagged templates inside data bindings
|
|
257
|
-
https://github.com/vuejs/vue/issues/9721
|
|
258
|
-
*/
|
|
259
183
|
VAttribute: function (node) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
sortNodeArgumentValue(node,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
sortNodeArgumentValue(node, arg)
|
|
269
|
-
})
|
|
270
|
-
break
|
|
271
|
-
case astUtil.isObjectExpression(node):
|
|
272
|
-
node.value.expression.properties.forEach((prop) => {
|
|
273
|
-
sortNodeArgumentValue(node, prop)
|
|
274
|
-
})
|
|
275
|
-
break
|
|
184
|
+
if (node.value && node.value.type === 'VLiteral') {
|
|
185
|
+
sortNodeArgumentValue(node)
|
|
186
|
+
} else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ArrayExpression') {
|
|
187
|
+
node.value.expression.elements.forEach((arg) => {
|
|
188
|
+
sortNodeArgumentValue(node, arg)
|
|
189
|
+
})
|
|
190
|
+
} else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ObjectExpression') {
|
|
191
|
+
sortNodeArgumentValue(node, prop)
|
|
276
192
|
}
|
|
277
193
|
},
|
|
278
194
|
}
|
|
279
195
|
|
|
280
|
-
|
|
196
|
+
if (context.parserServices == null || context.parserServices.defineTemplateBodyVisitor == null) {
|
|
197
|
+
return scriptVisitor
|
|
198
|
+
} else {
|
|
199
|
+
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
|
|
200
|
+
}
|
|
281
201
|
},
|
|
282
202
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const astUtil = require('../utils/ast')
|
|
4
|
+
const resolveContext = require('../utils/resolve-context')
|
|
5
|
+
const { validate } = require('@master/css-validator')
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Check the validity of classes with your configuration',
|
|
11
|
+
category: 'Stylistic Issues',
|
|
12
|
+
recommended: false,
|
|
13
|
+
url: 'https://beta.css.master.co/docs/code-linting#check-the-validity-of-classes-with-your-configuration',
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
invalidClass: '{{message}}',
|
|
17
|
+
disallowTraditionalClass: '{{message}}',
|
|
18
|
+
},
|
|
19
|
+
fixable: null
|
|
20
|
+
},
|
|
21
|
+
create: function (context) {
|
|
22
|
+
const { options, settings, config } = resolveContext(context)
|
|
23
|
+
const checkNodeArgumentValue = (node, arg = null) => {
|
|
24
|
+
astUtil.parseNodeRecursive(
|
|
25
|
+
node,
|
|
26
|
+
arg,
|
|
27
|
+
(classNames, node) => {
|
|
28
|
+
const sourceCode = context.getSourceCode()
|
|
29
|
+
const sourceCodeLines = sourceCode.lines
|
|
30
|
+
const nodeStartLine = node.loc.start.line
|
|
31
|
+
const nodeEndLine = node.loc.end.line
|
|
32
|
+
for (const className of classNames) {
|
|
33
|
+
const { isMasterCSS, errors } = validate(className, { config })
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
for (const error of errors) {
|
|
36
|
+
if (isMasterCSS) {
|
|
37
|
+
context.report({
|
|
38
|
+
loc: astUtil.findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
|
|
39
|
+
messageId: 'invalidClass',
|
|
40
|
+
data: {
|
|
41
|
+
message: error.message + '.',
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
} else if (!isMasterCSS && options.disallowTraditionalClass) {
|
|
45
|
+
context.report({
|
|
46
|
+
loc: astUtil.findLoc(className, sourceCodeLines, nodeStartLine, nodeEndLine),
|
|
47
|
+
messageId: 'disallowTraditionalClass',
|
|
48
|
+
data: {
|
|
49
|
+
message: `Disallow a traditional class \`${className}\`.`,
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
false,
|
|
58
|
+
false,
|
|
59
|
+
settings.ignoredKeys
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const callExpressionVisitor = function (node) {
|
|
64
|
+
const calleeStr = astUtil.calleeToString(node.callee)
|
|
65
|
+
if (settings.callees.findIndex((name) => calleeStr === name) === -1) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
node.arguments.forEach((arg) => {
|
|
70
|
+
checkNodeArgumentValue(node, arg)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const scriptVisitor = {
|
|
75
|
+
CallExpression: callExpressionVisitor,
|
|
76
|
+
JSXAttribute: function (node) {
|
|
77
|
+
if (!node.name || !new RegExp(settings.classMatching).test(node.name.name)) return
|
|
78
|
+
if (node.value && node.value.type === 'Literal') {
|
|
79
|
+
checkNodeArgumentValue(node)
|
|
80
|
+
} else if (node.value && node.value.type === 'JSXExpressionContainer') {
|
|
81
|
+
checkNodeArgumentValue(node, node.value.expression)
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
SvelteAttribute: function (node) {
|
|
85
|
+
if (!node.key?.name || !new RegExp(settings.classMatching).test(node.key.name)) return
|
|
86
|
+
for (const eachValue of node.value) {
|
|
87
|
+
checkNodeArgumentValue(node, eachValue)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
TextAttribute: function (node) {
|
|
91
|
+
if (!node.name || !new RegExp(settings.classMatching).test(node.name)) return
|
|
92
|
+
checkNodeArgumentValue(node)
|
|
93
|
+
},
|
|
94
|
+
TaggedTemplateExpression: function (node) {
|
|
95
|
+
if (!settings.tags.includes(node.tag.name)) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
checkNodeArgumentValue(node, node.quasi)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const templateBodyVisitor = {
|
|
103
|
+
CallExpression: callExpressionVisitor,
|
|
104
|
+
VAttribute: function (node) {
|
|
105
|
+
if (node.value && node.value.type === 'VLiteral') {
|
|
106
|
+
checkNodeArgumentValue(node)
|
|
107
|
+
} else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ArrayExpression') {
|
|
108
|
+
node.value.expression.elements.forEach((arg) => {
|
|
109
|
+
checkNodeArgumentValue(node, arg)
|
|
110
|
+
})
|
|
111
|
+
} else if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression.type === 'ObjectExpression') {
|
|
112
|
+
checkNodeArgumentValue(node, prop)
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (context.parserServices == null || context.parserServices.defineTemplateBodyVisitor == null) {
|
|
118
|
+
return scriptVisitor
|
|
119
|
+
} else {
|
|
120
|
+
return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
}
|
package/lib/utils/ast.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const separatorRegEx = /([\t\n\f\r ]+)/
|
|
4
|
+
|
|
5
|
+
function calleeToString(calleeNode) {
|
|
6
|
+
if (calleeNode.type === 'Identifier') {
|
|
7
|
+
return calleeNode.name
|
|
8
|
+
}
|
|
9
|
+
if (calleeNode.type === 'MemberExpression') {
|
|
10
|
+
return `${calleeNode.object.name}.${calleeNode.property.name}`
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractRangeFromNode(node) {
|
|
15
|
+
if (node.type === 'TextAttribute' && node.name === 'class') {
|
|
16
|
+
return [node.valueSpan.fullStart.offset, node.valueSpan.end.offset]
|
|
17
|
+
}
|
|
18
|
+
switch (node.value.type) {
|
|
19
|
+
case 'JSXExpressionContainer':
|
|
20
|
+
return node.value.expression.range
|
|
21
|
+
default:
|
|
22
|
+
return node.value.range
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractValueFromNode(node) {
|
|
27
|
+
if (node.type === 'TextAttribute' && node.name === 'class') {
|
|
28
|
+
return node.value
|
|
29
|
+
}
|
|
30
|
+
switch (node.value.type) {
|
|
31
|
+
case 'JSXExpressionContainer':
|
|
32
|
+
return node.value.expression.value
|
|
33
|
+
case 'VExpressionContainer':
|
|
34
|
+
switch (node.value.expression.type) {
|
|
35
|
+
case 'ArrayExpression':
|
|
36
|
+
return node.value.expression.elements
|
|
37
|
+
case 'ObjectExpression':
|
|
38
|
+
return node.value.expression.properties
|
|
39
|
+
}
|
|
40
|
+
return node.value.expression.value
|
|
41
|
+
default:
|
|
42
|
+
return node.value.value
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractClassnamesFromValue(classStr) {
|
|
47
|
+
if (typeof classStr !== 'string') {
|
|
48
|
+
return { classNames: [], whitespaces: [], headSpace: false, tailSpace: false }
|
|
49
|
+
}
|
|
50
|
+
let parts = classStr.split(separatorRegEx)
|
|
51
|
+
if (parts[0] === '') {
|
|
52
|
+
parts.shift()
|
|
53
|
+
}
|
|
54
|
+
if (parts[parts.length - 1] === '') {
|
|
55
|
+
parts.pop()
|
|
56
|
+
}
|
|
57
|
+
let headSpace = separatorRegEx.test(parts[0])
|
|
58
|
+
let tailSpace = separatorRegEx.test(parts[parts.length - 1])
|
|
59
|
+
const isClass = (_, i) => (headSpace ? i % 2 !== 0 : i % 2 === 0)
|
|
60
|
+
const isNotClass = (_, i) => (headSpace ? i % 2 === 0 : i % 2 !== 0)
|
|
61
|
+
let classNames = parts.filter(isClass)
|
|
62
|
+
let whitespaces = parts.filter(isNotClass)
|
|
63
|
+
return { classNames: classNames, whitespaces: whitespaces, headSpace: headSpace, tailSpace: tailSpace }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Inspect and parse an abstract syntax node and run a callback function
|
|
69
|
+
*
|
|
70
|
+
* @param {ASTNode} rootNode The current root node being parsed by eslint
|
|
71
|
+
* @param {ASTNode} childNode The AST node child argument being checked
|
|
72
|
+
* @param {Function} cb The callback function
|
|
73
|
+
* @param {Boolean} skipConditional Optional, indicate distinct parsing for conditional nodes
|
|
74
|
+
* @param {Boolean} isolate Optional, set internally to isolate parsing and validation on conditional children
|
|
75
|
+
* @param {Array} ignoredKeys Optional, set object keys which should not be parsed e.g. for `cva`
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
function parseNodeRecursive(rootNode, childNode, cb, skipConditional = false, isolate = false, ignoredKeys = []) {
|
|
79
|
+
// TODO allow vue non litteral
|
|
80
|
+
let originalClassNamesValue
|
|
81
|
+
let classNames
|
|
82
|
+
if (childNode === null) {
|
|
83
|
+
originalClassNamesValue = extractValueFromNode(rootNode);
|
|
84
|
+
({ classNames } = extractClassnamesFromValue(originalClassNamesValue))
|
|
85
|
+
classNames = [... new Set(classNames)]
|
|
86
|
+
if (classNames.length === 0) {
|
|
87
|
+
// Don't run for empty className
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
cb(classNames, rootNode)
|
|
91
|
+
} else if (childNode === undefined) {
|
|
92
|
+
// Ignore invalid child candidates (probably inside complex TemplateLiteral)
|
|
93
|
+
return
|
|
94
|
+
} else {
|
|
95
|
+
const forceIsolation = skipConditional ? true : isolate
|
|
96
|
+
let trim = false
|
|
97
|
+
switch (childNode.type) {
|
|
98
|
+
case 'TemplateLiteral':
|
|
99
|
+
childNode.expressions.forEach((exp) => {
|
|
100
|
+
parseNodeRecursive(rootNode, exp, cb, skipConditional, forceIsolation, ignoredKeys)
|
|
101
|
+
})
|
|
102
|
+
childNode.quasis.forEach((quasis) => {
|
|
103
|
+
parseNodeRecursive(rootNode, quasis, cb, skipConditional, isolate, ignoredKeys)
|
|
104
|
+
})
|
|
105
|
+
return
|
|
106
|
+
case 'ConditionalExpression':
|
|
107
|
+
parseNodeRecursive(rootNode, childNode.consequent, cb, skipConditional, forceIsolation, ignoredKeys)
|
|
108
|
+
parseNodeRecursive(rootNode, childNode.alternate, cb, skipConditional, forceIsolation, ignoredKeys)
|
|
109
|
+
return
|
|
110
|
+
case 'LogicalExpression':
|
|
111
|
+
parseNodeRecursive(rootNode, childNode.right, cb, skipConditional, forceIsolation, ignoredKeys)
|
|
112
|
+
return
|
|
113
|
+
case 'ArrayExpression':
|
|
114
|
+
childNode.elements.forEach((el) => {
|
|
115
|
+
parseNodeRecursive(rootNode, el, cb, skipConditional, forceIsolation, ignoredKeys)
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
case 'ObjectExpression':
|
|
119
|
+
childNode.properties.forEach((prop) => {
|
|
120
|
+
const isUsedByClassNamesPlugin = rootNode.callee && rootNode.callee.name === 'classnames'
|
|
121
|
+
|
|
122
|
+
if (prop.type === 'SpreadElement') {
|
|
123
|
+
// Ignore spread elements
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (prop.key.type === 'Identifier' && ignoredKeys.includes(prop.key.name)) {
|
|
128
|
+
// Ignore specific keys defined in settings
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
parseNodeRecursive(
|
|
133
|
+
rootNode,
|
|
134
|
+
isUsedByClassNamesPlugin ? prop.key : prop.value,
|
|
135
|
+
cb,
|
|
136
|
+
skipConditional,
|
|
137
|
+
forceIsolation,
|
|
138
|
+
ignoredKeys
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
return
|
|
142
|
+
case 'Property':
|
|
143
|
+
parseNodeRecursive(rootNode, childNode.key, cb, skipConditional, forceIsolation, ignoredKeys)
|
|
144
|
+
return
|
|
145
|
+
case 'Literal':
|
|
146
|
+
trim = true
|
|
147
|
+
originalClassNamesValue = childNode.value
|
|
148
|
+
break
|
|
149
|
+
case 'TemplateElement':
|
|
150
|
+
originalClassNamesValue = childNode.value.raw
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
({ classNames } = extractClassnamesFromValue(originalClassNamesValue))
|
|
154
|
+
classNames = removeDuplicatesFromArray(classNames)
|
|
155
|
+
if (classNames.length === 0) {
|
|
156
|
+
// Don't run for empty className
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const targetNode = isolate ? null : rootNode
|
|
160
|
+
cb(classNames, targetNode)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getTemplateElementPrefix(text, raw) {
|
|
165
|
+
const idx = text.indexOf(raw)
|
|
166
|
+
if (idx === 0) {
|
|
167
|
+
return ''
|
|
168
|
+
}
|
|
169
|
+
return text.split(raw).shift()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getTemplateElementSuffix(text, raw) {
|
|
173
|
+
if (text.indexOf(raw) === -1) {
|
|
174
|
+
return ''
|
|
175
|
+
}
|
|
176
|
+
return text.split(raw).pop()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getTemplateElementBody(text, prefix, suffix) {
|
|
180
|
+
let arr = text.split(prefix)
|
|
181
|
+
arr.shift()
|
|
182
|
+
let body = arr.join(prefix)
|
|
183
|
+
arr = body.split(suffix)
|
|
184
|
+
arr.pop()
|
|
185
|
+
return arr.join(suffix)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function findLoc(text, lines, startLine, endLine) {
|
|
189
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
190
|
+
const sourceCodeLine = lines[i - 1]
|
|
191
|
+
const index = sourceCodeLine.indexOf(text)
|
|
192
|
+
if (index !== -1) {
|
|
193
|
+
return {
|
|
194
|
+
start: {
|
|
195
|
+
line: i,
|
|
196
|
+
column: index
|
|
197
|
+
},
|
|
198
|
+
end: {
|
|
199
|
+
line: i,
|
|
200
|
+
column: index + text.length
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
calleeToString,
|
|
210
|
+
extractRangeFromNode,
|
|
211
|
+
extractValueFromNode,
|
|
212
|
+
extractClassnamesFromValue,
|
|
213
|
+
getTemplateElementPrefix,
|
|
214
|
+
getTemplateElementSuffix,
|
|
215
|
+
getTemplateElementBody,
|
|
216
|
+
parseNodeRecursive,
|
|
217
|
+
findLoc
|
|
218
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const exploreConfig = require('explore-config').default
|
|
4
|
+
|
|
5
|
+
const resolveContext = function (context) {
|
|
6
|
+
const settings = Object.assign({
|
|
7
|
+
callees: ['classnames', 'clsx', 'ctl', 'cva', 'cv', 'classVariant', 'styled'],
|
|
8
|
+
ignoredKeys: ['compoundVariants', 'defaultVariants'],
|
|
9
|
+
classMatching: '^class(Name)?$',
|
|
10
|
+
config: 'master.css.*',
|
|
11
|
+
tags: []
|
|
12
|
+
}, context.settings?.['@master/css'])
|
|
13
|
+
const config = settings?.config
|
|
14
|
+
return {
|
|
15
|
+
settings,
|
|
16
|
+
options: context.options[0] || {},
|
|
17
|
+
config: typeof config === 'object' ? config : exploreConfig(settings?.config || '')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = resolveContext
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@master/eslint-plugin-css","scripts":{"test":"mocha tests --recursive --timeout 10000","test:file":"mocha tests"},"keywords":["mastercss","eslint","eslintplugin"],"license":"MIT","description":"Master CSS ESLint Plugin","author":"Aoyue Design LLC.","funding":"https://css.master.co/docs/donate","homepage":"https://css.master.co","bugs":{"url":"https://github.com/master-co/css/issues"},"repository":{"type":"git","url":"https://github.com/master-co/css.git","directory":"packages/eslint-plugin"},"sideEffects":false,"main":"./lib/index.js","exports":"./lib/index.js","publishConfig":{"access":"public"},"dependencies":{"@master/css":"^2.0.0-beta.
|
|
1
|
+
{"name":"@master/eslint-plugin-css","scripts":{"test":"mocha tests --recursive --timeout 10000","test:file":"mocha tests"},"keywords":["mastercss","eslint","eslintplugin"],"license":"MIT","description":"Master CSS ESLint Plugin","author":"Aoyue Design LLC.","funding":"https://css.master.co/docs/donate","homepage":"https://css.master.co","bugs":{"url":"https://github.com/master-co/css/issues"},"repository":{"type":"git","url":"https://github.com/master-co/css.git","directory":"packages/eslint-plugin"},"sideEffects":false,"main":"./lib/index.js","exports":"./lib/index.js","publishConfig":{"access":"public"},"dependencies":{"@master/css":"^2.0.0-beta.186","@master/css-validator":"^2.0.0-beta.186","explore-config":"^2.2.10","requireindex":"^1.2.0"},"devDependencies":{"@angular-eslint/template-parser":"^16.2.0","@types/node":"^20.6.0","@typescript-eslint/parser":"^5.50.0","eslint":"^7.1.0","mocha":"^10.2.0","svelte-eslint-parser":"^0.33.1","vue-eslint-parser":"^9.3.1"},"version":"2.0.0-beta.186"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const rule = require('../../lib/rules/class-order')
|
|
4
|
+
const RuleTester = require('eslint').RuleTester
|
|
5
|
+
const ruleTester = new RuleTester({
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
ruleTester.run('class order', rule, {
|
|
14
|
+
valid: [
|
|
15
|
+
{ code: '<h1 className={"bg:black fg:white f:24 m:8 p:8"}>Welcome {name}</h1>' }
|
|
16
|
+
],
|
|
17
|
+
invalid: [
|
|
18
|
+
{
|
|
19
|
+
code: `
|
|
20
|
+
export interface FakePropsInterface {
|
|
21
|
+
readonly name?: string;
|
|
22
|
+
}
|
|
23
|
+
function Fake({
|
|
24
|
+
name = 'yolo'
|
|
25
|
+
}: FakeProps) {
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<h1 className={"m:8 bg:black p:8 fg:white f:24"}>Welcome {name}</h1>
|
|
29
|
+
<p>Bye {name}</p>
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
export default Fake;
|
|
34
|
+
`,
|
|
35
|
+
output: `
|
|
36
|
+
export interface FakePropsInterface {
|
|
37
|
+
readonly name?: string;
|
|
38
|
+
}
|
|
39
|
+
function Fake({
|
|
40
|
+
name = 'yolo'
|
|
41
|
+
}: FakeProps) {
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<h1 className={"bg:black fg:white f:24 m:8 p:8"}>Welcome {name}</h1>
|
|
45
|
+
<p>Bye {name}</p>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
export default Fake;
|
|
50
|
+
`,
|
|
51
|
+
parser: require.resolve('@typescript-eslint/parser'),
|
|
52
|
+
errors: [{ messageId: 'invalidClassOrder' }],
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
})
|