tailwind-typescript-plugin 1.4.0-beta.21 → 1.4.0-beta.22

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.
@@ -35,13 +35,16 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
35
35
  * Vue generates code like __VLS_ctx.clsx(...) for template expressions
36
36
  * where clsx is imported in the script section. We need to check if the
37
37
  * function name (not __VLS_ctx) is directly imported.
38
+ *
39
+ * Also handles namespace imports: __VLS_ctx.utils.clsx(...) for `import * as utils from 'clsx'`
38
40
  */
39
41
  shouldValidateFunctionCall(callExpression, utilityFunctions, context) {
40
- // First, check if this is a __VLS_ctx.functionName() pattern
42
+ // First, check if this is a __VLS_ctx.functionName() or __VLS_ctx.namespace.functionName() pattern
41
43
  if (context) {
42
44
  const expr = callExpression.expression;
43
45
  if (context.typescript.isPropertyAccessExpression(expr)) {
44
46
  const objectExpr = expr.expression;
47
+ // Pattern 1: __VLS_ctx.functionName() - direct import
45
48
  if (context.typescript.isIdentifier(objectExpr) && objectExpr.text === '__VLS_ctx') {
46
49
  const functionName = expr.name.text;
47
50
  // Check each utility function configuration
@@ -60,6 +63,31 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
60
63
  }
61
64
  return false;
62
65
  }
66
+ // Pattern 2: __VLS_ctx.namespace.functionName() - namespace import
67
+ // e.g., import * as utils from 'clsx' -> __VLS_ctx.utils.clsx()
68
+ if (context.typescript.isPropertyAccessExpression(objectExpr)) {
69
+ const namespaceRoot = objectExpr.expression;
70
+ if (context.typescript.isIdentifier(namespaceRoot) &&
71
+ namespaceRoot.text === '__VLS_ctx') {
72
+ const namespaceName = objectExpr.name.text; // e.g., 'utils'
73
+ const functionName = expr.name.text; // e.g., 'clsx'
74
+ // Check each utility function configuration
75
+ for (const utilityFunc of utilityFunctions) {
76
+ if (typeof utilityFunc === 'string') {
77
+ if (utilityFunc === functionName) {
78
+ return true;
79
+ }
80
+ }
81
+ else if (utilityFunc.name === functionName) {
82
+ // Check if namespace is imported from expected module
83
+ if (this.isNamespaceImportedFrom(namespaceName, utilityFunc.from, context)) {
84
+ return true;
85
+ }
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ }
63
91
  }
64
92
  }
65
93
  // Fall back to base implementation for non-Vue patterns
@@ -67,17 +95,12 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
67
95
  }
68
96
  canHandle(node, context) {
69
97
  // We handle call expressions that look like Vue's generated element calls
70
- // Pattern: __VLS_asFunctionalElement(...)({ ...{ class: ... } })
98
+ // Pattern 1 (intrinsic elements): __VLS_asFunctionalElement(...)({ ...{ class: ... } })
99
+ // Pattern 2 (custom components): __VLS_1({ colorStyles: ... }, ...)
71
100
  if (!context.typescript.isCallExpression(node)) {
72
101
  return false;
73
102
  }
74
- // Check if this is a chained call (the outer call to the element function)
75
- // The pattern is: func(...)({...}) where the result of func(...) is called again
76
- const expression = node.expression;
77
- if (!context.typescript.isCallExpression(expression)) {
78
- return false;
79
- }
80
- // Check if the arguments contain an object with spread that has a 'class' property
103
+ // Check if the arguments contain an object with class properties
81
104
  if (node.arguments.length === 0) {
82
105
  return false;
83
106
  }
@@ -85,10 +108,28 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
85
108
  if (!context.typescript.isObjectLiteralExpression(firstArg)) {
86
109
  return false;
87
110
  }
88
- // Look for spread assignments with 'class' property
89
- return this.hasClassSpreadProperty(firstArg, context);
111
+ const expression = node.expression;
112
+ // Pattern 1: Chained call (intrinsic elements)
113
+ // func(...)({...}) where the result of func(...) is called again
114
+ if (context.typescript.isCallExpression(expression)) {
115
+ // Look for spread assignments with class property
116
+ return this.hasClassSpreadProperty(firstArg, context);
117
+ }
118
+ // Pattern 2: Identifier call (custom components)
119
+ // __VLS_N({ classAttribute: ... }, ...)
120
+ if (context.typescript.isIdentifier(expression)) {
121
+ const name = expression.text;
122
+ // Vue generates __VLS_0, __VLS_1, etc. for component instances
123
+ if (name.startsWith('__VLS_')) {
124
+ // Check for direct class attribute properties
125
+ return this.hasClassDirectProperty(firstArg, context);
126
+ }
127
+ }
128
+ return false;
90
129
  }
91
130
  hasClassSpreadProperty(obj, context) {
131
+ // Build set of class attribute names to check
132
+ const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
92
133
  for (const prop of obj.properties) {
93
134
  if (context.typescript.isSpreadAssignment(prop)) {
94
135
  const spreadExpr = prop.expression;
@@ -96,7 +137,7 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
96
137
  for (const innerProp of spreadExpr.properties) {
97
138
  if (context.typescript.isPropertyAssignment(innerProp)) {
98
139
  const name = innerProp.name;
99
- if (context.typescript.isIdentifier(name) && name.text === 'class') {
140
+ if (context.typescript.isIdentifier(name) && classAttributeNames.has(name.text)) {
100
141
  return true;
101
142
  }
102
143
  }
@@ -106,6 +147,22 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
106
147
  }
107
148
  return false;
108
149
  }
150
+ /**
151
+ * Check if an object has direct class attribute properties (for custom components).
152
+ * Vue generates direct properties like: { colorStyles: "bg-blue-500" }
153
+ */
154
+ hasClassDirectProperty(obj, context) {
155
+ const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
156
+ for (const prop of obj.properties) {
157
+ if (context.typescript.isPropertyAssignment(prop)) {
158
+ const name = prop.name;
159
+ if (context.typescript.isIdentifier(name) && classAttributeNames.has(name.text)) {
160
+ return true;
161
+ }
162
+ }
163
+ }
164
+ return false;
165
+ }
109
166
  extract(node, context) {
110
167
  const classNames = [];
111
168
  if (!context.typescript.isCallExpression(node)) {
@@ -115,26 +172,44 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
115
172
  if (!firstArg || !context.typescript.isObjectLiteralExpression(firstArg)) {
116
173
  return classNames;
117
174
  }
118
- // Find spread assignments with 'class' property
175
+ // Build set of class attribute names to check
176
+ const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
177
+ // Process all properties in the object literal
119
178
  for (const prop of firstArg.properties) {
120
- if (!context.typescript.isSpreadAssignment(prop)) {
121
- continue;
122
- }
123
- const spreadExpr = prop.expression;
124
- if (!context.typescript.isObjectLiteralExpression(spreadExpr)) {
125
- continue;
179
+ // Handle spread assignments: ...{ class: "..." }
180
+ if (context.typescript.isSpreadAssignment(prop)) {
181
+ const spreadExpr = prop.expression;
182
+ if (context.typescript.isObjectLiteralExpression(spreadExpr)) {
183
+ for (const innerProp of spreadExpr.properties) {
184
+ if (!context.typescript.isPropertyAssignment(innerProp)) {
185
+ continue;
186
+ }
187
+ const name = innerProp.name;
188
+ if (!context.typescript.isIdentifier(name)) {
189
+ continue;
190
+ }
191
+ // Check if this is a class attribute
192
+ if (!classAttributeNames.has(name.text)) {
193
+ continue;
194
+ }
195
+ const value = innerProp.initializer;
196
+ const attributeId = `${innerProp.getStart()}-${innerProp.getEnd()}`;
197
+ classNames.push(...this.extractClassesFromValue(value, context, attributeId));
198
+ }
199
+ }
126
200
  }
127
- for (const innerProp of spreadExpr.properties) {
128
- if (!context.typescript.isPropertyAssignment(innerProp)) {
201
+ // Handle direct property assignments: colorStyles: "..."
202
+ else if (context.typescript.isPropertyAssignment(prop)) {
203
+ const name = prop.name;
204
+ if (!context.typescript.isIdentifier(name)) {
129
205
  continue;
130
206
  }
131
- const name = innerProp.name;
132
- if (!context.typescript.isIdentifier(name) || name.text !== 'class') {
207
+ // Check if this is a class attribute (custom attributes like colorStyles)
208
+ if (!classAttributeNames.has(name.text)) {
133
209
  continue;
134
210
  }
135
- const value = innerProp.initializer;
136
- const attributeId = `${innerProp.getStart()}-${innerProp.getEnd()}`;
137
- // Handle different class value types
211
+ const value = prop.initializer;
212
+ const attributeId = `${prop.getStart()}-${prop.getEnd()}`;
138
213
  classNames.push(...this.extractClassesFromValue(value, context, attributeId));
139
214
  }
140
215
  }
@@ -180,47 +255,8 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
180
255
  }
181
256
  }
182
257
  if (objectExpr) {
183
- for (const prop of objectExpr.properties) {
184
- if (context.typescript.isPropertyAssignment(prop)) {
185
- const propName = prop.name;
186
- let className;
187
- let start;
188
- // Handle string literal keys: { 'bg-red-500': true }
189
- if (context.typescript.isStringLiteral(propName)) {
190
- className = propName.text;
191
- start = propName.getStart() + 1; // Skip opening quote
192
- }
193
- // Handle identifier keys: { flex: true }
194
- else if (context.typescript.isIdentifier(propName)) {
195
- className = propName.text;
196
- start = propName.getStart();
197
- }
198
- if (className && start !== undefined) {
199
- classNames.push({
200
- className,
201
- absoluteStart: start,
202
- length: className.length,
203
- line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
204
- file: context.sourceFile.fileName,
205
- attributeId
206
- });
207
- }
208
- }
209
- // Handle shorthand properties: { flex }
210
- else if (context.typescript.isShorthandPropertyAssignment(prop)) {
211
- const className = prop.name.text;
212
- const start = prop.name.getStart();
213
- classNames.push({
214
- className,
215
- absoluteStart: start,
216
- length: className.length,
217
- line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
218
- file: context.sourceFile.fileName,
219
- attributeId
220
- });
221
- }
222
- }
223
- return classNames;
258
+ // Use extractFromObjectExpression which handles computed property names
259
+ return this.extractFromObjectExpression(objectExpr, context, attributeId);
224
260
  }
225
261
  // Array literal: class: ['flex', 'items-center']
226
262
  // Vue wraps expressions in parentheses: class: (['flex', 'items-center'])
@@ -235,8 +271,8 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
235
271
  }
236
272
  }
237
273
  if (arrayExpr) {
238
- const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
239
- return addAttributeId(this.expressionExtractor.extract(arrayExpr, context));
274
+ // Process array elements directly to handle __VLS_ctx references
275
+ return this.extractFromArrayExpression(arrayExpr, context, attributeId);
240
276
  }
241
277
  // Template literal or other expressions - delegate to expression extractor
242
278
  // Vue wraps expressions in parentheses: class: (`flex items-center`)
@@ -268,9 +304,84 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
268
304
  callExpr = inner;
269
305
  }
270
306
  }
271
- if (callExpr && this.shouldValidateFunctionCall(callExpr, context.utilityFunctions, context)) {
272
- const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
273
- return addAttributeId(this.expressionExtractor.extract(callExpr, context));
307
+ if (callExpr) {
308
+ // Check if it's a utility function (clsx, cn, etc.) - extract all arguments
309
+ if (this.shouldValidateFunctionCall(callExpr, context.utilityFunctions, context)) {
310
+ const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
311
+ return addAttributeId(this.expressionExtractor.extract(callExpr, context));
312
+ }
313
+ // Check for CVA/TV function calls with class override: button({ class: '...' })
314
+ // These are __VLS_ctx.functionName({ class: '...' }) patterns
315
+ const classOverrideClasses = this.extractFromCvaTvClassOverride(callExpr, context, attributeId);
316
+ if (classOverrideClasses.length > 0) {
317
+ return classOverrideClasses;
318
+ }
319
+ }
320
+ // Handle conditional (ternary) expressions: class: (isActive ? 'flex' : 'hidden')
321
+ // Vue wraps expressions in parentheses: class: (__VLS_ctx.isActive ? 'active' : 'inactive')
322
+ let conditionalExpr;
323
+ if (context.typescript.isConditionalExpression(value)) {
324
+ conditionalExpr = value;
325
+ }
326
+ else if (context.typescript.isParenthesizedExpression(value)) {
327
+ const inner = value.expression;
328
+ if (context.typescript.isConditionalExpression(inner)) {
329
+ conditionalExpr = inner;
330
+ }
331
+ }
332
+ if (conditionalExpr) {
333
+ return this.extractFromConditionalExpression(conditionalExpr, context, attributeId);
334
+ }
335
+ // Handle binary expressions: class: (isActive && 'flex')
336
+ // Vue wraps expressions in parentheses: class: (__VLS_ctx.isActive && 'active')
337
+ let binaryExpr;
338
+ if (context.typescript.isBinaryExpression(value)) {
339
+ binaryExpr = value;
340
+ }
341
+ else if (context.typescript.isParenthesizedExpression(value)) {
342
+ const inner = value.expression;
343
+ if (context.typescript.isBinaryExpression(inner)) {
344
+ binaryExpr = inner;
345
+ }
346
+ }
347
+ if (binaryExpr) {
348
+ return this.extractFromBinaryExpression(binaryExpr, context, attributeId);
349
+ }
350
+ // Handle type assertions: class: ('invalid-class' as string)
351
+ // Vue wraps expressions: class: (('invalid-class' as string))
352
+ let asExpr;
353
+ if (context.typescript.isAsExpression(value)) {
354
+ asExpr = value;
355
+ }
356
+ else if (context.typescript.isParenthesizedExpression(value)) {
357
+ let inner = value.expression;
358
+ // Double unwrap for nested parentheses
359
+ if (context.typescript.isParenthesizedExpression(inner)) {
360
+ inner = inner.expression;
361
+ }
362
+ if (context.typescript.isAsExpression(inner)) {
363
+ asExpr = inner;
364
+ }
365
+ }
366
+ if (asExpr) {
367
+ return this.extractClassesFromValue(asExpr.expression, context, attributeId);
368
+ }
369
+ // Handle non-null assertions: class: (someClass!)
370
+ let nonNullExpr;
371
+ if (context.typescript.isNonNullExpression(value)) {
372
+ nonNullExpr = value;
373
+ }
374
+ else if (context.typescript.isParenthesizedExpression(value)) {
375
+ let inner = value.expression;
376
+ if (context.typescript.isParenthesizedExpression(inner)) {
377
+ inner = inner.expression;
378
+ }
379
+ if (context.typescript.isNonNullExpression(inner)) {
380
+ nonNullExpr = inner;
381
+ }
382
+ }
383
+ if (nonNullExpr) {
384
+ return this.extractClassesFromValue(nonNullExpr.expression, context, attributeId);
274
385
  }
275
386
  // Handle __VLS_ctx.propertyName patterns for variable/computed/function references
276
387
  // Vue generates: class: (__VLS_ctx.myClass) for :class="myClass"
@@ -280,6 +391,396 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
280
391
  }
281
392
  return classNames;
282
393
  }
394
+ /**
395
+ * Extract classes from an array expression, handling __VLS_ctx references.
396
+ * This method processes array elements directly to support variable references.
397
+ */
398
+ extractFromArrayExpression(arrayExpr, context, attributeId) {
399
+ const { typescript } = context;
400
+ const classNames = [];
401
+ for (const element of arrayExpr.elements) {
402
+ if (element === undefined)
403
+ continue;
404
+ // String literal: 'flex'
405
+ if (typescript.isStringLiteral(element)) {
406
+ const fullText = element.text;
407
+ if (fullText.length > 0) {
408
+ const stringContentStart = element.getStart() + 1;
409
+ let offset = 0;
410
+ const parts = fullText.split(/(\s+)/);
411
+ for (const part of parts) {
412
+ if (part && !/^\s+$/.test(part)) {
413
+ classNames.push({
414
+ className: part,
415
+ absoluteStart: stringContentStart + offset,
416
+ length: part.length,
417
+ line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset)
418
+ .line + 1,
419
+ file: context.sourceFile.fileName,
420
+ attributeId
421
+ });
422
+ }
423
+ offset += part.length;
424
+ }
425
+ }
426
+ }
427
+ // Object literal: { 'bg-red-500': isActive }
428
+ else if (typescript.isObjectLiteralExpression(element)) {
429
+ classNames.push(...this.extractFromObjectExpression(element, context, attributeId));
430
+ }
431
+ // Spread element: ...classes
432
+ else if (typescript.isSpreadElement(element)) {
433
+ // Try to resolve __VLS_ctx reference
434
+ const vlsResults = this.extractFromVlsCtxReference(element.expression, context, attributeId);
435
+ if (vlsResults.length > 0) {
436
+ classNames.push(...vlsResults);
437
+ }
438
+ }
439
+ // Handle __VLS_ctx.variable references: __VLS_ctx.myClass
440
+ else if (typescript.isPropertyAccessExpression(element)) {
441
+ const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
442
+ if (vlsResults.length > 0) {
443
+ classNames.push(...vlsResults);
444
+ }
445
+ }
446
+ // Handle parenthesized expressions: (__VLS_ctx.myVar)
447
+ else if (typescript.isParenthesizedExpression(element)) {
448
+ const inner = element.expression;
449
+ if (typescript.isPropertyAccessExpression(inner)) {
450
+ const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
451
+ if (vlsResults.length > 0) {
452
+ classNames.push(...vlsResults);
453
+ }
454
+ }
455
+ else if (typescript.isArrayLiteralExpression(inner)) {
456
+ // Nested array in parentheses
457
+ classNames.push(...this.extractFromArrayExpression(inner, context, attributeId));
458
+ }
459
+ }
460
+ // Handle nested arrays recursively
461
+ else if (typescript.isArrayLiteralExpression(element)) {
462
+ classNames.push(...this.extractFromArrayExpression(element, context, attributeId));
463
+ }
464
+ // Handle ternary/conditional expressions
465
+ else if (typescript.isConditionalExpression(element)) {
466
+ // Extract from both branches
467
+ classNames.push(...this.extractFromConditionalElement(element, context, attributeId));
468
+ }
469
+ }
470
+ return classNames;
471
+ }
472
+ /**
473
+ * Extract classes from an object expression, handling computed property names.
474
+ */
475
+ extractFromObjectExpression(objExpr, context, attributeId) {
476
+ const { typescript } = context;
477
+ const classNames = [];
478
+ for (const prop of objExpr.properties) {
479
+ if (typescript.isPropertyAssignment(prop)) {
480
+ const propName = prop.name;
481
+ let className;
482
+ let start;
483
+ if (typescript.isStringLiteral(propName)) {
484
+ className = propName.text;
485
+ start = propName.getStart() + 1;
486
+ }
487
+ else if (typescript.isIdentifier(propName)) {
488
+ className = propName.text;
489
+ start = propName.getStart();
490
+ }
491
+ // Handle computed property names: { [__VLS_ctx.myVar]: true }
492
+ else if (typescript.isComputedPropertyName(propName)) {
493
+ let computedExpr = propName.expression;
494
+ // Unwrap parentheses
495
+ if (typescript.isParenthesizedExpression(computedExpr)) {
496
+ computedExpr = computedExpr.expression;
497
+ }
498
+ // Resolve __VLS_ctx.variable pattern
499
+ if (typescript.isPropertyAccessExpression(computedExpr)) {
500
+ const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
501
+ if (vlsResults.length > 0) {
502
+ classNames.push(...vlsResults);
503
+ continue;
504
+ }
505
+ }
506
+ }
507
+ if (className && start !== undefined) {
508
+ classNames.push({
509
+ className,
510
+ absoluteStart: start,
511
+ length: className.length,
512
+ line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
513
+ file: context.sourceFile.fileName,
514
+ attributeId
515
+ });
516
+ }
517
+ }
518
+ else if (typescript.isShorthandPropertyAssignment(prop)) {
519
+ const className = prop.name.text;
520
+ const start = prop.name.getStart();
521
+ classNames.push({
522
+ className,
523
+ absoluteStart: start,
524
+ length: className.length,
525
+ line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
526
+ file: context.sourceFile.fileName,
527
+ attributeId
528
+ });
529
+ }
530
+ }
531
+ return classNames;
532
+ }
533
+ /**
534
+ * Extract classes from a conditional (ternary) expression in array context.
535
+ */
536
+ extractFromConditionalElement(conditional, context, attributeId) {
537
+ return this.extractFromConditionalExpression(conditional, context, attributeId);
538
+ }
539
+ /**
540
+ * Extract classes from a conditional (ternary) expression.
541
+ * Handles: isActive ? 'flex' : 'hidden'
542
+ */
543
+ extractFromConditionalExpression(conditional, context, attributeId) {
544
+ const classNames = [];
545
+ // Use the ternary's position as a unique identifier (like ExpressionExtractor)
546
+ const ternaryId = conditional.getStart();
547
+ // Extract from true branch with conditionalBranchId
548
+ const whenTrue = conditional.whenTrue;
549
+ classNames.push(...this.extractFromBranchExpression(whenTrue, context, attributeId, `ternary:true:${ternaryId}`));
550
+ // Extract from false branch with conditionalBranchId
551
+ const whenFalse = conditional.whenFalse;
552
+ classNames.push(...this.extractFromBranchExpression(whenFalse, context, attributeId, `ternary:false:${ternaryId}`));
553
+ return classNames;
554
+ }
555
+ /**
556
+ * Extract classes from a binary expression.
557
+ * Handles: isActive && 'flex', isDisabled || 'fallback'
558
+ */
559
+ extractFromBinaryExpression(binary, context, attributeId) {
560
+ const classNames = [];
561
+ // Extract from left operand (for patterns like: 'flex' || fallback)
562
+ classNames.push(...this.extractFromBranchExpression(binary.left, context, attributeId));
563
+ // Extract from right operand (for patterns like: isActive && 'flex')
564
+ classNames.push(...this.extractFromBranchExpression(binary.right, context, attributeId));
565
+ return classNames;
566
+ }
567
+ /**
568
+ * Extract classes from a branch expression (ternary branch or binary operand).
569
+ * Handles string literals, nested ternaries, nested binaries, and VLS references.
570
+ */
571
+ extractFromBranchExpression(expr, context, attributeId, conditionalBranchId) {
572
+ const { typescript } = context;
573
+ const classNames = [];
574
+ // Helper to add conditionalBranchId to extracted classes
575
+ const addBranchId = (classes) => conditionalBranchId ? classes.map(c => ({ ...c, conditionalBranchId })) : classes;
576
+ // String literal: 'flex items-center'
577
+ if (typescript.isStringLiteral(expr)) {
578
+ classNames.push(...addBranchId(this.extractClassesFromStringLiteral(expr, context, attributeId)));
579
+ }
580
+ // Nested ternary: condition ? 'a' : (nested ? 'b' : 'c')
581
+ else if (typescript.isConditionalExpression(expr)) {
582
+ classNames.push(...this.extractFromConditionalExpression(expr, context, attributeId));
583
+ }
584
+ // Nested binary: isA && isB && 'class'
585
+ else if (typescript.isBinaryExpression(expr)) {
586
+ classNames.push(...addBranchId(this.extractFromBinaryExpression(expr, context, attributeId)));
587
+ }
588
+ // Parenthesized expression
589
+ else if (typescript.isParenthesizedExpression(expr)) {
590
+ classNames.push(...this.extractFromBranchExpression(expr.expression, context, attributeId, conditionalBranchId));
591
+ }
592
+ // VLS ctx reference: __VLS_ctx.myClass
593
+ else if (typescript.isPropertyAccessExpression(expr)) {
594
+ const vlsResults = this.extractFromVlsCtxReference(expr, context, attributeId);
595
+ classNames.push(...addBranchId(vlsResults));
596
+ }
597
+ // Template literal: `flex ${something}`
598
+ else if (typescript.isTemplateExpression(expr) ||
599
+ typescript.isNoSubstitutionTemplateLiteral(expr)) {
600
+ const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
601
+ classNames.push(...addBranchId(addAttributeId(this.expressionExtractor.extract(expr, context))));
602
+ }
603
+ return classNames;
604
+ }
605
+ /**
606
+ * Extract classes from CVA/TV function calls with class override.
607
+ * Handles patterns like: button({ color: 'primary', class: 'invalid-class' })
608
+ *
609
+ * Only extracts from functions that are defined using cva() or tv(),
610
+ * not from arbitrary custom functions.
611
+ */
612
+ extractFromCvaTvClassOverride(callExpr, context, attributeId) {
613
+ const { typescript } = context;
614
+ const classNames = [];
615
+ // Check if this is a __VLS_ctx.functionName pattern
616
+ const calleeExpr = callExpr.expression;
617
+ if (!typescript.isPropertyAccessExpression(calleeExpr)) {
618
+ return classNames;
619
+ }
620
+ const objectExpr = calleeExpr.expression;
621
+ if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== '__VLS_ctx') {
622
+ return classNames;
623
+ }
624
+ const functionName = calleeExpr.name;
625
+ if (!typescript.isIdentifier(functionName)) {
626
+ return classNames;
627
+ }
628
+ // Check if this function is defined using cva() or tv()
629
+ // by looking at its definition
630
+ if (!this.isCvaTvFunction(functionName, context)) {
631
+ return classNames;
632
+ }
633
+ // Look for object argument with class/className property
634
+ if (callExpr.arguments.length === 0) {
635
+ return classNames;
636
+ }
637
+ const firstArg = callExpr.arguments[0];
638
+ if (!typescript.isObjectLiteralExpression(firstArg)) {
639
+ return classNames;
640
+ }
641
+ // Find class or className property
642
+ for (const prop of firstArg.properties) {
643
+ if (!typescript.isPropertyAssignment(prop)) {
644
+ continue;
645
+ }
646
+ const propName = prop.name;
647
+ if (!typescript.isIdentifier(propName)) {
648
+ continue;
649
+ }
650
+ if (propName.text !== 'class' && propName.text !== 'className') {
651
+ continue;
652
+ }
653
+ // Extract classes from the property value
654
+ const value = prop.initializer;
655
+ if (typescript.isStringLiteral(value)) {
656
+ classNames.push(...this.extractClassesFromStringLiteral(value, context, attributeId));
657
+ }
658
+ else if (typescript.isArrayLiteralExpression(value)) {
659
+ classNames.push(...this.extractFromArrayExpression(value, context, attributeId));
660
+ }
661
+ else if (typescript.isTemplateExpression(value) ||
662
+ typescript.isNoSubstitutionTemplateLiteral(value)) {
663
+ const addAttrId = (classes) => classes.map(c => ({ ...c, attributeId }));
664
+ classNames.push(...addAttrId(this.expressionExtractor.extract(value, context)));
665
+ }
666
+ }
667
+ return classNames;
668
+ }
669
+ /**
670
+ * Check if a function is defined using cva() or tv().
671
+ * Resolves the function symbol and checks if its initializer is a cva/tv call.
672
+ */
673
+ isCvaTvFunction(functionName, context) {
674
+ const { typescript, typeChecker } = context;
675
+ if (!typeChecker) {
676
+ return false;
677
+ }
678
+ // Get the symbol for the function name
679
+ const symbol = typeChecker.getSymbolAtLocation(functionName);
680
+ if (!symbol) {
681
+ return false;
682
+ }
683
+ const declarations = symbol.getDeclarations();
684
+ if (!declarations || declarations.length === 0) {
685
+ return false;
686
+ }
687
+ for (const declaration of declarations) {
688
+ // Check PropertySignature -> typeof reference in Volar 3.x
689
+ if (typescript.isPropertySignature(declaration) && declaration.type) {
690
+ if (typescript.isTypeQueryNode(declaration.type)) {
691
+ const exprName = declaration.type.exprName;
692
+ if (typescript.isIdentifier(exprName)) {
693
+ // Resolve the actual variable
694
+ const varSymbol = typeChecker.getSymbolAtLocation(exprName);
695
+ if (varSymbol) {
696
+ const varDeclarations = varSymbol.getDeclarations();
697
+ if (varDeclarations) {
698
+ for (const varDecl of varDeclarations) {
699
+ if (typescript.isVariableDeclaration(varDecl) && varDecl.initializer) {
700
+ if (this.isCallToCvaOrTv(varDecl.initializer, context)) {
701
+ return true;
702
+ }
703
+ }
704
+ }
705
+ }
706
+ }
707
+ }
708
+ }
709
+ }
710
+ // Check direct variable declaration
711
+ else if (typescript.isVariableDeclaration(declaration) && declaration.initializer) {
712
+ if (this.isCallToCvaOrTv(declaration.initializer, context)) {
713
+ return true;
714
+ }
715
+ }
716
+ // Check property assignment in Vue's return
717
+ else if (typescript.isPropertyAssignment(declaration)) {
718
+ let expr = declaration.initializer;
719
+ if (typescript.isAsExpression(expr)) {
720
+ expr = expr.expression;
721
+ }
722
+ if (typescript.isIdentifier(expr)) {
723
+ const refSymbol = typeChecker.getSymbolAtLocation(expr);
724
+ if (refSymbol) {
725
+ const refDeclarations = refSymbol.getDeclarations();
726
+ if (refDeclarations) {
727
+ for (const refDecl of refDeclarations) {
728
+ if (typescript.isVariableDeclaration(refDecl) && refDecl.initializer) {
729
+ if (this.isCallToCvaOrTv(refDecl.initializer, context)) {
730
+ return true;
731
+ }
732
+ }
733
+ }
734
+ }
735
+ }
736
+ }
737
+ }
738
+ }
739
+ return false;
740
+ }
741
+ /**
742
+ * Check if an expression is a call to cva() or tv().
743
+ */
744
+ isCallToCvaOrTv(expr, context) {
745
+ const { typescript } = context;
746
+ if (!typescript.isCallExpression(expr)) {
747
+ return false;
748
+ }
749
+ const callee = expr.expression;
750
+ // Direct call: cva(...) or tv(...)
751
+ if (typescript.isIdentifier(callee)) {
752
+ const name = callee.text;
753
+ return name === 'cva' || name === 'tv' || name === 'tvLite';
754
+ }
755
+ return false;
756
+ }
757
+ /**
758
+ * Extract classes from a string literal with attributeId.
759
+ */
760
+ extractClassesFromStringLiteral(literal, context, attributeId) {
761
+ const classNames = [];
762
+ const fullText = literal.text;
763
+ if (fullText.length === 0) {
764
+ return classNames;
765
+ }
766
+ const stringContentStart = literal.getStart() + 1;
767
+ let offset = 0;
768
+ const parts = fullText.split(/(\s+)/);
769
+ for (const part of parts) {
770
+ if (part && !/^\s+$/.test(part)) {
771
+ classNames.push({
772
+ className: part,
773
+ absoluteStart: stringContentStart + offset,
774
+ length: part.length,
775
+ line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset).line + 1,
776
+ file: context.sourceFile.fileName,
777
+ attributeId
778
+ });
779
+ }
780
+ offset += part.length;
781
+ }
782
+ return classNames;
783
+ }
283
784
  /**
284
785
  * Extract classes from __VLS_ctx patterns by resolving variable/function references.
285
786
  *
@@ -820,8 +1321,11 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
820
1321
  }
821
1322
  /**
822
1323
  * Extract classes from an expression (array, object, string, etc.)
1324
+ * @param expr The expression to extract from
1325
+ * @param context The extraction context
1326
+ * @param attributeId Optional attribute ID for tracking
823
1327
  */
824
- extractFromExpression(expr, context) {
1328
+ extractFromExpression(expr, context, attributeId) {
825
1329
  const { typescript } = context;
826
1330
  const classNames = [];
827
1331
  // String literal: 'flex items-center'
@@ -854,13 +1358,36 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
854
1358
  if (element === undefined)
855
1359
  continue;
856
1360
  if (typescript.isStringLiteral(element)) {
857
- classNames.push(...this.extractFromExpression(element, context));
1361
+ classNames.push(...this.extractFromExpression(element, context, attributeId));
858
1362
  }
859
1363
  else if (typescript.isObjectLiteralExpression(element)) {
860
- classNames.push(...this.extractFromExpression(element, context));
1364
+ classNames.push(...this.extractFromExpression(element, context, attributeId));
861
1365
  }
862
1366
  else if (typescript.isSpreadElement(element)) {
863
- classNames.push(...this.extractFromExpression(element.expression, context));
1367
+ classNames.push(...this.extractFromExpression(element.expression, context, attributeId));
1368
+ }
1369
+ // Handle __VLS_ctx.variable references in arrays
1370
+ else if (typescript.isPropertyAccessExpression(element)) {
1371
+ if (attributeId) {
1372
+ const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
1373
+ if (vlsResults.length > 0) {
1374
+ classNames.push(...vlsResults);
1375
+ }
1376
+ }
1377
+ }
1378
+ // Handle parenthesized expressions: (__VLS_ctx.myVar)
1379
+ else if (typescript.isParenthesizedExpression(element)) {
1380
+ const inner = element.expression;
1381
+ if (typescript.isPropertyAccessExpression(inner) && attributeId) {
1382
+ const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
1383
+ if (vlsResults.length > 0) {
1384
+ classNames.push(...vlsResults);
1385
+ }
1386
+ }
1387
+ }
1388
+ // Handle nested arrays recursively
1389
+ else if (typescript.isArrayLiteralExpression(element)) {
1390
+ classNames.push(...this.extractFromExpression(element, context, attributeId));
864
1391
  }
865
1392
  }
866
1393
  return classNames;
@@ -880,13 +1407,30 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
880
1407
  className = propName.text;
881
1408
  start = propName.getStart();
882
1409
  }
1410
+ // Handle computed property names: { [__VLS_ctx.myVar]: true }
1411
+ else if (typescript.isComputedPropertyName(propName)) {
1412
+ let computedExpr = propName.expression;
1413
+ // Unwrap parentheses: { [(__VLS_ctx.myVar)]: true }
1414
+ if (typescript.isParenthesizedExpression(computedExpr)) {
1415
+ computedExpr = computedExpr.expression;
1416
+ }
1417
+ // Resolve __VLS_ctx.variable pattern
1418
+ if (typescript.isPropertyAccessExpression(computedExpr) && attributeId) {
1419
+ const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
1420
+ if (vlsResults.length > 0) {
1421
+ classNames.push(...vlsResults);
1422
+ continue;
1423
+ }
1424
+ }
1425
+ }
883
1426
  if (className && start !== undefined) {
884
1427
  classNames.push({
885
1428
  className,
886
1429
  absoluteStart: start,
887
1430
  length: className.length,
888
1431
  line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
889
- file: context.sourceFile.fileName
1432
+ file: context.sourceFile.fileName,
1433
+ attributeId
890
1434
  });
891
1435
  }
892
1436
  }
@@ -898,12 +1442,43 @@ class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
898
1442
  absoluteStart: start,
899
1443
  length: className.length,
900
1444
  line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
901
- file: context.sourceFile.fileName
1445
+ file: context.sourceFile.fileName,
1446
+ attributeId
902
1447
  });
903
1448
  }
904
1449
  }
905
1450
  return classNames;
906
1451
  }
1452
+ // Handle conditional (ternary) expressions: isActive ? 'flex' : 'hidden'
1453
+ if (typescript.isConditionalExpression(expr)) {
1454
+ // Extract from both branches recursively
1455
+ classNames.push(...this.extractFromExpression(expr.whenTrue, context, attributeId));
1456
+ classNames.push(...this.extractFromExpression(expr.whenFalse, context, attributeId));
1457
+ return classNames;
1458
+ }
1459
+ // Handle binary expressions: isActive && 'flex', isDisabled || 'fallback'
1460
+ if (typescript.isBinaryExpression(expr)) {
1461
+ classNames.push(...this.extractFromExpression(expr.left, context, attributeId));
1462
+ classNames.push(...this.extractFromExpression(expr.right, context, attributeId));
1463
+ return classNames;
1464
+ }
1465
+ // Handle parenthesized expressions: ('flex items-center')
1466
+ if (typescript.isParenthesizedExpression(expr)) {
1467
+ return this.extractFromExpression(expr.expression, context, attributeId);
1468
+ }
1469
+ // Handle type assertions: 'flex' as string, 'flex' as const
1470
+ if (typescript.isAsExpression(expr)) {
1471
+ return this.extractFromExpression(expr.expression, context, attributeId);
1472
+ }
1473
+ // Handle non-null assertions: someValue!
1474
+ if (typescript.isNonNullExpression(expr)) {
1475
+ return this.extractFromExpression(expr.expression, context, attributeId);
1476
+ }
1477
+ // Handle template literals
1478
+ if (typescript.isTemplateExpression(expr) || typescript.isNoSubstitutionTemplateLiteral(expr)) {
1479
+ const addAttrId = (classes) => attributeId ? classes.map(c => ({ ...c, attributeId })) : classes;
1480
+ return addAttrId(this.expressionExtractor.extract(expr, context));
1481
+ }
907
1482
  return classNames;
908
1483
  }
909
1484
  }