eslint-plugin-tailwind-variants 2.0.3 → 2.1.1

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.
Files changed (45) hide show
  1. package/README.md +6 -4
  2. package/dist/src/index.d.ts +31 -0
  3. package/dist/src/index.d.ts.map +1 -0
  4. package/dist/src/rules/index.d.ts +7 -0
  5. package/dist/src/rules/index.d.ts.map +1 -0
  6. package/dist/src/rules/limited-inline-classes.d.ts +41 -0
  7. package/dist/src/rules/limited-inline-classes.d.ts.map +1 -0
  8. package/dist/src/rules/require-variants-call-styles-name.d.ts +19 -0
  9. package/dist/src/rules/require-variants-call-styles-name.d.ts.map +1 -0
  10. package/dist/src/rules/require-variants-suffix.d.ts +19 -0
  11. package/dist/src/rules/require-variants-suffix.d.ts.map +1 -0
  12. package/dist/src/rules/sort-custom-properties.d.ts +86 -0
  13. package/dist/src/rules/sort-custom-properties.d.ts.map +1 -0
  14. package/dist/src/utils/create-rule-visitors.d.ts +2 -0
  15. package/dist/src/utils/create-rule-visitors.d.ts.map +1 -0
  16. package/dist/src/utils/get-bind-class-expression.d.ts +3 -0
  17. package/dist/src/utils/get-bind-class-expression.d.ts.map +1 -0
  18. package/package.json +43 -37
  19. package/src/index.js +61 -0
  20. package/{dist → src}/rules/index.js +5 -5
  21. package/src/rules/limited-inline-classes.js +440 -0
  22. package/src/rules/limited-inline-classes.test.js +268 -0
  23. package/src/rules/require-variants-call-styles-name.js +195 -0
  24. package/src/rules/require-variants-call-styles-name.test.js +105 -0
  25. package/src/rules/require-variants-suffix.js +146 -0
  26. package/src/rules/require-variants-suffix.test.js +81 -0
  27. package/src/rules/sort-custom-properties.js +596 -0
  28. package/src/rules/sort-custom-properties.test.js +758 -0
  29. package/src/utils/create-rule-visitors.js +28 -0
  30. package/src/utils/get-bind-class-expression.js +36 -0
  31. package/dist/index.d.ts +0 -11
  32. package/dist/index.js +0 -43
  33. package/dist/rules/index.d.ts +0 -2
  34. package/dist/rules/limited-inline-classes.d.ts +0 -23
  35. package/dist/rules/limited-inline-classes.js +0 -198
  36. package/dist/rules/require-variants-call-styles-name.d.ts +0 -17
  37. package/dist/rules/require-variants-call-styles-name.js +0 -75
  38. package/dist/rules/require-variants-suffix.d.ts +0 -17
  39. package/dist/rules/require-variants-suffix.js +0 -66
  40. package/dist/rules/sort-custom-properties.d.ts +0 -37
  41. package/dist/rules/sort-custom-properties.js +0 -210
  42. package/dist/utils/create-rule-visitors.d.ts +0 -6
  43. package/dist/utils/create-rule-visitors.js +0 -18
  44. package/dist/utils/get-bind-class-expression.d.ts +0 -6
  45. package/dist/utils/get-bind-class-expression.js +0 -20
@@ -0,0 +1,596 @@
1
+ /**
2
+ * @typedef {object} CustomProperty
3
+ * @property {import("estree").Node} node AST node of the custom property declaration.
4
+ * @property {number} orderIndex Index of the property based on matching order patterns.
5
+ * @property {string} property Name of the custom property (e.g., '--color-primary').
6
+ */
7
+
8
+ /** Selection support for custom tailwind blocks. */
9
+ const BLOCK_SELECTOR =
10
+ "Rule > Block, AtRule[name='theme'] > Block, AtRule[name='utility'] > Block";
11
+
12
+ const DEFAULT_ORDER = [
13
+ "^--spacing-",
14
+ "^--size-",
15
+ "^--font-",
16
+ "^--weight-",
17
+ "^--leading-",
18
+ "^--tracking-",
19
+ "^--radius-",
20
+ "^--shadow-",
21
+ "^--animate-",
22
+ "^--transition-",
23
+ "^--color-",
24
+ ];
25
+
26
+ export const MESSAGE_IDS = {
27
+ invalidPattern: "invalidPattern",
28
+ missingEmptyLineBetweenGroups: "missingEmptyLineBetweenGroups",
29
+ patternTooLong: "patternTooLong",
30
+ unsortedCustomProperties: "unsortedCustomProperties",
31
+ };
32
+
33
+ /** @typedef {typeof MESSAGE_IDS[keyof typeof MESSAGE_IDS]} MessageIds */
34
+
35
+ /**
36
+ * @typedef {object} RuleOptions
37
+ * @property {boolean} [emptyLineBetweenGroups=false] Add empty line between different prefix groups.
38
+ * @property {string[]} [order=DEFAULT_ORDER] Order of patterns (RegExp strings) for custom properties.
39
+ */
40
+
41
+ /** @type {import("eslint").Rule.RuleModule} */
42
+ export const rule = {
43
+ create: (context) => {
44
+ const [options = {}] = /** @type {[RuleOptions]} */ (context.options);
45
+ const order = options.order ?? DEFAULT_ORDER;
46
+ const emptyLineBetweenGroups = options.emptyLineBetweenGroups ?? false;
47
+ const { sourceCode } = context;
48
+
49
+ const compiledOrder = compileOrderPatterns(order, context);
50
+ const getMatchingOrderIndex = createOrderIndexGetter(compiledOrder);
51
+
52
+ /** @type {CustomProperty[][]} */
53
+ const blockStack = [];
54
+
55
+ return {
56
+ [`${BLOCK_SELECTOR}:exit`]: handleBlockExit({
57
+ blockStack,
58
+ context,
59
+ emptyLineBetweenGroups,
60
+ order,
61
+ sourceCode,
62
+ }),
63
+ [`:matches(${BLOCK_SELECTOR}) > Declaration`]: collectDeclaration(
64
+ blockStack,
65
+ getMatchingOrderIndex,
66
+ ),
67
+ [BLOCK_SELECTOR]() {
68
+ blockStack.push([]);
69
+ },
70
+ };
71
+ },
72
+
73
+ meta: {
74
+ defaultOptions: [
75
+ {
76
+ emptyLineBetweenGroups: false,
77
+ order: DEFAULT_ORDER,
78
+ },
79
+ ],
80
+ docs: {
81
+ description:
82
+ "Enforce sorting of CSS custom properties based on RegEx patterns within declaration blocks.",
83
+ },
84
+ fixable: "code",
85
+ messages: {
86
+ invalidPattern:
87
+ "The pattern '{{pattern}}' is not a valid regular expression",
88
+ missingEmptyLineBetweenGroups:
89
+ "Expected empty line between different custom property prefix groups",
90
+ patternTooLong:
91
+ "The pattern '{{pattern}}' is too long and may cause performance issues",
92
+ unsortedCustomProperties:
93
+ "Custom properties should be sorted by the defined order: {{order}}",
94
+ },
95
+ schema: [
96
+ {
97
+ additionalProperties: false,
98
+ properties: {
99
+ emptyLineBetweenGroups: {
100
+ default: false,
101
+ description: "Add empty line between different prefix groups",
102
+ type: "boolean",
103
+ },
104
+ order: {
105
+ default: DEFAULT_ORDER,
106
+ description: "Array of RegEx patterns defining the sort order",
107
+ items: {
108
+ type: "string",
109
+ },
110
+ type: "array",
111
+ },
112
+ },
113
+ type: "object",
114
+ },
115
+ ],
116
+ type: "layout",
117
+ },
118
+ };
119
+
120
+ /**
121
+ * @typedef {object} FixerConfig
122
+ * @property {CustomProperty[]} currentBlockProperties Properties in the current block being processed.
123
+ * @property {CustomProperty[]} sorted Properties sorted according to the defined order.
124
+ * @property {boolean} emptyLineBetweenGroups Whether empty lines are required between groups.
125
+ * @property {import("eslint").SourceCode} sourceCode ESLint SourceCode instance for text extraction.
126
+ */
127
+
128
+ /**
129
+ * @typedef {object} BlockHandlerConfig
130
+ * @property {CustomProperty[][]} blockStack Stack of property blocks being processed.
131
+ * @property {boolean} emptyLineBetweenGroups Whether empty lines are required between groups.
132
+ * @property {string[]} order Array of RegEx patterns defining the sort order.
133
+ * @property {import("eslint").SourceCode} sourceCode ESLint SourceCode instance for text extraction.
134
+ * @property {import("eslint").Rule.RuleContext} context ESLint rule context for reporting.
135
+ */
136
+
137
+ /**
138
+ * Get location information with offsets from a node.
139
+ * @param {import("estree").Node} node
140
+ * @returns {import("estree").SourceLocation} Location information with offsets.
141
+ * @throws {Error} If node is missing location or offset information.
142
+ */
143
+ const getNodeLoc = (node) => {
144
+ if (
145
+ node.loc === null ||
146
+ typeof node.loc === "undefined" ||
147
+ node.loc.start === null ||
148
+ node.loc.end === null ||
149
+ !("offset" in node.loc.start) ||
150
+ !("offset" in node.loc.end) ||
151
+ typeof node.loc.start.offset !== "number" ||
152
+ typeof node.loc.end.offset !== "number"
153
+ ) {
154
+ throw new Error("Node missing location or offset information");
155
+ }
156
+
157
+ return node.loc;
158
+ };
159
+
160
+ /**
161
+ * Compile regular expression pattern with validation and error reporting.
162
+ * Reports errors for patterns that are too long or invalid, returning a
163
+ * no-match fallback regex on failure.
164
+ * @param {string} pattern
165
+ * @param {import("eslint").Rule.RuleContext} context
166
+ * @returns {RegExp} Compiled regex or fallback regex that matches nothing.
167
+ */
168
+ const compilePattern = (pattern, context) => {
169
+ const MATCHES_NOTHING = /(?!)/;
170
+ const MAX_PATTERN_LENGTH = 100;
171
+ if (pattern.length > MAX_PATTERN_LENGTH) {
172
+ context.report({
173
+ data: { pattern },
174
+ loc: { column: 1, line: 1 },
175
+ messageId: MESSAGE_IDS.patternTooLong,
176
+ });
177
+
178
+ return MATCHES_NOTHING;
179
+ }
180
+
181
+ try {
182
+ return new RegExp(pattern);
183
+ } catch {
184
+ context.report({
185
+ data: { pattern },
186
+ loc: { column: 1, line: 1 },
187
+ messageId: MESSAGE_IDS.invalidPattern,
188
+ });
189
+ // Fallback: escape special chars and treat as a prefix match
190
+ return MATCHES_NOTHING;
191
+ }
192
+ };
193
+
194
+ /**
195
+ * Compile array of order patterns into regexes.
196
+ * @param {string[]} order
197
+ * @param {import("eslint").Rule.RuleContext} context
198
+ * @returns {RegExp[]} Array of compiled regular expressions for order patterns.
199
+ */
200
+ const compileOrderPatterns = (order, context) =>
201
+ order.map((pattern) => compilePattern(pattern, context));
202
+
203
+ /**
204
+ * Callback to get the order index of a property name based on compiled regex patterns.
205
+ * @callback OrderIndexGetter
206
+ * @param {string} propName Property name to check.
207
+ * @returns {number} Order index based on matching pattern, or length of patterns if no match.
208
+ */
209
+
210
+ /**
211
+ * Create a function that returns the order index of a property name based on
212
+ * compiled regex patterns. Returns pattern array length if no match found.
213
+ * @param {RegExp[]} compiledOrder - Compiled regex patterns in priority order.
214
+ * @returns {OrderIndexGetter} Function that takes a property name and returns its order index.
215
+ */
216
+ const createOrderIndexGetter = (compiledOrder) => (propName) => {
217
+ for (let i = 0; i < compiledOrder.length; i += 1) {
218
+ if (compiledOrder[i].test(propName)) {
219
+ return i;
220
+ }
221
+ }
222
+ return compiledOrder.length;
223
+ };
224
+
225
+ /**
226
+ * Check if properties are sorted by order index first, then alphabetically
227
+ * within the same index.
228
+ * @param {CustomProperty[]} properties
229
+ * @returns {boolean} `true` if properties are sorted.
230
+ */
231
+ const checkIfSorted = (properties) => {
232
+ for (let i = 1; i < properties.length; i += 1) {
233
+ const prev = properties[i - 1];
234
+ const curr = properties[i];
235
+
236
+ if (prev.orderIndex > curr.orderIndex) {
237
+ return false;
238
+ }
239
+
240
+ if (prev.orderIndex === curr.orderIndex && prev.property > curr.property) {
241
+ return false;
242
+ }
243
+ }
244
+ return true;
245
+ };
246
+
247
+ /**
248
+ * Check if empty lines are missing between property groups.
249
+ * @param {CustomProperty[]} properties
250
+ * @returns {boolean} `true` if empty lines are missing between groups.
251
+ */
252
+ const hasMissingGroupSpacing = (properties) => {
253
+ // Minimum lines between groups to be considered as having empty lines
254
+ const SPACING_THRESHOLD = 2;
255
+ for (let i = 1; i < properties.length; i += 1) {
256
+ const prev = properties[i - 1];
257
+ const curr = properties[i];
258
+
259
+ if (
260
+ prev.orderIndex !== curr.orderIndex &&
261
+ typeof curr.node.loc?.start.line !== "undefined" &&
262
+ typeof prev.node.loc?.end.line !== "undefined"
263
+ ) {
264
+ const linesBetween = curr.node.loc.start.line - prev.node.loc.end.line;
265
+ if (linesBetween < SPACING_THRESHOLD) {
266
+ // Needs empty lines
267
+ return true;
268
+ }
269
+ }
270
+ }
271
+ return false;
272
+ };
273
+
274
+ /**
275
+ * Sort properties by order index and then alphabetically within the same index.
276
+ * @param {CustomProperty[]} properties
277
+ * @returns {CustomProperty[]} Sorted properties.
278
+ */
279
+ const sortProperties = (properties) =>
280
+ [...properties].sort((a, b) => {
281
+ if (a.orderIndex !== b.orderIndex) {
282
+ return a.orderIndex - b.orderIndex;
283
+ }
284
+
285
+ return a.property.localeCompare(b.property);
286
+ });
287
+
288
+ /**
289
+ * Get full declaration text including leading whitespace and trailing semicolon.
290
+ * Extracts from start of line to end of node, appending semicolon if present.
291
+ * @param {import("estree").SourceLocation} loc
292
+ * @param {import("eslint").SourceCode} sourceCode
293
+ * @returns {string} Full declaration text with indentation and semicolon.
294
+ */
295
+ const getFullDeclaration = (loc, sourceCode) => {
296
+ const lineStartIndex = sourceCode.getIndexFromLoc({
297
+ column: 1,
298
+ line: loc.start.line,
299
+ });
300
+
301
+ if (!("offset" in loc.end) || typeof loc.end.offset !== "number") {
302
+ throw new Error("Node missing offset information");
303
+ }
304
+
305
+ let endIndex = loc.end.offset;
306
+ if (sourceCode.text[endIndex] === ";") {
307
+ endIndex += 1;
308
+ }
309
+
310
+ return sourceCode.text.slice(lineStartIndex, endIndex);
311
+ };
312
+
313
+ /**
314
+ * Calculate the end for a property declaration. Returns the start line of the
315
+ * next property if it exists, otherwise one line past current property.
316
+ * @param {CustomProperty} prop
317
+ * @param {CustomProperty | undefined} nextProp
318
+ * @returns {number} End line number for the property range.
319
+ */
320
+ const calculateEndLine = (prop, nextProp) => {
321
+ if (typeof prop.node.loc === "undefined" || prop.node.loc === null) {
322
+ throw new Error("Node missing location information");
323
+ }
324
+
325
+ if (typeof nextProp !== "undefined") {
326
+ if (
327
+ typeof nextProp.node.loc === "undefined" ||
328
+ nextProp.node.loc === null
329
+ ) {
330
+ throw new Error("Node missing location information");
331
+ }
332
+ return nextProp.node.loc.start.line;
333
+ }
334
+
335
+ return prop.node.loc.end.line + 1;
336
+ };
337
+
338
+ /**
339
+ * Build replacement text for a property with optional empty line separator.
340
+ * Adds blank line before property if it's in a different group than the
341
+ * previous.
342
+ * @param {object} config
343
+ * @param {string} config.sortedDeclaration Full declaration text of the sorted property.
344
+ * @param {number} config.index Index of the current property in the sorted array.
345
+ * @param {CustomProperty[]} config.sorted Array of properties sorted by order and name.
346
+ * @param {boolean} config.emptyLineBetweenGroups Whether to add empty line between groups.
347
+ * @returns {string} Replacement text with optional leading newline and trailing newline.
348
+ */
349
+ const buildReplacement = (config) => {
350
+ let replacement = "";
351
+
352
+ if (config.emptyLineBetweenGroups && config.index > 0) {
353
+ const prevOrderIndex = config.sorted[config.index - 1].orderIndex;
354
+ const currOrderIndex = config.sorted[config.index].orderIndex;
355
+
356
+ if (prevOrderIndex !== currOrderIndex) {
357
+ replacement = "\n";
358
+ }
359
+ }
360
+
361
+ replacement += `${config.sortedDeclaration}\n`;
362
+ return replacement;
363
+ };
364
+
365
+ /**
366
+ * Create a single fix to replace a property with its sorted equivalent.
367
+ * Extracts the sorted property declaration and replaces the current property's
368
+ * full line range, including proper group spacing.
369
+ * @param {object} options
370
+ * @param {CustomProperty} options.prop Current property to be replaced.
371
+ * @param {number} options.index Index of the current property in the sorted array.
372
+ * @param {FixerConfig} options.config Fixer configuration with sorted properties and settings.
373
+ * @param {import("eslint").Rule.RuleFixer} options.fixer ESLint fixer instance for creating text replacements.
374
+ * @returns {import("eslint").Rule.Fix} Fix that replaces current property with sorted version.
375
+ */
376
+ // oxlint-disable-next-line max-statements
377
+ const createSingleFix = (options) => {
378
+ const { sorted, currentBlockProperties, emptyLineBetweenGroups, sourceCode } =
379
+ options.config;
380
+
381
+ const sortedNode = sorted[options.index].node;
382
+ const propNode = options.prop.node;
383
+
384
+ const sortedLoc = getNodeLoc(sortedNode);
385
+ const propLoc = getNodeLoc(propNode);
386
+
387
+ const sortedDeclaration = getFullDeclaration(sortedLoc, sourceCode);
388
+
389
+ const currentLineStart = sourceCode.getIndexFromLoc({
390
+ column: 1,
391
+ line: propLoc.start.line,
392
+ });
393
+
394
+ const endLine = calculateEndLine(
395
+ options.prop,
396
+ currentBlockProperties[options.index + 1],
397
+ );
398
+ const currentLineEnd = sourceCode.getIndexFromLoc({
399
+ column: 1,
400
+ line: endLine,
401
+ });
402
+
403
+ const replacement = buildReplacement({
404
+ emptyLineBetweenGroups,
405
+ index: options.index,
406
+ sorted,
407
+ sortedDeclaration,
408
+ });
409
+
410
+ return options.fixer.replaceTextRange(
411
+ [currentLineStart, currentLineEnd],
412
+ replacement,
413
+ );
414
+ };
415
+
416
+ /**
417
+ * @callback FixerFunction
418
+ * @param {import("eslint").Rule.RuleFixer} fixer ESLint rule fixer.
419
+ * @returns {import("eslint").Rule.Fix[] | null} Array of fixes or null if unsafe.
420
+ */
421
+
422
+ /**
423
+ * Create a fixer function that reorders all properties in a block. Returns null
424
+ * if any node is missing offset information, otherwise returns fixes for all
425
+ * properties in the current block.
426
+ * @param {FixerConfig} config - Fixer configuration with sorted properties.
427
+ * @returns {FixerFunction} Fixer function that generates fixes or null if unsafe.
428
+ */
429
+ const createFixer = (config) => (fixer) => {
430
+ const { sorted, currentBlockProperties } = config;
431
+
432
+ try {
433
+ for (const item of sorted) {
434
+ getNodeLoc(item.node);
435
+ }
436
+ } catch {
437
+ return null;
438
+ }
439
+
440
+ const fixes = currentBlockProperties.map((prop, index) =>
441
+ createSingleFix({ config, fixer, index, prop }),
442
+ );
443
+
444
+ return fixes;
445
+ };
446
+
447
+ /**
448
+ * Check if empty lines are required between groups and if any are missing.
449
+ * @param {boolean} emptyLineBetweenGroups
450
+ * @param {CustomProperty[]} currentBlockProperties
451
+ * @returns {boolean} `true` if empty lines are required between groups.
452
+ */
453
+ const checkEmptyLinesIfRequired = (
454
+ emptyLineBetweenGroups,
455
+ currentBlockProperties,
456
+ ) => {
457
+ if (!emptyLineBetweenGroups) {
458
+ return false;
459
+ }
460
+
461
+ return hasMissingGroupSpacing(currentBlockProperties);
462
+ };
463
+
464
+ /**
465
+ * Process property block for violations and report if sorting or spacing issues
466
+ * found. Checks sort order first, then group spacing if applicable, and reports with
467
+ * autofix.
468
+ * @param {CustomProperty[]} currentBlockProperties
469
+ * @param {BlockHandlerConfig} config
470
+ */
471
+ const processBlockViolations = (currentBlockProperties, config) => {
472
+ const isSorted = checkIfSorted(currentBlockProperties);
473
+ let needsEmptyLines = false;
474
+ if (isSorted) {
475
+ needsEmptyLines = checkEmptyLinesIfRequired(
476
+ config.emptyLineBetweenGroups,
477
+ currentBlockProperties,
478
+ );
479
+ }
480
+
481
+ const messageId = getViolationMessageId(isSorted, needsEmptyLines);
482
+ if (!messageId) {
483
+ return;
484
+ }
485
+
486
+ const sorted = sortProperties(currentBlockProperties);
487
+ const report = createReportDescriptor({
488
+ currentBlockProperties,
489
+ emptyLineBetweenGroups: config.emptyLineBetweenGroups,
490
+ messageId,
491
+ order: config.order,
492
+ sorted,
493
+ sourceCode: config.sourceCode,
494
+ });
495
+
496
+ config.context.report(report);
497
+ };
498
+
499
+ /**
500
+ * Determine the appropriate violation message based on sorting and spacing
501
+ * state.
502
+ * @param {boolean} isSorted
503
+ * @param {boolean} needsEmptyLines
504
+ * @returns {MessageIds | undefined} Message ID for the violation.
505
+ */
506
+ const getViolationMessageId = (isSorted, needsEmptyLines) => {
507
+ if (isSorted && !needsEmptyLines) {
508
+ return;
509
+ }
510
+
511
+ if (isSorted) {
512
+ return MESSAGE_IDS.missingEmptyLineBetweenGroups;
513
+ }
514
+
515
+ return MESSAGE_IDS.unsortedCustomProperties;
516
+ };
517
+
518
+ /**
519
+ * Create ESLint report descriptor with autofix for property ordering violations.
520
+ * @param {object} config
521
+ * @param {CustomProperty[]} config.currentBlockProperties Properties in the current block being processed.
522
+ * @param {CustomProperty[]} config.sorted Properties sorted according to the defined order.
523
+ * @param {boolean} config.emptyLineBetweenGroups Whether to add empty line between groups in the fix.
524
+ * @param {import("eslint").SourceCode} config.sourceCode ESLint SourceCode instance for text extraction.
525
+ * @param {MessageIds} config.messageId Message ID for the violation being reported.
526
+ * @param {string[]} config.order Array of RegEx patterns defining the sort order, used for error message if unsorted.
527
+ * @returns {import("eslint").Rule.ReportDescriptor} Report descriptor with fixer and message.
528
+ */
529
+ const createReportDescriptor = (config) => ({
530
+ fix: createFixer(config),
531
+ messageId: config.messageId,
532
+ node: config.currentBlockProperties[0].node,
533
+ // oxlint-disable-next-line oxc/no-rest-spread-properties data is readonly
534
+ ...(config.messageId === MESSAGE_IDS.unsortedCustomProperties && {
535
+ data: {
536
+ order: config.order.join(", "),
537
+ },
538
+ }),
539
+ });
540
+
541
+ /**
542
+ * Create exit handler for property blocks. Pops block from stack and validates property ordering if block has sufficient
543
+ * properties.
544
+ * @param {BlockHandlerConfig} config - Block handler configuration.
545
+ * @returns {() => void} Exit handler function for the AST node.
546
+ */
547
+ const handleBlockExit = (config) => () => {
548
+ const currentBlockProperties = config.blockStack.pop();
549
+
550
+ const MIN_PROPERTIES_TO_CHECK = 2;
551
+ if (
552
+ typeof currentBlockProperties === "undefined" ||
553
+ currentBlockProperties.length < MIN_PROPERTIES_TO_CHECK
554
+ ) {
555
+ return;
556
+ }
557
+
558
+ processBlockViolations(currentBlockProperties, config);
559
+ };
560
+
561
+ /**
562
+ * @callback DeclarationCollector
563
+ * @param {import("estree").Node} node AST node with property name.
564
+ * @returns {void}
565
+ */
566
+
567
+ /**
568
+ * Create collector for CSS declaration nodes to track custom properties. Only
569
+ * processes properties starting with '--' and adds them to the current block.
570
+ * @param {CustomProperty[][]} blockStack - Stack of property blocks being processed.
571
+ * @param {OrderIndexGetter} getMatchingOrderIndex - Function to get order index for a property name.
572
+ * @returns {DeclarationCollector} Declaration collector function for AST nodes.
573
+ */
574
+ const collectDeclaration = (blockStack, getMatchingOrderIndex) => (node) => {
575
+ if (!("property" in node)) {
576
+ return;
577
+ }
578
+
579
+ const property = String(node.property);
580
+ if (!property.startsWith("--")) {
581
+ return;
582
+ }
583
+
584
+ const orderIndex = getMatchingOrderIndex(property);
585
+ const currentBlock = blockStack.at(-1);
586
+
587
+ if (!currentBlock) {
588
+ return;
589
+ }
590
+
591
+ currentBlock.push({
592
+ node,
593
+ orderIndex,
594
+ property,
595
+ });
596
+ };