tailwind-typescript-plugin 1.4.1-beta.3 → 1.4.1-beta.31
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/CHANGELOG.md +155 -0
- package/README.md +369 -71
- package/lib/core/interfaces.d.ts +40 -13
- package/lib/core/interfaces.d.ts.map +1 -1
- package/lib/core/types.d.ts +112 -1
- package/lib/core/types.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.d.ts +56 -3
- package/lib/extractors/BaseExtractor.d.ts.map +1 -1
- package/lib/extractors/BaseExtractor.js +194 -5
- package/lib/extractors/BaseExtractor.js.map +1 -1
- package/lib/extractors/BaseExtractor.spec.d.ts +2 -0
- package/lib/extractors/BaseExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/BaseExtractor.spec.js +421 -0
- package/lib/extractors/BaseExtractor.spec.js.map +1 -0
- package/lib/extractors/CvaExtractor.js +1 -1
- package/lib/extractors/CvaExtractor.js.map +1 -1
- package/lib/extractors/CvaExtractor.spec.d.ts +2 -0
- package/lib/extractors/CvaExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/CvaExtractor.spec.js +1177 -0
- package/lib/extractors/CvaExtractor.spec.js.map +1 -0
- package/lib/extractors/ExpressionExtractor.d.ts.map +1 -1
- package/lib/extractors/ExpressionExtractor.js +35 -5
- package/lib/extractors/ExpressionExtractor.js.map +1 -1
- package/lib/extractors/ExpressionExtractor.spec.d.ts +2 -0
- package/lib/extractors/ExpressionExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/ExpressionExtractor.spec.js +316 -0
- package/lib/extractors/ExpressionExtractor.spec.js.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.d.ts +7 -1
- package/lib/extractors/JsxAttributeExtractor.d.ts.map +1 -1
- package/lib/extractors/JsxAttributeExtractor.js +21 -8
- package/lib/extractors/JsxAttributeExtractor.js.map +1 -1
- package/lib/extractors/JsxAttributeExtractor.spec.d.ts +2 -0
- package/lib/extractors/JsxAttributeExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/JsxAttributeExtractor.spec.js +430 -0
- package/lib/extractors/JsxAttributeExtractor.spec.js.map +1 -0
- package/lib/extractors/TailwindVariantsExtractor.d.ts.map +1 -1
- package/lib/extractors/TailwindVariantsExtractor.js +1 -5
- package/lib/extractors/TailwindVariantsExtractor.js.map +1 -1
- package/lib/extractors/TailwindVariantsExtractor.spec.d.ts +2 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.js +1407 -0
- package/lib/extractors/TailwindVariantsExtractor.spec.js.map +1 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.d.ts +2 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.js +240 -0
- package/lib/extractors/TemplateExpressionExtractor.spec.js.map +1 -0
- package/lib/extractors/VariableReferenceExtractor.d.ts.map +1 -1
- package/lib/extractors/VariableReferenceExtractor.js +21 -0
- package/lib/extractors/VariableReferenceExtractor.js.map +1 -1
- package/lib/extractors/VariableReferenceExtractor.spec.d.ts +2 -0
- package/lib/extractors/VariableReferenceExtractor.spec.d.ts.map +1 -0
- package/lib/extractors/VariableReferenceExtractor.spec.js +138 -0
- package/lib/extractors/VariableReferenceExtractor.spec.js.map +1 -0
- package/lib/extractors/VueAttributeExtractor.d.ts +202 -0
- package/lib/extractors/VueAttributeExtractor.d.ts.map +1 -0
- package/lib/extractors/VueAttributeExtractor.js +1691 -0
- package/lib/extractors/VueAttributeExtractor.js.map +1 -0
- package/lib/extractors/VueExpressionExtractor.d.ts +34 -0
- package/lib/extractors/VueExpressionExtractor.d.ts.map +1 -0
- package/lib/extractors/VueExpressionExtractor.js +171 -0
- package/lib/extractors/VueExpressionExtractor.js.map +1 -0
- package/lib/infrastructure/TailwindValidator.css-vars.spec.js +1 -11
- package/lib/infrastructure/TailwindValidator.css-vars.spec.js.map +1 -1
- package/lib/infrastructure/TailwindValidator.d.ts +10 -3
- package/lib/infrastructure/TailwindValidator.d.ts.map +1 -1
- package/lib/infrastructure/TailwindValidator.js +68 -28
- package/lib/infrastructure/TailwindValidator.js.map +1 -1
- package/lib/infrastructure/TailwindValidator.spec.js +50 -17
- package/lib/infrastructure/TailwindValidator.spec.js.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.d.ts +22 -1
- package/lib/plugin/TailwindTypescriptPlugin.d.ts.map +1 -1
- package/lib/plugin/TailwindTypescriptPlugin.js +133 -50
- package/lib/plugin/TailwindTypescriptPlugin.js.map +1 -1
- package/lib/services/ClassNameExtractionService.d.ts +27 -6
- package/lib/services/ClassNameExtractionService.d.ts.map +1 -1
- package/lib/services/ClassNameExtractionService.js +80 -17
- package/lib/services/ClassNameExtractionService.js.map +1 -1
- package/lib/services/ClassNameExtractionService.spec.d.ts +2 -0
- package/lib/services/ClassNameExtractionService.spec.d.ts.map +1 -0
- package/lib/services/ClassNameExtractionService.spec.js +215 -0
- package/lib/services/ClassNameExtractionService.spec.js.map +1 -0
- package/lib/services/CodeActionService.spec.js +1 -2
- package/lib/services/CodeActionService.spec.js.map +1 -1
- package/lib/services/CompletionService.d.ts +121 -0
- package/lib/services/CompletionService.d.ts.map +1 -0
- package/lib/services/CompletionService.js +573 -0
- package/lib/services/CompletionService.js.map +1 -0
- package/lib/services/CompletionService.spec.d.ts +2 -0
- package/lib/services/CompletionService.spec.d.ts.map +1 -0
- package/lib/services/CompletionService.spec.js +1182 -0
- package/lib/services/CompletionService.spec.js.map +1 -0
- package/lib/services/ConfigSchemaValidator.d.ts +40 -0
- package/lib/services/ConfigSchemaValidator.d.ts.map +1 -0
- package/lib/services/ConfigSchemaValidator.js +139 -0
- package/lib/services/ConfigSchemaValidator.js.map +1 -0
- package/lib/services/ConfigSchemaValidator.spec.d.ts +2 -0
- package/lib/services/ConfigSchemaValidator.spec.d.ts.map +1 -0
- package/lib/services/ConfigSchemaValidator.spec.js +344 -0
- package/lib/services/ConfigSchemaValidator.spec.js.map +1 -0
- package/lib/services/ConflictClassDetection.spec.js +53 -5
- package/lib/services/ConflictClassDetection.spec.js.map +1 -1
- package/lib/services/DiagnosticService.d.ts +8 -8
- package/lib/services/DiagnosticService.d.ts.map +1 -1
- package/lib/services/DiagnosticService.js +29 -13
- package/lib/services/DiagnosticService.js.map +1 -1
- package/lib/services/DiagnosticService.spec.d.ts +2 -0
- package/lib/services/DiagnosticService.spec.d.ts.map +1 -0
- package/lib/services/DiagnosticService.spec.js +259 -0
- package/lib/services/DiagnosticService.spec.js.map +1 -0
- package/lib/services/DuplicateClassDetection.spec.js +20 -21
- package/lib/services/DuplicateClassDetection.spec.js.map +1 -1
- package/lib/services/FileDiagnosticCache.spec.d.ts +2 -0
- package/lib/services/FileDiagnosticCache.spec.d.ts.map +1 -0
- package/lib/services/FileDiagnosticCache.spec.js +213 -0
- package/lib/services/FileDiagnosticCache.spec.js.map +1 -0
- package/lib/services/PerformanceCache.spec.d.ts +2 -0
- package/lib/services/PerformanceCache.spec.d.ts.map +1 -0
- package/lib/services/PerformanceCache.spec.js +168 -0
- package/lib/services/PerformanceCache.spec.js.map +1 -0
- package/lib/services/PluginConfigService.d.ts +66 -15
- package/lib/services/PluginConfigService.d.ts.map +1 -1
- package/lib/services/PluginConfigService.js +230 -73
- package/lib/services/PluginConfigService.js.map +1 -1
- package/lib/services/PluginConfigService.spec.d.ts +2 -0
- package/lib/services/PluginConfigService.spec.d.ts.map +1 -0
- package/lib/services/PluginConfigService.spec.js +93 -0
- package/lib/services/PluginConfigService.spec.js.map +1 -0
- package/lib/services/ValidationService.d.ts +7 -5
- package/lib/services/ValidationService.d.ts.map +1 -1
- package/lib/services/ValidationService.js +40 -43
- package/lib/services/ValidationService.js.map +1 -1
- package/lib/services/ValidationService.spec.d.ts +2 -0
- package/lib/services/ValidationService.spec.d.ts.map +1 -0
- package/lib/services/ValidationService.spec.js +289 -0
- package/lib/services/ValidationService.spec.js.map +1 -0
- package/lib/utils/FrameworkDetector.d.ts +23 -0
- package/lib/utils/FrameworkDetector.d.ts.map +1 -0
- package/lib/utils/FrameworkDetector.js +47 -0
- package/lib/utils/FrameworkDetector.js.map +1 -0
- package/lib/utils/FrameworkDetector.spec.d.ts +2 -0
- package/lib/utils/FrameworkDetector.spec.d.ts.map +1 -0
- package/lib/utils/FrameworkDetector.spec.js +67 -0
- package/lib/utils/FrameworkDetector.spec.js.map +1 -0
- package/package.json +11 -4
- package/lib/extractors/StringLiteralExtractor.d.ts +0 -12
- package/lib/extractors/StringLiteralExtractor.d.ts.map +0 -1
- package/lib/extractors/StringLiteralExtractor.js +0 -21
- package/lib/extractors/StringLiteralExtractor.js.map +0 -1
- package/lib/services/ClassNameExtractionService.original.d.ts +0 -20
- package/lib/services/ClassNameExtractionService.original.d.ts.map +0 -1
- package/lib/services/ClassNameExtractionService.original.js +0 -48
- package/lib/services/ClassNameExtractionService.original.js.map +0 -1
|
@@ -0,0 +1,1691 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VueAttributeExtractor = void 0;
|
|
4
|
+
const BaseExtractor_1 = require("./BaseExtractor");
|
|
5
|
+
const VueExpressionExtractor_1 = require("./VueExpressionExtractor");
|
|
6
|
+
/**
|
|
7
|
+
* Extracts class names from Vue template class attributes
|
|
8
|
+
*
|
|
9
|
+
* When @vue/language-tools (Volar) transforms Vue SFC templates, it generates
|
|
10
|
+
* TypeScript code using function calls with object spreads:
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* __VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
|
|
14
|
+
* ...{ class: "flex items-center" },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* For dynamic classes:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* __VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
|
|
21
|
+
* ...{ class: ({ 'bg-red-500': isActive }) },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* This extractor handles these patterns to extract class names.
|
|
26
|
+
*/
|
|
27
|
+
class VueAttributeExtractor extends BaseExtractor_1.BaseExtractor {
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
this.expressionExtractor = new VueExpressionExtractor_1.VueExpressionExtractor();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fast filter: Vue patterns are always CallExpression nodes (~95% node skip rate)
|
|
34
|
+
* Volar transforms templates into: __VLS_asFunctionalElement(...)({...})
|
|
35
|
+
*/
|
|
36
|
+
getNodeFilter() {
|
|
37
|
+
return (node, typescript) => typescript.isCallExpression(node);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Override to handle Vue's __VLS_ctx pattern.
|
|
41
|
+
*
|
|
42
|
+
* Vue generates code like __VLS_ctx.clsx(...) for template expressions
|
|
43
|
+
* where clsx is imported in the script section. We need to check if the
|
|
44
|
+
* function name (not __VLS_ctx) is directly imported.
|
|
45
|
+
*
|
|
46
|
+
* Also handles namespace imports: __VLS_ctx.utils.clsx(...) for `import * as utils from 'clsx'`
|
|
47
|
+
*/
|
|
48
|
+
shouldValidateFunctionCall(callExpression, utilityFunctions, context) {
|
|
49
|
+
// First, check if this is a __VLS_ctx.functionName() or __VLS_ctx.namespace.functionName() pattern
|
|
50
|
+
if (context) {
|
|
51
|
+
const expr = callExpression.expression;
|
|
52
|
+
if (context.typescript.isPropertyAccessExpression(expr)) {
|
|
53
|
+
const objectExpr = expr.expression;
|
|
54
|
+
// Pattern 1: __VLS_ctx.functionName() - direct import
|
|
55
|
+
if (context.typescript.isIdentifier(objectExpr) && objectExpr.text === '__VLS_ctx') {
|
|
56
|
+
const functionName = expr.name.text;
|
|
57
|
+
// Check each utility function configuration
|
|
58
|
+
for (const utilityFunc of utilityFunctions) {
|
|
59
|
+
if (typeof utilityFunc === 'string') {
|
|
60
|
+
if (utilityFunc === functionName) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (utilityFunc.name === functionName) {
|
|
65
|
+
// Check if the function is directly imported from expected module
|
|
66
|
+
if (this.isImportedFrom(functionName, utilityFunc.from, context)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
// Pattern 2: __VLS_ctx.namespace.functionName() - namespace import
|
|
74
|
+
// e.g., import * as utils from 'clsx' -> __VLS_ctx.utils.clsx()
|
|
75
|
+
if (context.typescript.isPropertyAccessExpression(objectExpr)) {
|
|
76
|
+
const namespaceRoot = objectExpr.expression;
|
|
77
|
+
if (context.typescript.isIdentifier(namespaceRoot) &&
|
|
78
|
+
namespaceRoot.text === '__VLS_ctx') {
|
|
79
|
+
const namespaceName = objectExpr.name.text; // e.g., 'utils'
|
|
80
|
+
const functionName = expr.name.text; // e.g., 'clsx'
|
|
81
|
+
// Check each utility function configuration
|
|
82
|
+
for (const utilityFunc of utilityFunctions) {
|
|
83
|
+
if (typeof utilityFunc === 'string') {
|
|
84
|
+
if (utilityFunc === functionName) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (utilityFunc.name === functionName) {
|
|
89
|
+
// Check if namespace is imported from expected module
|
|
90
|
+
if (this.isNamespaceImportedFrom(namespaceName, utilityFunc.from, context)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Fall back to base implementation for non-Vue patterns
|
|
101
|
+
return super.shouldValidateFunctionCall(callExpression, utilityFunctions, context);
|
|
102
|
+
}
|
|
103
|
+
canHandle(node, context) {
|
|
104
|
+
// We handle call expressions that look like Vue's generated element calls
|
|
105
|
+
// Pattern 1 (intrinsic elements): __VLS_asFunctionalElement(...)({ ...{ class: ... } })
|
|
106
|
+
// Pattern 2 (custom components): __VLS_1({ colorStyles: ... }, ...)
|
|
107
|
+
if (!context.typescript.isCallExpression(node)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// Check if the arguments contain an object with class properties
|
|
111
|
+
if (node.arguments.length === 0) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const firstArg = node.arguments[0];
|
|
115
|
+
if (!context.typescript.isObjectLiteralExpression(firstArg)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const expression = node.expression;
|
|
119
|
+
// Pattern 1: Chained call (intrinsic elements)
|
|
120
|
+
// func(...)({...}) where the result of func(...) is called again
|
|
121
|
+
if (context.typescript.isCallExpression(expression)) {
|
|
122
|
+
// Look for spread assignments with class property
|
|
123
|
+
return this.hasClassSpreadProperty(firstArg, context);
|
|
124
|
+
}
|
|
125
|
+
// Pattern 2: Identifier call (custom components)
|
|
126
|
+
// __VLS_N({ classAttribute: ... }, ...)
|
|
127
|
+
if (context.typescript.isIdentifier(expression)) {
|
|
128
|
+
const name = expression.text;
|
|
129
|
+
// Vue generates __VLS_0, __VLS_1, etc. for component instances
|
|
130
|
+
if (name.startsWith('__VLS_')) {
|
|
131
|
+
// Check for direct class attribute properties
|
|
132
|
+
return this.hasClassDirectProperty(firstArg, context);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
hasClassSpreadProperty(obj, context) {
|
|
138
|
+
// Build set of class attribute names to check
|
|
139
|
+
const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
|
|
140
|
+
for (const prop of obj.properties) {
|
|
141
|
+
if (context.typescript.isSpreadAssignment(prop)) {
|
|
142
|
+
const spreadExpr = prop.expression;
|
|
143
|
+
if (context.typescript.isObjectLiteralExpression(spreadExpr)) {
|
|
144
|
+
for (const innerProp of spreadExpr.properties) {
|
|
145
|
+
if (context.typescript.isPropertyAssignment(innerProp)) {
|
|
146
|
+
const name = innerProp.name;
|
|
147
|
+
if (context.typescript.isIdentifier(name) && classAttributeNames.has(name.text)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if an object has direct class attribute properties (for custom components).
|
|
159
|
+
* Vue generates direct properties like: { colorStyles: "bg-blue-500" }
|
|
160
|
+
*/
|
|
161
|
+
hasClassDirectProperty(obj, context) {
|
|
162
|
+
const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
|
|
163
|
+
for (const prop of obj.properties) {
|
|
164
|
+
if (context.typescript.isPropertyAssignment(prop)) {
|
|
165
|
+
const name = prop.name;
|
|
166
|
+
if (context.typescript.isIdentifier(name) && classAttributeNames.has(name.text)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
extract(node, context) {
|
|
174
|
+
const classNames = [];
|
|
175
|
+
if (!context.typescript.isCallExpression(node)) {
|
|
176
|
+
return classNames;
|
|
177
|
+
}
|
|
178
|
+
const firstArg = node.arguments[0];
|
|
179
|
+
if (!firstArg || !context.typescript.isObjectLiteralExpression(firstArg)) {
|
|
180
|
+
return classNames;
|
|
181
|
+
}
|
|
182
|
+
// Build set of class attribute names to check
|
|
183
|
+
const classAttributeNames = new Set(['class', ...(context.classAttributes || [])]);
|
|
184
|
+
// Process all properties in the object literal
|
|
185
|
+
for (const prop of firstArg.properties) {
|
|
186
|
+
// Handle spread assignments: ...{ class: "..." }
|
|
187
|
+
if (context.typescript.isSpreadAssignment(prop)) {
|
|
188
|
+
const spreadExpr = prop.expression;
|
|
189
|
+
if (context.typescript.isObjectLiteralExpression(spreadExpr)) {
|
|
190
|
+
for (const innerProp of spreadExpr.properties) {
|
|
191
|
+
if (!context.typescript.isPropertyAssignment(innerProp)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const name = innerProp.name;
|
|
195
|
+
if (!context.typescript.isIdentifier(name)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Check if this is a class attribute
|
|
199
|
+
if (!classAttributeNames.has(name.text)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const value = innerProp.initializer;
|
|
203
|
+
const attributeId = `${innerProp.getStart()}-${innerProp.getEnd()}`;
|
|
204
|
+
classNames.push(...this.extractClassesFromValue(value, context, attributeId));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Handle direct property assignments: colorStyles: "..."
|
|
209
|
+
else if (context.typescript.isPropertyAssignment(prop)) {
|
|
210
|
+
const name = prop.name;
|
|
211
|
+
if (!context.typescript.isIdentifier(name)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Check if this is a class attribute (custom attributes like colorStyles)
|
|
215
|
+
if (!classAttributeNames.has(name.text)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const value = prop.initializer;
|
|
219
|
+
const attributeId = `${prop.getStart()}-${prop.getEnd()}`;
|
|
220
|
+
classNames.push(...this.extractClassesFromValue(value, context, attributeId));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return classNames;
|
|
224
|
+
}
|
|
225
|
+
extractClassesFromValue(value, context, attributeId) {
|
|
226
|
+
const classNames = [];
|
|
227
|
+
// Static string literal: class: "flex items-center"
|
|
228
|
+
if (context.typescript.isStringLiteral(value)) {
|
|
229
|
+
const fullText = value.text;
|
|
230
|
+
if (fullText.length === 0) {
|
|
231
|
+
return classNames;
|
|
232
|
+
}
|
|
233
|
+
const stringContentStart = value.getStart() + 1;
|
|
234
|
+
let offset = 0;
|
|
235
|
+
const parts = fullText.split(/(\s+)/);
|
|
236
|
+
for (const part of parts) {
|
|
237
|
+
if (part && !/^\s+$/.test(part)) {
|
|
238
|
+
classNames.push({
|
|
239
|
+
className: part,
|
|
240
|
+
absoluteStart: stringContentStart + offset,
|
|
241
|
+
length: part.length,
|
|
242
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset).line +
|
|
243
|
+
1,
|
|
244
|
+
file: context.sourceFile.fileName,
|
|
245
|
+
attributeId
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
offset += part.length;
|
|
249
|
+
}
|
|
250
|
+
return classNames;
|
|
251
|
+
}
|
|
252
|
+
// Object literal for dynamic classes: class: { 'bg-red-500': isActive }
|
|
253
|
+
// Or wrapped in parentheses: class: ({ 'bg-red-500': isActive })
|
|
254
|
+
let objectExpr;
|
|
255
|
+
if (context.typescript.isObjectLiteralExpression(value)) {
|
|
256
|
+
objectExpr = value;
|
|
257
|
+
}
|
|
258
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
259
|
+
const inner = value.expression;
|
|
260
|
+
if (context.typescript.isObjectLiteralExpression(inner)) {
|
|
261
|
+
objectExpr = inner;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (objectExpr) {
|
|
265
|
+
// Use extractFromObjectExpression which handles computed property names
|
|
266
|
+
return this.extractFromObjectExpression(objectExpr, context, attributeId);
|
|
267
|
+
}
|
|
268
|
+
// Array literal: class: ['flex', 'items-center']
|
|
269
|
+
// Vue wraps expressions in parentheses: class: (['flex', 'items-center'])
|
|
270
|
+
let arrayExpr;
|
|
271
|
+
if (context.typescript.isArrayLiteralExpression(value)) {
|
|
272
|
+
arrayExpr = value;
|
|
273
|
+
}
|
|
274
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
275
|
+
const inner = value.expression;
|
|
276
|
+
if (context.typescript.isArrayLiteralExpression(inner)) {
|
|
277
|
+
arrayExpr = inner;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (arrayExpr) {
|
|
281
|
+
// Process array elements directly to handle __VLS_ctx references
|
|
282
|
+
return this.extractFromArrayExpression(arrayExpr, context, attributeId);
|
|
283
|
+
}
|
|
284
|
+
// Template literal or other expressions - delegate to expression extractor
|
|
285
|
+
// Vue wraps expressions in parentheses: class: (`flex items-center`)
|
|
286
|
+
let templateExpr;
|
|
287
|
+
if (context.typescript.isTemplateExpression(value) ||
|
|
288
|
+
context.typescript.isNoSubstitutionTemplateLiteral(value)) {
|
|
289
|
+
templateExpr = value;
|
|
290
|
+
}
|
|
291
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
292
|
+
const inner = value.expression;
|
|
293
|
+
if (context.typescript.isTemplateExpression(inner) ||
|
|
294
|
+
context.typescript.isNoSubstitutionTemplateLiteral(inner)) {
|
|
295
|
+
templateExpr = inner;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (templateExpr) {
|
|
299
|
+
const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
300
|
+
return addAttributeId(this.expressionExtractor.extract(templateExpr, context));
|
|
301
|
+
}
|
|
302
|
+
// Call expression (utility functions like cn, clsx)
|
|
303
|
+
// Vue wraps expressions in parentheses: class: (__VLS_ctx.clsx(...))
|
|
304
|
+
let callExpr;
|
|
305
|
+
if (context.typescript.isCallExpression(value)) {
|
|
306
|
+
callExpr = value;
|
|
307
|
+
}
|
|
308
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
309
|
+
const inner = value.expression;
|
|
310
|
+
if (context.typescript.isCallExpression(inner)) {
|
|
311
|
+
callExpr = inner;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (callExpr) {
|
|
315
|
+
// Check if it's a utility function (clsx, cn, etc.) - extract all arguments
|
|
316
|
+
if (this.shouldValidateFunctionCall(callExpr, context.utilityFunctions, context)) {
|
|
317
|
+
const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
318
|
+
return addAttributeId(this.expressionExtractor.extract(callExpr, context));
|
|
319
|
+
}
|
|
320
|
+
// Check for CVA/TV function calls with class override: button({ class: '...' })
|
|
321
|
+
// These are __VLS_ctx.functionName({ class: '...' }) patterns
|
|
322
|
+
const classOverrideClasses = this.extractFromCvaTvClassOverride(callExpr, context, attributeId);
|
|
323
|
+
if (classOverrideClasses.length > 0) {
|
|
324
|
+
return classOverrideClasses;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Handle conditional (ternary) expressions: class: (isActive ? 'flex' : 'hidden')
|
|
328
|
+
// Vue wraps expressions in parentheses: class: (__VLS_ctx.isActive ? 'active' : 'inactive')
|
|
329
|
+
let conditionalExpr;
|
|
330
|
+
if (context.typescript.isConditionalExpression(value)) {
|
|
331
|
+
conditionalExpr = value;
|
|
332
|
+
}
|
|
333
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
334
|
+
const inner = value.expression;
|
|
335
|
+
if (context.typescript.isConditionalExpression(inner)) {
|
|
336
|
+
conditionalExpr = inner;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (conditionalExpr) {
|
|
340
|
+
return this.extractFromConditionalExpression(conditionalExpr, context, attributeId);
|
|
341
|
+
}
|
|
342
|
+
// Handle binary expressions: class: (isActive && 'flex')
|
|
343
|
+
// Vue wraps expressions in parentheses: class: (__VLS_ctx.isActive && 'active')
|
|
344
|
+
let binaryExpr;
|
|
345
|
+
if (context.typescript.isBinaryExpression(value)) {
|
|
346
|
+
binaryExpr = value;
|
|
347
|
+
}
|
|
348
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
349
|
+
const inner = value.expression;
|
|
350
|
+
if (context.typescript.isBinaryExpression(inner)) {
|
|
351
|
+
binaryExpr = inner;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (binaryExpr) {
|
|
355
|
+
return this.extractFromBinaryExpression(binaryExpr, context, attributeId);
|
|
356
|
+
}
|
|
357
|
+
// Handle type assertions: class: ('invalid-class' as string)
|
|
358
|
+
// Vue wraps expressions: class: (('invalid-class' as string))
|
|
359
|
+
let asExpr;
|
|
360
|
+
if (context.typescript.isAsExpression(value)) {
|
|
361
|
+
asExpr = value;
|
|
362
|
+
}
|
|
363
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
364
|
+
let inner = value.expression;
|
|
365
|
+
// Double unwrap for nested parentheses
|
|
366
|
+
if (context.typescript.isParenthesizedExpression(inner)) {
|
|
367
|
+
inner = inner.expression;
|
|
368
|
+
}
|
|
369
|
+
if (context.typescript.isAsExpression(inner)) {
|
|
370
|
+
asExpr = inner;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (asExpr) {
|
|
374
|
+
return this.extractClassesFromValue(asExpr.expression, context, attributeId);
|
|
375
|
+
}
|
|
376
|
+
// Handle non-null assertions: class: (someClass!)
|
|
377
|
+
let nonNullExpr;
|
|
378
|
+
if (context.typescript.isNonNullExpression(value)) {
|
|
379
|
+
nonNullExpr = value;
|
|
380
|
+
}
|
|
381
|
+
else if (context.typescript.isParenthesizedExpression(value)) {
|
|
382
|
+
let inner = value.expression;
|
|
383
|
+
if (context.typescript.isParenthesizedExpression(inner)) {
|
|
384
|
+
inner = inner.expression;
|
|
385
|
+
}
|
|
386
|
+
if (context.typescript.isNonNullExpression(inner)) {
|
|
387
|
+
nonNullExpr = inner;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (nonNullExpr) {
|
|
391
|
+
return this.extractClassesFromValue(nonNullExpr.expression, context, attributeId);
|
|
392
|
+
}
|
|
393
|
+
// Handle __VLS_ctx.propertyName patterns for variable/computed/function references
|
|
394
|
+
// Vue generates: class: (__VLS_ctx.myClass) for :class="myClass"
|
|
395
|
+
const resolvedClasses = this.extractFromVlsCtxReference(value, context, attributeId);
|
|
396
|
+
if (resolvedClasses.length > 0) {
|
|
397
|
+
return resolvedClasses;
|
|
398
|
+
}
|
|
399
|
+
// Handle props.propertyName patterns with default values from withDefaults
|
|
400
|
+
// Vue generates: class: (props.buttonClass) for :class="props.buttonClass"
|
|
401
|
+
const propsDefaultClasses = this.extractFromPropsWithDefaults(value, context, attributeId);
|
|
402
|
+
if (propsDefaultClasses.length > 0) {
|
|
403
|
+
return propsDefaultClasses;
|
|
404
|
+
}
|
|
405
|
+
return classNames;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Extract classes from an array expression, handling __VLS_ctx references.
|
|
409
|
+
* This method processes array elements directly to support variable references.
|
|
410
|
+
*/
|
|
411
|
+
extractFromArrayExpression(arrayExpr, context, attributeId) {
|
|
412
|
+
const { typescript } = context;
|
|
413
|
+
const classNames = [];
|
|
414
|
+
for (const element of arrayExpr.elements) {
|
|
415
|
+
if (element === undefined)
|
|
416
|
+
continue;
|
|
417
|
+
// String literal: 'flex'
|
|
418
|
+
if (typescript.isStringLiteral(element)) {
|
|
419
|
+
const fullText = element.text;
|
|
420
|
+
if (fullText.length > 0) {
|
|
421
|
+
const stringContentStart = element.getStart() + 1;
|
|
422
|
+
let offset = 0;
|
|
423
|
+
const parts = fullText.split(/(\s+)/);
|
|
424
|
+
for (const part of parts) {
|
|
425
|
+
if (part && !/^\s+$/.test(part)) {
|
|
426
|
+
classNames.push({
|
|
427
|
+
className: part,
|
|
428
|
+
absoluteStart: stringContentStart + offset,
|
|
429
|
+
length: part.length,
|
|
430
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset)
|
|
431
|
+
.line + 1,
|
|
432
|
+
file: context.sourceFile.fileName,
|
|
433
|
+
attributeId
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
offset += part.length;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Object literal: { 'bg-red-500': isActive }
|
|
441
|
+
else if (typescript.isObjectLiteralExpression(element)) {
|
|
442
|
+
classNames.push(...this.extractFromObjectExpression(element, context, attributeId));
|
|
443
|
+
}
|
|
444
|
+
// Spread element: ...classes
|
|
445
|
+
else if (typescript.isSpreadElement(element)) {
|
|
446
|
+
// Try to resolve __VLS_ctx reference
|
|
447
|
+
const vlsResults = this.extractFromVlsCtxReference(element.expression, context, attributeId);
|
|
448
|
+
if (vlsResults.length > 0) {
|
|
449
|
+
classNames.push(...vlsResults);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Handle __VLS_ctx.variable references: __VLS_ctx.myClass
|
|
453
|
+
else if (typescript.isPropertyAccessExpression(element)) {
|
|
454
|
+
const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
|
|
455
|
+
if (vlsResults.length > 0) {
|
|
456
|
+
classNames.push(...vlsResults);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// Handle parenthesized expressions: (__VLS_ctx.myVar)
|
|
460
|
+
else if (typescript.isParenthesizedExpression(element)) {
|
|
461
|
+
const inner = element.expression;
|
|
462
|
+
if (typescript.isPropertyAccessExpression(inner)) {
|
|
463
|
+
const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
|
|
464
|
+
if (vlsResults.length > 0) {
|
|
465
|
+
classNames.push(...vlsResults);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (typescript.isArrayLiteralExpression(inner)) {
|
|
469
|
+
// Nested array in parentheses
|
|
470
|
+
classNames.push(...this.extractFromArrayExpression(inner, context, attributeId));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Handle nested arrays recursively
|
|
474
|
+
else if (typescript.isArrayLiteralExpression(element)) {
|
|
475
|
+
classNames.push(...this.extractFromArrayExpression(element, context, attributeId));
|
|
476
|
+
}
|
|
477
|
+
// Handle ternary/conditional expressions
|
|
478
|
+
else if (typescript.isConditionalExpression(element)) {
|
|
479
|
+
// Extract from both branches
|
|
480
|
+
classNames.push(...this.extractFromConditionalElement(element, context, attributeId));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return classNames;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Extract classes from an object expression, handling computed property names.
|
|
487
|
+
*/
|
|
488
|
+
extractFromObjectExpression(objExpr, context, attributeId) {
|
|
489
|
+
const { typescript } = context;
|
|
490
|
+
const classNames = [];
|
|
491
|
+
for (const prop of objExpr.properties) {
|
|
492
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
493
|
+
const propName = prop.name;
|
|
494
|
+
let className;
|
|
495
|
+
let start;
|
|
496
|
+
if (typescript.isStringLiteral(propName)) {
|
|
497
|
+
className = propName.text;
|
|
498
|
+
start = propName.getStart() + 1;
|
|
499
|
+
}
|
|
500
|
+
else if (typescript.isIdentifier(propName)) {
|
|
501
|
+
className = propName.text;
|
|
502
|
+
start = propName.getStart();
|
|
503
|
+
}
|
|
504
|
+
// Handle computed property names: { [__VLS_ctx.myVar]: true }
|
|
505
|
+
else if (typescript.isComputedPropertyName(propName)) {
|
|
506
|
+
let computedExpr = propName.expression;
|
|
507
|
+
// Unwrap parentheses
|
|
508
|
+
if (typescript.isParenthesizedExpression(computedExpr)) {
|
|
509
|
+
computedExpr = computedExpr.expression;
|
|
510
|
+
}
|
|
511
|
+
// Resolve __VLS_ctx.variable pattern
|
|
512
|
+
if (typescript.isPropertyAccessExpression(computedExpr)) {
|
|
513
|
+
const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
|
|
514
|
+
if (vlsResults.length > 0) {
|
|
515
|
+
classNames.push(...vlsResults);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (className && start !== undefined) {
|
|
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
|
+
else if (typescript.isShorthandPropertyAssignment(prop)) {
|
|
532
|
+
const className = prop.name.text;
|
|
533
|
+
const start = prop.name.getStart();
|
|
534
|
+
classNames.push({
|
|
535
|
+
className,
|
|
536
|
+
absoluteStart: start,
|
|
537
|
+
length: className.length,
|
|
538
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
539
|
+
file: context.sourceFile.fileName,
|
|
540
|
+
attributeId
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return classNames;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Extract classes from a conditional (ternary) expression in array context.
|
|
548
|
+
*/
|
|
549
|
+
extractFromConditionalElement(conditional, context, attributeId) {
|
|
550
|
+
return this.extractFromConditionalExpression(conditional, context, attributeId);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Extract classes from a conditional (ternary) expression.
|
|
554
|
+
* Handles: isActive ? 'flex' : 'hidden'
|
|
555
|
+
*/
|
|
556
|
+
extractFromConditionalExpression(conditional, context, attributeId) {
|
|
557
|
+
const classNames = [];
|
|
558
|
+
// Use the ternary's position as a unique identifier (like ExpressionExtractor)
|
|
559
|
+
const ternaryId = conditional.getStart();
|
|
560
|
+
// Extract from true branch with conditionalBranchId
|
|
561
|
+
const whenTrue = conditional.whenTrue;
|
|
562
|
+
classNames.push(...this.extractFromBranchExpression(whenTrue, context, attributeId, `ternary:true:${ternaryId}`));
|
|
563
|
+
// Extract from false branch with conditionalBranchId
|
|
564
|
+
const whenFalse = conditional.whenFalse;
|
|
565
|
+
classNames.push(...this.extractFromBranchExpression(whenFalse, context, attributeId, `ternary:false:${ternaryId}`));
|
|
566
|
+
return classNames;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Extract classes from a binary expression.
|
|
570
|
+
* Handles: isActive && 'flex', isDisabled || 'fallback'
|
|
571
|
+
*/
|
|
572
|
+
extractFromBinaryExpression(binary, context, attributeId) {
|
|
573
|
+
const classNames = [];
|
|
574
|
+
// Extract from left operand (for patterns like: 'flex' || fallback)
|
|
575
|
+
classNames.push(...this.extractFromBranchExpression(binary.left, context, attributeId));
|
|
576
|
+
// Extract from right operand (for patterns like: isActive && 'flex')
|
|
577
|
+
classNames.push(...this.extractFromBranchExpression(binary.right, context, attributeId));
|
|
578
|
+
return classNames;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Extract classes from a branch expression (ternary branch or binary operand).
|
|
582
|
+
* Handles string literals, nested ternaries, nested binaries, and VLS references.
|
|
583
|
+
*/
|
|
584
|
+
extractFromBranchExpression(expr, context, attributeId, conditionalBranchId) {
|
|
585
|
+
const { typescript } = context;
|
|
586
|
+
const classNames = [];
|
|
587
|
+
// Helper to add conditionalBranchId to extracted classes
|
|
588
|
+
const addBranchId = (classes) => conditionalBranchId ? classes.map(c => ({ ...c, conditionalBranchId })) : classes;
|
|
589
|
+
// String literal: 'flex items-center'
|
|
590
|
+
if (typescript.isStringLiteral(expr)) {
|
|
591
|
+
classNames.push(...addBranchId(this.extractClassesFromStringLiteral(expr, context, attributeId)));
|
|
592
|
+
}
|
|
593
|
+
// Nested ternary: condition ? 'a' : (nested ? 'b' : 'c')
|
|
594
|
+
else if (typescript.isConditionalExpression(expr)) {
|
|
595
|
+
classNames.push(...this.extractFromConditionalExpression(expr, context, attributeId));
|
|
596
|
+
}
|
|
597
|
+
// Nested binary: isA && isB && 'class'
|
|
598
|
+
else if (typescript.isBinaryExpression(expr)) {
|
|
599
|
+
classNames.push(...addBranchId(this.extractFromBinaryExpression(expr, context, attributeId)));
|
|
600
|
+
}
|
|
601
|
+
// Parenthesized expression
|
|
602
|
+
else if (typescript.isParenthesizedExpression(expr)) {
|
|
603
|
+
classNames.push(...this.extractFromBranchExpression(expr.expression, context, attributeId, conditionalBranchId));
|
|
604
|
+
}
|
|
605
|
+
// VLS ctx reference: __VLS_ctx.myClass
|
|
606
|
+
else if (typescript.isPropertyAccessExpression(expr)) {
|
|
607
|
+
const vlsResults = this.extractFromVlsCtxReference(expr, context, attributeId);
|
|
608
|
+
classNames.push(...addBranchId(vlsResults));
|
|
609
|
+
}
|
|
610
|
+
// Template literal: `flex ${something}`
|
|
611
|
+
else if (typescript.isTemplateExpression(expr) ||
|
|
612
|
+
typescript.isNoSubstitutionTemplateLiteral(expr)) {
|
|
613
|
+
const addAttributeId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
614
|
+
classNames.push(...addBranchId(addAttributeId(this.expressionExtractor.extract(expr, context))));
|
|
615
|
+
}
|
|
616
|
+
return classNames;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Extract classes from CVA/TV function calls with class override.
|
|
620
|
+
* Handles patterns like: button({ color: 'primary', class: 'invalid-class' })
|
|
621
|
+
*
|
|
622
|
+
* Only extracts from functions that are defined using cva() or tv(),
|
|
623
|
+
* not from arbitrary custom functions.
|
|
624
|
+
*/
|
|
625
|
+
extractFromCvaTvClassOverride(callExpr, context, attributeId) {
|
|
626
|
+
const { typescript } = context;
|
|
627
|
+
const classNames = [];
|
|
628
|
+
// Check if this is a __VLS_ctx.functionName pattern
|
|
629
|
+
const calleeExpr = callExpr.expression;
|
|
630
|
+
if (!typescript.isPropertyAccessExpression(calleeExpr)) {
|
|
631
|
+
return classNames;
|
|
632
|
+
}
|
|
633
|
+
const objectExpr = calleeExpr.expression;
|
|
634
|
+
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== '__VLS_ctx') {
|
|
635
|
+
return classNames;
|
|
636
|
+
}
|
|
637
|
+
const functionName = calleeExpr.name;
|
|
638
|
+
if (!typescript.isIdentifier(functionName)) {
|
|
639
|
+
return classNames;
|
|
640
|
+
}
|
|
641
|
+
// Check if this function is defined using cva() or tv()
|
|
642
|
+
// by looking at its definition
|
|
643
|
+
if (!this.isCvaTvFunction(functionName, context)) {
|
|
644
|
+
return classNames;
|
|
645
|
+
}
|
|
646
|
+
// Look for object argument with class/className property
|
|
647
|
+
if (callExpr.arguments.length === 0) {
|
|
648
|
+
return classNames;
|
|
649
|
+
}
|
|
650
|
+
const firstArg = callExpr.arguments[0];
|
|
651
|
+
if (!typescript.isObjectLiteralExpression(firstArg)) {
|
|
652
|
+
return classNames;
|
|
653
|
+
}
|
|
654
|
+
// Find class or className property
|
|
655
|
+
for (const prop of firstArg.properties) {
|
|
656
|
+
if (!typescript.isPropertyAssignment(prop)) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
const propName = prop.name;
|
|
660
|
+
if (!typescript.isIdentifier(propName)) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (propName.text !== 'class' && propName.text !== 'className') {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
// Extract classes from the property value
|
|
667
|
+
const value = prop.initializer;
|
|
668
|
+
if (typescript.isStringLiteral(value)) {
|
|
669
|
+
classNames.push(...this.extractClassesFromStringLiteral(value, context, attributeId));
|
|
670
|
+
}
|
|
671
|
+
else if (typescript.isArrayLiteralExpression(value)) {
|
|
672
|
+
classNames.push(...this.extractFromArrayExpression(value, context, attributeId));
|
|
673
|
+
}
|
|
674
|
+
else if (typescript.isTemplateExpression(value) ||
|
|
675
|
+
typescript.isNoSubstitutionTemplateLiteral(value)) {
|
|
676
|
+
const addAttrId = (classes) => classes.map(c => ({ ...c, attributeId }));
|
|
677
|
+
classNames.push(...addAttrId(this.expressionExtractor.extract(value, context)));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return classNames;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Check if a function is defined using cva() or tv().
|
|
684
|
+
* Resolves the function symbol and checks if its initializer is a cva/tv call.
|
|
685
|
+
*/
|
|
686
|
+
isCvaTvFunction(functionName, context) {
|
|
687
|
+
const { typescript, typeChecker } = context;
|
|
688
|
+
if (!typeChecker) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
// Get the symbol for the function name
|
|
692
|
+
const symbol = typeChecker.getSymbolAtLocation(functionName);
|
|
693
|
+
if (!symbol) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
const declarations = symbol.getDeclarations();
|
|
697
|
+
if (!declarations || declarations.length === 0) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
for (const declaration of declarations) {
|
|
701
|
+
// Check PropertySignature -> typeof reference in Volar 3.x
|
|
702
|
+
if (typescript.isPropertySignature(declaration) && declaration.type) {
|
|
703
|
+
if (typescript.isTypeQueryNode(declaration.type)) {
|
|
704
|
+
const exprName = declaration.type.exprName;
|
|
705
|
+
if (typescript.isIdentifier(exprName)) {
|
|
706
|
+
// Resolve the actual variable
|
|
707
|
+
const varSymbol = typeChecker.getSymbolAtLocation(exprName);
|
|
708
|
+
if (varSymbol) {
|
|
709
|
+
const varDeclarations = varSymbol.getDeclarations();
|
|
710
|
+
if (varDeclarations) {
|
|
711
|
+
for (const varDecl of varDeclarations) {
|
|
712
|
+
if (typescript.isVariableDeclaration(varDecl) && varDecl.initializer) {
|
|
713
|
+
if (this.isCallToCvaOrTv(varDecl.initializer, context)) {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Check direct variable declaration
|
|
724
|
+
else if (typescript.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
725
|
+
if (this.isCallToCvaOrTv(declaration.initializer, context)) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Check property assignment in Vue's return
|
|
730
|
+
else if (typescript.isPropertyAssignment(declaration)) {
|
|
731
|
+
let expr = declaration.initializer;
|
|
732
|
+
if (typescript.isAsExpression(expr)) {
|
|
733
|
+
expr = expr.expression;
|
|
734
|
+
}
|
|
735
|
+
if (typescript.isIdentifier(expr)) {
|
|
736
|
+
const refSymbol = typeChecker.getSymbolAtLocation(expr);
|
|
737
|
+
if (refSymbol) {
|
|
738
|
+
const refDeclarations = refSymbol.getDeclarations();
|
|
739
|
+
if (refDeclarations) {
|
|
740
|
+
for (const refDecl of refDeclarations) {
|
|
741
|
+
if (typescript.isVariableDeclaration(refDecl) && refDecl.initializer) {
|
|
742
|
+
if (this.isCallToCvaOrTv(refDecl.initializer, context)) {
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Check if an expression is a call to cva() or tv().
|
|
756
|
+
*/
|
|
757
|
+
isCallToCvaOrTv(expr, context) {
|
|
758
|
+
const { typescript } = context;
|
|
759
|
+
if (!typescript.isCallExpression(expr)) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
const callee = expr.expression;
|
|
763
|
+
// Direct call: cva(...) or tv(...)
|
|
764
|
+
if (typescript.isIdentifier(callee)) {
|
|
765
|
+
const name = callee.text;
|
|
766
|
+
return name === 'cva' || name === 'tv' || name === 'tvLite';
|
|
767
|
+
}
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Extract classes from a string literal with attributeId.
|
|
772
|
+
*/
|
|
773
|
+
extractClassesFromStringLiteral(literal, context, attributeId) {
|
|
774
|
+
const classNames = [];
|
|
775
|
+
const fullText = literal.text;
|
|
776
|
+
if (fullText.length === 0) {
|
|
777
|
+
return classNames;
|
|
778
|
+
}
|
|
779
|
+
const stringContentStart = literal.getStart() + 1;
|
|
780
|
+
let offset = 0;
|
|
781
|
+
const parts = fullText.split(/(\s+)/);
|
|
782
|
+
for (const part of parts) {
|
|
783
|
+
if (part && !/^\s+$/.test(part)) {
|
|
784
|
+
classNames.push({
|
|
785
|
+
className: part,
|
|
786
|
+
absoluteStart: stringContentStart + offset,
|
|
787
|
+
length: part.length,
|
|
788
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset).line + 1,
|
|
789
|
+
file: context.sourceFile.fileName,
|
|
790
|
+
attributeId
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
offset += part.length;
|
|
794
|
+
}
|
|
795
|
+
return classNames;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Extract classes from __VLS_ctx patterns by resolving variable/function references.
|
|
799
|
+
*
|
|
800
|
+
* Vue's generated code transforms template expressions in two ways:
|
|
801
|
+
* - :class="myClass" becomes class: (__VLS_ctx.myClass) - property access
|
|
802
|
+
* - :class="getClasses()" becomes class: (__VLS_ctx.getClasses()) - call expression
|
|
803
|
+
*
|
|
804
|
+
* The symbol resolves to a property assignment in Vue's generated code like:
|
|
805
|
+
* `return { myClass: myClass as typeof myClass }` or
|
|
806
|
+
* `return { getClasses: getClasses as typeof getClasses }`
|
|
807
|
+
* We need to follow the reference chain to find the actual declaration.
|
|
808
|
+
*
|
|
809
|
+
* IMPORTANT: We use the TEMPLATE position (the propertyName in __VLS_ctx.propertyName)
|
|
810
|
+
* for diagnostics because Volar only maps diagnostics from the template-generated section,
|
|
811
|
+
* not from the script section. Script section positions have valid mappings but Volar
|
|
812
|
+
* doesn't apply them for diagnostics.
|
|
813
|
+
*/
|
|
814
|
+
extractFromVlsCtxReference(value, context, attributeId) {
|
|
815
|
+
const { typescript, typeChecker } = context;
|
|
816
|
+
if (!typeChecker) {
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
// Unwrap parenthesized expression if present
|
|
820
|
+
let expr = value;
|
|
821
|
+
if (typescript.isParenthesizedExpression(expr)) {
|
|
822
|
+
expr = expr.expression;
|
|
823
|
+
}
|
|
824
|
+
// Handle call expressions: __VLS_ctx.getClasses()
|
|
825
|
+
if (typescript.isCallExpression(expr)) {
|
|
826
|
+
const calleeExpr = expr.expression;
|
|
827
|
+
if (typescript.isPropertyAccessExpression(calleeExpr)) {
|
|
828
|
+
const objectExpr = calleeExpr.expression;
|
|
829
|
+
if (typescript.isIdentifier(objectExpr) && objectExpr.text === '__VLS_ctx') {
|
|
830
|
+
const functionName = calleeExpr.name;
|
|
831
|
+
if (typescript.isIdentifier(functionName)) {
|
|
832
|
+
// Use the function name position in template for diagnostics
|
|
833
|
+
const templatePosition = functionName.getStart();
|
|
834
|
+
const templateLength = functionName.text.length;
|
|
835
|
+
return this.extractFromVlsCtxFunctionCall(functionName, context, attributeId, templatePosition, templateLength);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return [];
|
|
840
|
+
}
|
|
841
|
+
// Check if this is a __VLS_ctx.propertyName pattern (property access)
|
|
842
|
+
if (!typescript.isPropertyAccessExpression(expr)) {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
const objectExpr = expr.expression;
|
|
846
|
+
// Handle nested property access: __VLS_ctx.obj.property
|
|
847
|
+
// e.g., __VLS_ctx.slotProps.buttonClass
|
|
848
|
+
if (typescript.isPropertyAccessExpression(objectExpr)) {
|
|
849
|
+
const nestedObject = objectExpr.expression;
|
|
850
|
+
if (typescript.isIdentifier(nestedObject) && nestedObject.text === '__VLS_ctx') {
|
|
851
|
+
// This is __VLS_ctx.something.somethingElse
|
|
852
|
+
const middlePropName = objectExpr.name;
|
|
853
|
+
const finalPropName = expr.name;
|
|
854
|
+
if (typescript.isIdentifier(middlePropName) && typescript.isIdentifier(finalPropName)) {
|
|
855
|
+
// Resolve the middle property (e.g., slotProps) to get its type/value
|
|
856
|
+
const middleSymbol = typeChecker.getSymbolAtLocation(middlePropName);
|
|
857
|
+
if (middleSymbol) {
|
|
858
|
+
const middleDeclarations = middleSymbol.getDeclarations();
|
|
859
|
+
if (middleDeclarations) {
|
|
860
|
+
for (const middleDecl of middleDeclarations) {
|
|
861
|
+
// Handle variable declaration: const slotProps = { buttonClass: '...' }
|
|
862
|
+
if (typescript.isVariableDeclaration(middleDecl) && middleDecl.initializer) {
|
|
863
|
+
if (typescript.isObjectLiteralExpression(middleDecl.initializer)) {
|
|
864
|
+
// Find the property in the object literal
|
|
865
|
+
for (const prop of middleDecl.initializer.properties) {
|
|
866
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
867
|
+
const propName = prop.name;
|
|
868
|
+
if (typescript.isIdentifier(propName) &&
|
|
869
|
+
propName.text === finalPropName.text) {
|
|
870
|
+
// Found the property, extract classes from its value
|
|
871
|
+
// Keep original positions from the class string
|
|
872
|
+
const classes = this.extractFromExpression(prop.initializer, context, attributeId);
|
|
873
|
+
return classes.map(c => ({
|
|
874
|
+
...c,
|
|
875
|
+
attributeId
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Handle property signature in Volar's generated types
|
|
883
|
+
else if (typescript.isPropertySignature(middleDecl) && middleDecl.type) {
|
|
884
|
+
if (typescript.isTypeQueryNode(middleDecl.type)) {
|
|
885
|
+
const exprName = middleDecl.type.exprName;
|
|
886
|
+
if (typescript.isIdentifier(exprName)) {
|
|
887
|
+
const varSymbol = typeChecker.getSymbolAtLocation(exprName);
|
|
888
|
+
if (varSymbol) {
|
|
889
|
+
const varDeclarations = varSymbol.getDeclarations();
|
|
890
|
+
if (varDeclarations) {
|
|
891
|
+
for (const varDecl of varDeclarations) {
|
|
892
|
+
if (typescript.isVariableDeclaration(varDecl) &&
|
|
893
|
+
varDecl.initializer &&
|
|
894
|
+
typescript.isObjectLiteralExpression(varDecl.initializer)) {
|
|
895
|
+
// Find the property
|
|
896
|
+
for (const prop of varDecl.initializer.properties) {
|
|
897
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
898
|
+
const pName = prop.name;
|
|
899
|
+
if (typescript.isIdentifier(pName) &&
|
|
900
|
+
pName.text === finalPropName.text) {
|
|
901
|
+
// Keep original positions from the class string
|
|
902
|
+
const classes = this.extractFromExpression(prop.initializer, context, attributeId);
|
|
903
|
+
return classes.map(c => ({
|
|
904
|
+
...c,
|
|
905
|
+
attributeId
|
|
906
|
+
}));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return [];
|
|
923
|
+
}
|
|
924
|
+
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== '__VLS_ctx') {
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
// Get the property name identifier (e.g., 'myClass' from __VLS_ctx.myClass)
|
|
928
|
+
const propertyName = expr.name;
|
|
929
|
+
// Vue's generated code uses regular identifiers, not private identifiers
|
|
930
|
+
if (!typescript.isIdentifier(propertyName)) {
|
|
931
|
+
return [];
|
|
932
|
+
}
|
|
933
|
+
// IMPORTANT: Use the template position (propertyName) for diagnostics
|
|
934
|
+
// This position has a valid Volar mapping that will be applied in the IDE
|
|
935
|
+
const templatePosition = propertyName.getStart();
|
|
936
|
+
const templateLength = propertyName.text.length;
|
|
937
|
+
// Use the type checker to resolve the symbol
|
|
938
|
+
const symbol = typeChecker.getSymbolAtLocation(propertyName);
|
|
939
|
+
if (!symbol) {
|
|
940
|
+
return [];
|
|
941
|
+
}
|
|
942
|
+
const declarations = symbol.getDeclarations();
|
|
943
|
+
if (!declarations || declarations.length === 0) {
|
|
944
|
+
return [];
|
|
945
|
+
}
|
|
946
|
+
const classNames = [];
|
|
947
|
+
// Keep original positions - diagnostic will point to where class is defined
|
|
948
|
+
const addAttributeId = (classes) => classes.map(c => ({
|
|
949
|
+
...c,
|
|
950
|
+
attributeId
|
|
951
|
+
}));
|
|
952
|
+
for (const declaration of declarations) {
|
|
953
|
+
// Handle variable declarations: const myClass = 'flex items-center'
|
|
954
|
+
if (typescript.isVariableDeclaration(declaration)) {
|
|
955
|
+
const initializer = declaration.initializer;
|
|
956
|
+
if (initializer) {
|
|
957
|
+
// Check if this is a computed() or inject() call
|
|
958
|
+
if (typescript.isCallExpression(initializer)) {
|
|
959
|
+
// Handle computed() calls
|
|
960
|
+
const computedClasses = this.extractFromComputedCall(initializer, context, attributeId, templatePosition, templateLength);
|
|
961
|
+
if (computedClasses.length > 0) {
|
|
962
|
+
classNames.push(...computedClasses);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
// Handle inject() calls with default value: inject('key', 'default-classes')
|
|
966
|
+
const injectClasses = this.extractFromInjectCall(initializer, context, attributeId);
|
|
967
|
+
if (injectClasses.length > 0) {
|
|
968
|
+
classNames.push(...injectClasses);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// For regular variables, extract classes from the initializer
|
|
973
|
+
// Keep original position from string literal
|
|
974
|
+
classNames.push(...addAttributeId(this.extractFromExpression(initializer, context)));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Handle function declarations: function getClasses() { return [...] }
|
|
978
|
+
else if (typescript.isFunctionDeclaration(declaration)) {
|
|
979
|
+
const funcClasses = this.extractFromFunctionDeclaration(declaration, context, attributeId, templatePosition, templateLength);
|
|
980
|
+
classNames.push(...funcClasses);
|
|
981
|
+
}
|
|
982
|
+
// Handle property assignments in Vue's generated code:
|
|
983
|
+
// `return { myClass: myClass as typeof myClass }`
|
|
984
|
+
// The declaration is the PropertyAssignment, and we need to follow the reference
|
|
985
|
+
else if (typescript.isPropertyAssignment(declaration)) {
|
|
986
|
+
const initializer = declaration.initializer;
|
|
987
|
+
// Follow the reference: `myClass as typeof myClass` or just `myClass`
|
|
988
|
+
const resolvedClasses = this.resolvePropertyAssignmentClasses(initializer, context, attributeId, templatePosition, templateLength);
|
|
989
|
+
classNames.push(...resolvedClasses);
|
|
990
|
+
}
|
|
991
|
+
// Handle shorthand property assignments: `return { myClass }`
|
|
992
|
+
else if (typescript.isShorthandPropertyAssignment(declaration)) {
|
|
993
|
+
// The name itself is the reference to the variable
|
|
994
|
+
const resolvedClasses = this.resolveIdentifierClasses(declaration.name, context, attributeId, templatePosition, templateLength);
|
|
995
|
+
classNames.push(...resolvedClasses);
|
|
996
|
+
}
|
|
997
|
+
// Handle property signatures in Volar 3.x:
|
|
998
|
+
// type __VLS_SetupExposed = { myClass: typeof myClass; }
|
|
999
|
+
// The type is a string literal type that contains the class value
|
|
1000
|
+
else if (typescript.isPropertySignature(declaration)) {
|
|
1001
|
+
const resolvedClasses = this.extractFromPropertySignatureType(propertyName, context, attributeId);
|
|
1002
|
+
classNames.push(...resolvedClasses);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return classNames;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Extract classes from a PropertySignature by getting the type.
|
|
1009
|
+
*
|
|
1010
|
+
* In Volar 3.x, script variables are exposed via a type like:
|
|
1011
|
+
* type __VLS_SetupExposed = { myClass: typeof myClass; }
|
|
1012
|
+
*
|
|
1013
|
+
* We prioritize finding the actual variable declaration to use original positions,
|
|
1014
|
+
* so errors point to the actual class strings in the script section.
|
|
1015
|
+
*/
|
|
1016
|
+
extractFromPropertySignatureType(identifier, context, attributeId) {
|
|
1017
|
+
const { typescript, typeChecker } = context;
|
|
1018
|
+
if (!typeChecker) {
|
|
1019
|
+
return [];
|
|
1020
|
+
}
|
|
1021
|
+
// For computed/functions, we need to use template position since
|
|
1022
|
+
// the actual class values are in dynamic expressions
|
|
1023
|
+
const templatePosition = identifier.getStart();
|
|
1024
|
+
const templateLength = identifier.text.length;
|
|
1025
|
+
// PRIORITY 1: Try to find the actual variable declaration via PropertySignature's typeof
|
|
1026
|
+
// This allows us to use original positions for string literals in the script section
|
|
1027
|
+
const symbol = typeChecker.getSymbolAtLocation(identifier);
|
|
1028
|
+
if (symbol) {
|
|
1029
|
+
const declarations = symbol.getDeclarations();
|
|
1030
|
+
if (declarations) {
|
|
1031
|
+
for (const decl of declarations) {
|
|
1032
|
+
if (typescript.isPropertySignature(decl) && decl.type) {
|
|
1033
|
+
// Check if the type is a TypeQuery (typeof expression)
|
|
1034
|
+
if (typescript.isTypeQueryNode(decl.type)) {
|
|
1035
|
+
// The exprName contains the identifier we need to resolve
|
|
1036
|
+
const exprName = decl.type.exprName;
|
|
1037
|
+
if (typescript.isIdentifier(exprName)) {
|
|
1038
|
+
// Get the symbol for this identifier
|
|
1039
|
+
const varSymbol = typeChecker.getSymbolAtLocation(exprName);
|
|
1040
|
+
if (varSymbol) {
|
|
1041
|
+
const varDeclarations = varSymbol.getDeclarations();
|
|
1042
|
+
if (varDeclarations) {
|
|
1043
|
+
for (const varDecl of varDeclarations) {
|
|
1044
|
+
if (typescript.isVariableDeclaration(varDecl) && varDecl.initializer) {
|
|
1045
|
+
// Check if it's a computed() or inject() call - use ORIGINAL position
|
|
1046
|
+
// so errors point to actual class strings in script
|
|
1047
|
+
if (typescript.isCallExpression(varDecl.initializer)) {
|
|
1048
|
+
const computedClasses = this.extractFromComputedCall(varDecl.initializer, context, attributeId
|
|
1049
|
+
// Don't pass templatePosition - use original positions
|
|
1050
|
+
);
|
|
1051
|
+
if (computedClasses.length > 0) {
|
|
1052
|
+
return computedClasses;
|
|
1053
|
+
}
|
|
1054
|
+
// Check for inject() calls with default value
|
|
1055
|
+
const injectClasses = this.extractFromInjectCall(varDecl.initializer, context, attributeId);
|
|
1056
|
+
if (injectClasses.length > 0) {
|
|
1057
|
+
return injectClasses;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// For string literals, use ORIGINAL position from script
|
|
1061
|
+
// This makes errors point to the actual invalid class
|
|
1062
|
+
const classes = this.extractFromExpression(varDecl.initializer, context);
|
|
1063
|
+
if (classes.length > 0) {
|
|
1064
|
+
return classes.map(c => ({ ...c, attributeId }));
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
// PRIORITY 2: Fall back to type-based extraction with template position
|
|
1077
|
+
// This handles cases where we can't find the actual declaration
|
|
1078
|
+
const type = typeChecker.getTypeAtLocation(identifier);
|
|
1079
|
+
// Check if it's a string literal type (simple string variable)
|
|
1080
|
+
if (type.isStringLiteral()) {
|
|
1081
|
+
const classValue = type.value;
|
|
1082
|
+
return this.parseClassString(classValue, identifier, context, attributeId);
|
|
1083
|
+
}
|
|
1084
|
+
// For union types (e.g., ternary expressions), collect all string literal types
|
|
1085
|
+
if (type.isUnion()) {
|
|
1086
|
+
const classNames = [];
|
|
1087
|
+
for (const unionType of type.types) {
|
|
1088
|
+
if (unionType.isStringLiteral()) {
|
|
1089
|
+
classNames.push(...this.parseClassString(unionType.value, identifier, context, attributeId));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (classNames.length > 0) {
|
|
1093
|
+
return classNames;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// PRIORITY 3: Fallback via type.getSymbol() for non-PropertySignature cases
|
|
1097
|
+
const typeSymbol = type.getSymbol();
|
|
1098
|
+
if (typeSymbol) {
|
|
1099
|
+
const declarations = typeSymbol.getDeclarations();
|
|
1100
|
+
if (declarations) {
|
|
1101
|
+
for (const decl of declarations) {
|
|
1102
|
+
if (typescript.isVariableDeclaration(decl) && decl.initializer) {
|
|
1103
|
+
// Check if it's a computed() call
|
|
1104
|
+
if (typescript.isCallExpression(decl.initializer)) {
|
|
1105
|
+
const computedClasses = this.extractFromComputedCall(decl.initializer, context, attributeId, templatePosition, templateLength);
|
|
1106
|
+
if (computedClasses.length > 0) {
|
|
1107
|
+
return computedClasses;
|
|
1108
|
+
}
|
|
1109
|
+
// Check for inject() calls with default value
|
|
1110
|
+
const injectClasses = this.extractFromInjectCall(decl.initializer, context, attributeId);
|
|
1111
|
+
if (injectClasses.length > 0) {
|
|
1112
|
+
return injectClasses;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// Otherwise extract from the initializer directly
|
|
1116
|
+
const classes = this.extractFromExpression(decl.initializer, context);
|
|
1117
|
+
return classes.map(c => ({
|
|
1118
|
+
...c,
|
|
1119
|
+
attributeId,
|
|
1120
|
+
absoluteStart: templatePosition,
|
|
1121
|
+
length: templateLength,
|
|
1122
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(templatePosition).line + 1
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Parse a class string into ClassNameInfo array.
|
|
1132
|
+
* Uses the identifier position for diagnostics (mapped by Volar).
|
|
1133
|
+
*/
|
|
1134
|
+
parseClassString(classValue, identifier, context, attributeId) {
|
|
1135
|
+
const classNames = [];
|
|
1136
|
+
if (!classValue || classValue.length === 0) {
|
|
1137
|
+
return classNames;
|
|
1138
|
+
}
|
|
1139
|
+
// Use identifier position for all diagnostics (Volar will map this)
|
|
1140
|
+
const position = identifier.getStart();
|
|
1141
|
+
const line = context.sourceFile.getLineAndCharacterOfPosition(position).line + 1;
|
|
1142
|
+
const parts = classValue.split(/\s+/);
|
|
1143
|
+
for (const part of parts) {
|
|
1144
|
+
if (part && part.trim()) {
|
|
1145
|
+
classNames.push({
|
|
1146
|
+
className: part.trim(),
|
|
1147
|
+
absoluteStart: position,
|
|
1148
|
+
length: identifier.text.length,
|
|
1149
|
+
line,
|
|
1150
|
+
file: context.sourceFile.fileName,
|
|
1151
|
+
attributeId
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return classNames;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Extract classes from a __VLS_ctx.functionName() call by resolving the function.
|
|
1159
|
+
*/
|
|
1160
|
+
extractFromVlsCtxFunctionCall(functionName, context, attributeId, templatePosition, templateLength) {
|
|
1161
|
+
const { typescript, typeChecker } = context;
|
|
1162
|
+
if (!typeChecker) {
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
// Get the symbol for the function name
|
|
1166
|
+
const symbol = typeChecker.getSymbolAtLocation(functionName);
|
|
1167
|
+
if (!symbol) {
|
|
1168
|
+
return [];
|
|
1169
|
+
}
|
|
1170
|
+
const declarations = symbol.getDeclarations();
|
|
1171
|
+
if (!declarations || declarations.length === 0) {
|
|
1172
|
+
return [];
|
|
1173
|
+
}
|
|
1174
|
+
const classNames = [];
|
|
1175
|
+
for (const declaration of declarations) {
|
|
1176
|
+
// Handle property assignment in Vue's return: `return { getClasses: getClasses as typeof getClasses }`
|
|
1177
|
+
if (typescript.isPropertyAssignment(declaration)) {
|
|
1178
|
+
const initializer = declaration.initializer;
|
|
1179
|
+
// Unwrap type assertion: `getClasses as typeof getClasses` -> `getClasses`
|
|
1180
|
+
let expr = initializer;
|
|
1181
|
+
if (typescript.isAsExpression(expr)) {
|
|
1182
|
+
expr = expr.expression;
|
|
1183
|
+
}
|
|
1184
|
+
// Resolve the identifier to the actual function
|
|
1185
|
+
if (typescript.isIdentifier(expr)) {
|
|
1186
|
+
const funcClasses = this.resolveFunctionIdentifier(expr, context, attributeId, templatePosition, templateLength);
|
|
1187
|
+
classNames.push(...funcClasses);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// Handle shorthand property: `return { getClasses }`
|
|
1191
|
+
else if (typescript.isShorthandPropertyAssignment(declaration)) {
|
|
1192
|
+
const funcClasses = this.resolveFunctionIdentifier(declaration.name, context, attributeId, templatePosition, templateLength);
|
|
1193
|
+
classNames.push(...funcClasses);
|
|
1194
|
+
}
|
|
1195
|
+
// Handle property signature in Volar 3.x:
|
|
1196
|
+
// type __VLS_SetupExposed = { getClasses: typeof getClasses; }
|
|
1197
|
+
else if (typescript.isPropertySignature(declaration)) {
|
|
1198
|
+
// The type is `typeof getClasses`, which refers to the actual function
|
|
1199
|
+
// We need to get the type and find the function declaration
|
|
1200
|
+
const funcClasses = this.extractFromPropertySignatureFunction(functionName, context, attributeId);
|
|
1201
|
+
classNames.push(...funcClasses);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return classNames;
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Extract classes from a function referenced via PropertySignature.
|
|
1208
|
+
* In Volar 3.x: `type __VLS_SetupExposed = { getClasses: typeof getClasses; }`
|
|
1209
|
+
*/
|
|
1210
|
+
extractFromPropertySignatureFunction(functionName, context, attributeId) {
|
|
1211
|
+
const { typescript, typeChecker } = context;
|
|
1212
|
+
if (!typeChecker) {
|
|
1213
|
+
return [];
|
|
1214
|
+
}
|
|
1215
|
+
// Get the type at the function name location
|
|
1216
|
+
const type = typeChecker.getTypeAtLocation(functionName);
|
|
1217
|
+
// Try to find the actual function via type's symbol FIRST
|
|
1218
|
+
// This gives us access to the function body for analysis
|
|
1219
|
+
const typeSymbol = type.getSymbol();
|
|
1220
|
+
if (typeSymbol) {
|
|
1221
|
+
const funcDeclarations = typeSymbol.getDeclarations();
|
|
1222
|
+
if (funcDeclarations) {
|
|
1223
|
+
for (const decl of funcDeclarations) {
|
|
1224
|
+
if (typescript.isFunctionDeclaration(decl) && decl.body) {
|
|
1225
|
+
// Don't pass templatePosition - use original positions
|
|
1226
|
+
// so errors point to actual class strings in script
|
|
1227
|
+
return this.extractFromFunctionBody(decl.body, context, attributeId);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Fallback: try to extract from return type if it's a tuple of literals
|
|
1233
|
+
const callSignatures = type.getCallSignatures();
|
|
1234
|
+
if (callSignatures.length === 0) {
|
|
1235
|
+
return [];
|
|
1236
|
+
}
|
|
1237
|
+
const returnType = callSignatures[0].getReturnType();
|
|
1238
|
+
// For array types like string[], check if elements are string literal types
|
|
1239
|
+
if (typeChecker.isArrayType(returnType)) {
|
|
1240
|
+
const typeArgs = returnType.typeArguments;
|
|
1241
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
1242
|
+
const elementType = typeArgs[0];
|
|
1243
|
+
return this.extractClassesFromType(elementType, functionName, context, attributeId);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Extract classes from a TypeScript type (for union types, string literals, etc.)
|
|
1250
|
+
*/
|
|
1251
|
+
extractClassesFromType(type, identifier, context, attributeId) {
|
|
1252
|
+
const { typeChecker } = context;
|
|
1253
|
+
if (!typeChecker) {
|
|
1254
|
+
return [];
|
|
1255
|
+
}
|
|
1256
|
+
const classNames = [];
|
|
1257
|
+
// For union types, collect all string literal types
|
|
1258
|
+
if (type.isUnion()) {
|
|
1259
|
+
for (const unionType of type.types) {
|
|
1260
|
+
if (unionType.isStringLiteral()) {
|
|
1261
|
+
classNames.push(...this.parseClassString(unionType.value, identifier, context, attributeId));
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// For single string literal type
|
|
1266
|
+
else if (type.isStringLiteral()) {
|
|
1267
|
+
classNames.push(...this.parseClassString(type.value, identifier, context, attributeId));
|
|
1268
|
+
}
|
|
1269
|
+
return classNames;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Resolve a function identifier to its declaration and extract classes from return statements.
|
|
1273
|
+
*/
|
|
1274
|
+
resolveFunctionIdentifier(identifier, context, attributeId, templatePosition, templateLength) {
|
|
1275
|
+
const { typescript, typeChecker } = context;
|
|
1276
|
+
if (!typeChecker) {
|
|
1277
|
+
return [];
|
|
1278
|
+
}
|
|
1279
|
+
const symbol = typeChecker.getSymbolAtLocation(identifier);
|
|
1280
|
+
if (!symbol) {
|
|
1281
|
+
return [];
|
|
1282
|
+
}
|
|
1283
|
+
const declarations = symbol.getDeclarations();
|
|
1284
|
+
if (!declarations || declarations.length === 0) {
|
|
1285
|
+
return [];
|
|
1286
|
+
}
|
|
1287
|
+
const classNames = [];
|
|
1288
|
+
for (const declaration of declarations) {
|
|
1289
|
+
if (typescript.isFunctionDeclaration(declaration)) {
|
|
1290
|
+
classNames.push(...this.extractFromFunctionDeclaration(declaration, context, attributeId, templatePosition, templateLength));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return classNames;
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Resolve classes from a property assignment initializer.
|
|
1297
|
+
* Handles patterns like `myClass as typeof myClass` or just `myClass`.
|
|
1298
|
+
*/
|
|
1299
|
+
resolvePropertyAssignmentClasses(initializer, context, attributeId, templatePosition, templateLength) {
|
|
1300
|
+
const { typescript } = context;
|
|
1301
|
+
// Unwrap type assertions: `myClass as typeof myClass` -> `myClass`
|
|
1302
|
+
let expr = initializer;
|
|
1303
|
+
if (typescript.isAsExpression(expr)) {
|
|
1304
|
+
expr = expr.expression;
|
|
1305
|
+
}
|
|
1306
|
+
// If it's an identifier, resolve it to the actual variable
|
|
1307
|
+
if (typescript.isIdentifier(expr)) {
|
|
1308
|
+
return this.resolveIdentifierClasses(expr, context, attributeId, templatePosition, templateLength);
|
|
1309
|
+
}
|
|
1310
|
+
return [];
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Resolve classes from an identifier by finding its declaration.
|
|
1314
|
+
*/
|
|
1315
|
+
resolveIdentifierClasses(identifier, context, attributeId, templatePosition, templateLength) {
|
|
1316
|
+
const { typescript, typeChecker } = context;
|
|
1317
|
+
if (!typeChecker) {
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
const symbol = typeChecker.getSymbolAtLocation(identifier);
|
|
1321
|
+
if (!symbol) {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
const declarations = symbol.getDeclarations();
|
|
1325
|
+
if (!declarations || declarations.length === 0) {
|
|
1326
|
+
return [];
|
|
1327
|
+
}
|
|
1328
|
+
const classNames = [];
|
|
1329
|
+
// Keep original positions - diagnostic will point to where class is defined
|
|
1330
|
+
const addAttributeId = (classes) => classes.map(c => ({
|
|
1331
|
+
...c,
|
|
1332
|
+
attributeId
|
|
1333
|
+
}));
|
|
1334
|
+
for (const declaration of declarations) {
|
|
1335
|
+
if (typescript.isVariableDeclaration(declaration)) {
|
|
1336
|
+
const init = declaration.initializer;
|
|
1337
|
+
if (init) {
|
|
1338
|
+
// Check for computed() or inject() calls
|
|
1339
|
+
if (typescript.isCallExpression(init)) {
|
|
1340
|
+
const computedClasses = this.extractFromComputedCall(init, context, attributeId, templatePosition, templateLength);
|
|
1341
|
+
if (computedClasses.length > 0) {
|
|
1342
|
+
classNames.push(...computedClasses);
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
// Check for inject() calls with default value
|
|
1346
|
+
const injectClasses = this.extractFromInjectCall(init, context, attributeId);
|
|
1347
|
+
if (injectClasses.length > 0) {
|
|
1348
|
+
classNames.push(...injectClasses);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
// Extract classes from the initializer - keep original position
|
|
1353
|
+
classNames.push(...addAttributeId(this.extractFromExpression(init, context)));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
else if (typescript.isFunctionDeclaration(declaration)) {
|
|
1357
|
+
classNames.push(...this.extractFromFunctionDeclaration(declaration, context, attributeId, templatePosition, templateLength));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return classNames;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Extract classes from a computed() call expression.
|
|
1364
|
+
* Handles: const classes = computed(() => ['flex', 'items-center'])
|
|
1365
|
+
*/
|
|
1366
|
+
extractFromComputedCall(callExpr, context, attributeId, templatePosition, templateLength) {
|
|
1367
|
+
const { typescript } = context;
|
|
1368
|
+
// Check if this is a call to 'computed'
|
|
1369
|
+
const calleeExpr = callExpr.expression;
|
|
1370
|
+
if (!typescript.isIdentifier(calleeExpr) || calleeExpr.text !== 'computed') {
|
|
1371
|
+
return [];
|
|
1372
|
+
}
|
|
1373
|
+
// Get the callback argument
|
|
1374
|
+
if (callExpr.arguments.length === 0) {
|
|
1375
|
+
return [];
|
|
1376
|
+
}
|
|
1377
|
+
const callback = callExpr.arguments[0];
|
|
1378
|
+
// Handle arrow functions: computed(() => [...])
|
|
1379
|
+
if (typescript.isArrowFunction(callback)) {
|
|
1380
|
+
return this.extractFromFunctionBody(callback.body, context, attributeId, templatePosition, templateLength);
|
|
1381
|
+
}
|
|
1382
|
+
// Handle regular functions: computed(function() { return [...] })
|
|
1383
|
+
if (typescript.isFunctionExpression(callback)) {
|
|
1384
|
+
if (callback.body) {
|
|
1385
|
+
return this.extractFromFunctionBody(callback.body, context, attributeId, templatePosition, templateLength);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return [];
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Extract classes from an inject() call with a default value.
|
|
1392
|
+
* Handles: const classes = inject('key', 'flex items-center')
|
|
1393
|
+
*/
|
|
1394
|
+
extractFromInjectCall(callExpr, context, attributeId) {
|
|
1395
|
+
const { typescript } = context;
|
|
1396
|
+
// Check if this is a call to 'inject'
|
|
1397
|
+
const calleeExpr = callExpr.expression;
|
|
1398
|
+
if (!typescript.isIdentifier(calleeExpr) || calleeExpr.text !== 'inject') {
|
|
1399
|
+
return [];
|
|
1400
|
+
}
|
|
1401
|
+
// inject() needs at least 2 arguments for us to extract the default value
|
|
1402
|
+
// inject(key, defaultValue) or inject(key, defaultValue, treatDefaultAsFactory)
|
|
1403
|
+
if (callExpr.arguments.length < 2) {
|
|
1404
|
+
return [];
|
|
1405
|
+
}
|
|
1406
|
+
const defaultValue = callExpr.arguments[1];
|
|
1407
|
+
// Extract classes from the default value
|
|
1408
|
+
const classes = this.extractFromExpression(defaultValue, context, attributeId);
|
|
1409
|
+
return classes.map(c => ({ ...c, attributeId }));
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Extract classes from a function declaration's return statements.
|
|
1413
|
+
* Handles: function getClasses() { return ['flex', 'items-center']; }
|
|
1414
|
+
*/
|
|
1415
|
+
extractFromFunctionDeclaration(funcDecl, context, attributeId, templatePosition, templateLength) {
|
|
1416
|
+
if (!funcDecl.body) {
|
|
1417
|
+
return [];
|
|
1418
|
+
}
|
|
1419
|
+
return this.extractFromFunctionBody(funcDecl.body, context, attributeId, templatePosition, templateLength);
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Extract classes from a function body (block or expression).
|
|
1423
|
+
* Handles both arrow function expressions and function blocks.
|
|
1424
|
+
*/
|
|
1425
|
+
extractFromFunctionBody(body, context, attributeId, templatePosition, templateLength) {
|
|
1426
|
+
const { typescript } = context;
|
|
1427
|
+
const classNames = [];
|
|
1428
|
+
// If template position is provided, override positions for Volar mapping
|
|
1429
|
+
const processClasses = (classes) => {
|
|
1430
|
+
if (templatePosition !== undefined && templateLength !== undefined) {
|
|
1431
|
+
return classes.map(c => ({
|
|
1432
|
+
...c,
|
|
1433
|
+
attributeId,
|
|
1434
|
+
absoluteStart: templatePosition,
|
|
1435
|
+
length: templateLength,
|
|
1436
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(templatePosition).line + 1
|
|
1437
|
+
}));
|
|
1438
|
+
}
|
|
1439
|
+
return classes.map(c => ({ ...c, attributeId }));
|
|
1440
|
+
};
|
|
1441
|
+
// Handle concise arrow function: () => ['flex', 'items-center']
|
|
1442
|
+
if (!typescript.isBlock(body)) {
|
|
1443
|
+
// The body is an expression, extract classes from it
|
|
1444
|
+
classNames.push(...processClasses(this.extractFromExpression(body, context)));
|
|
1445
|
+
return classNames;
|
|
1446
|
+
}
|
|
1447
|
+
// Handle block body: look for return statements
|
|
1448
|
+
const visitNode = (node) => {
|
|
1449
|
+
if (typescript.isReturnStatement(node) && node.expression) {
|
|
1450
|
+
classNames.push(...processClasses(this.extractFromExpression(node.expression, context)));
|
|
1451
|
+
}
|
|
1452
|
+
typescript.forEachChild(node, visitNode);
|
|
1453
|
+
};
|
|
1454
|
+
typescript.forEachChild(body, visitNode);
|
|
1455
|
+
return classNames;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Extract classes from an expression (array, object, string, etc.)
|
|
1459
|
+
* @param expr The expression to extract from
|
|
1460
|
+
* @param context The extraction context
|
|
1461
|
+
* @param attributeId Optional attribute ID for tracking
|
|
1462
|
+
*/
|
|
1463
|
+
extractFromExpression(expr, context, attributeId) {
|
|
1464
|
+
const { typescript } = context;
|
|
1465
|
+
const classNames = [];
|
|
1466
|
+
// String literal: 'flex items-center'
|
|
1467
|
+
if (typescript.isStringLiteral(expr)) {
|
|
1468
|
+
const fullText = expr.text;
|
|
1469
|
+
if (fullText.length === 0) {
|
|
1470
|
+
return classNames;
|
|
1471
|
+
}
|
|
1472
|
+
const stringContentStart = expr.getStart() + 1;
|
|
1473
|
+
let offset = 0;
|
|
1474
|
+
const parts = fullText.split(/(\s+)/);
|
|
1475
|
+
for (const part of parts) {
|
|
1476
|
+
if (part && !/^\s+$/.test(part)) {
|
|
1477
|
+
classNames.push({
|
|
1478
|
+
className: part,
|
|
1479
|
+
absoluteStart: stringContentStart + offset,
|
|
1480
|
+
length: part.length,
|
|
1481
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(stringContentStart + offset).line +
|
|
1482
|
+
1,
|
|
1483
|
+
file: context.sourceFile.fileName
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
offset += part.length;
|
|
1487
|
+
}
|
|
1488
|
+
return classNames;
|
|
1489
|
+
}
|
|
1490
|
+
// Array literal: ['flex', 'items-center', { 'bg-red-500': isActive }]
|
|
1491
|
+
if (typescript.isArrayLiteralExpression(expr)) {
|
|
1492
|
+
for (const element of expr.elements) {
|
|
1493
|
+
if (element === undefined)
|
|
1494
|
+
continue;
|
|
1495
|
+
if (typescript.isStringLiteral(element)) {
|
|
1496
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
1497
|
+
}
|
|
1498
|
+
else if (typescript.isObjectLiteralExpression(element)) {
|
|
1499
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
1500
|
+
}
|
|
1501
|
+
else if (typescript.isSpreadElement(element)) {
|
|
1502
|
+
classNames.push(...this.extractFromExpression(element.expression, context, attributeId));
|
|
1503
|
+
}
|
|
1504
|
+
// Handle __VLS_ctx.variable references in arrays
|
|
1505
|
+
else if (typescript.isPropertyAccessExpression(element)) {
|
|
1506
|
+
if (attributeId) {
|
|
1507
|
+
const vlsResults = this.extractFromVlsCtxReference(element, context, attributeId);
|
|
1508
|
+
if (vlsResults.length > 0) {
|
|
1509
|
+
classNames.push(...vlsResults);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
// Handle parenthesized expressions: (__VLS_ctx.myVar)
|
|
1514
|
+
else if (typescript.isParenthesizedExpression(element)) {
|
|
1515
|
+
const inner = element.expression;
|
|
1516
|
+
if (typescript.isPropertyAccessExpression(inner) && attributeId) {
|
|
1517
|
+
const vlsResults = this.extractFromVlsCtxReference(inner, context, attributeId);
|
|
1518
|
+
if (vlsResults.length > 0) {
|
|
1519
|
+
classNames.push(...vlsResults);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
// Handle nested arrays recursively
|
|
1524
|
+
else if (typescript.isArrayLiteralExpression(element)) {
|
|
1525
|
+
classNames.push(...this.extractFromExpression(element, context, attributeId));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return classNames;
|
|
1529
|
+
}
|
|
1530
|
+
// Object literal: { 'flex': true, 'bg-red-500': isActive }
|
|
1531
|
+
if (typescript.isObjectLiteralExpression(expr)) {
|
|
1532
|
+
for (const prop of expr.properties) {
|
|
1533
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
1534
|
+
const propName = prop.name;
|
|
1535
|
+
let className;
|
|
1536
|
+
let start;
|
|
1537
|
+
if (typescript.isStringLiteral(propName)) {
|
|
1538
|
+
className = propName.text;
|
|
1539
|
+
start = propName.getStart() + 1;
|
|
1540
|
+
}
|
|
1541
|
+
else if (typescript.isIdentifier(propName)) {
|
|
1542
|
+
className = propName.text;
|
|
1543
|
+
start = propName.getStart();
|
|
1544
|
+
}
|
|
1545
|
+
// Handle computed property names: { [__VLS_ctx.myVar]: true }
|
|
1546
|
+
else if (typescript.isComputedPropertyName(propName)) {
|
|
1547
|
+
let computedExpr = propName.expression;
|
|
1548
|
+
// Unwrap parentheses: { [(__VLS_ctx.myVar)]: true }
|
|
1549
|
+
if (typescript.isParenthesizedExpression(computedExpr)) {
|
|
1550
|
+
computedExpr = computedExpr.expression;
|
|
1551
|
+
}
|
|
1552
|
+
// Resolve __VLS_ctx.variable pattern
|
|
1553
|
+
if (typescript.isPropertyAccessExpression(computedExpr) && attributeId) {
|
|
1554
|
+
const vlsResults = this.extractFromVlsCtxReference(computedExpr, context, attributeId);
|
|
1555
|
+
if (vlsResults.length > 0) {
|
|
1556
|
+
classNames.push(...vlsResults);
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
if (className && start !== undefined) {
|
|
1562
|
+
classNames.push({
|
|
1563
|
+
className,
|
|
1564
|
+
absoluteStart: start,
|
|
1565
|
+
length: className.length,
|
|
1566
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
1567
|
+
file: context.sourceFile.fileName,
|
|
1568
|
+
attributeId
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
else if (typescript.isShorthandPropertyAssignment(prop)) {
|
|
1573
|
+
const className = prop.name.text;
|
|
1574
|
+
const start = prop.name.getStart();
|
|
1575
|
+
classNames.push({
|
|
1576
|
+
className,
|
|
1577
|
+
absoluteStart: start,
|
|
1578
|
+
length: className.length,
|
|
1579
|
+
line: context.sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
1580
|
+
file: context.sourceFile.fileName,
|
|
1581
|
+
attributeId
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return classNames;
|
|
1586
|
+
}
|
|
1587
|
+
// Handle conditional (ternary) expressions: isActive ? 'flex' : 'hidden'
|
|
1588
|
+
if (typescript.isConditionalExpression(expr)) {
|
|
1589
|
+
// Extract from both branches recursively
|
|
1590
|
+
classNames.push(...this.extractFromExpression(expr.whenTrue, context, attributeId));
|
|
1591
|
+
classNames.push(...this.extractFromExpression(expr.whenFalse, context, attributeId));
|
|
1592
|
+
return classNames;
|
|
1593
|
+
}
|
|
1594
|
+
// Handle binary expressions: isActive && 'flex', isDisabled || 'fallback'
|
|
1595
|
+
if (typescript.isBinaryExpression(expr)) {
|
|
1596
|
+
classNames.push(...this.extractFromExpression(expr.left, context, attributeId));
|
|
1597
|
+
classNames.push(...this.extractFromExpression(expr.right, context, attributeId));
|
|
1598
|
+
return classNames;
|
|
1599
|
+
}
|
|
1600
|
+
// Handle parenthesized expressions: ('flex items-center')
|
|
1601
|
+
if (typescript.isParenthesizedExpression(expr)) {
|
|
1602
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1603
|
+
}
|
|
1604
|
+
// Handle type assertions: 'flex' as string, 'flex' as const
|
|
1605
|
+
if (typescript.isAsExpression(expr)) {
|
|
1606
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1607
|
+
}
|
|
1608
|
+
// Handle non-null assertions: someValue!
|
|
1609
|
+
if (typescript.isNonNullExpression(expr)) {
|
|
1610
|
+
return this.extractFromExpression(expr.expression, context, attributeId);
|
|
1611
|
+
}
|
|
1612
|
+
// Handle template literals
|
|
1613
|
+
if (typescript.isTemplateExpression(expr) || typescript.isNoSubstitutionTemplateLiteral(expr)) {
|
|
1614
|
+
const addAttrId = (classes) => attributeId ? classes.map(c => ({ ...c, attributeId })) : classes;
|
|
1615
|
+
return addAttrId(this.expressionExtractor.extract(expr, context));
|
|
1616
|
+
}
|
|
1617
|
+
return classNames;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Extract classes from props.propertyName patterns with default values.
|
|
1621
|
+
* Vue generates __VLS_defaults for withDefaults() calls.
|
|
1622
|
+
*
|
|
1623
|
+
* Generated code pattern:
|
|
1624
|
+
* const __VLS_defaults = { buttonClass: 'flex items-center' };
|
|
1625
|
+
* ...{ class: (props.buttonClass) }
|
|
1626
|
+
*/
|
|
1627
|
+
extractFromPropsWithDefaults(value, context, attributeId) {
|
|
1628
|
+
const { typescript, typeChecker } = context;
|
|
1629
|
+
if (!typeChecker) {
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1632
|
+
// Unwrap parentheses: (props.buttonClass) -> props.buttonClass
|
|
1633
|
+
let expr = value;
|
|
1634
|
+
if (typescript.isParenthesizedExpression(expr)) {
|
|
1635
|
+
expr = expr.expression;
|
|
1636
|
+
}
|
|
1637
|
+
// Check for props.propertyName pattern
|
|
1638
|
+
if (!typescript.isPropertyAccessExpression(expr)) {
|
|
1639
|
+
return [];
|
|
1640
|
+
}
|
|
1641
|
+
const objectExpr = expr.expression;
|
|
1642
|
+
if (!typescript.isIdentifier(objectExpr) || objectExpr.text !== 'props') {
|
|
1643
|
+
return [];
|
|
1644
|
+
}
|
|
1645
|
+
const propertyName = expr.name;
|
|
1646
|
+
if (!typescript.isIdentifier(propertyName)) {
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
// Look for __VLS_defaults in the source file
|
|
1650
|
+
const defaultsValue = this.findVlsDefaultsProperty(propertyName.text, context);
|
|
1651
|
+
if (defaultsValue) {
|
|
1652
|
+
return this.extractFromExpression(defaultsValue, context, attributeId);
|
|
1653
|
+
}
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Find a property value in __VLS_defaults object.
|
|
1658
|
+
*/
|
|
1659
|
+
findVlsDefaultsProperty(propertyName, context) {
|
|
1660
|
+
const { typescript, sourceFile } = context;
|
|
1661
|
+
// Walk through the source file to find __VLS_defaults
|
|
1662
|
+
let result;
|
|
1663
|
+
const visitor = (node) => {
|
|
1664
|
+
if (result)
|
|
1665
|
+
return;
|
|
1666
|
+
if (typescript.isVariableDeclaration(node)) {
|
|
1667
|
+
const name = node.name;
|
|
1668
|
+
if (typescript.isIdentifier(name) &&
|
|
1669
|
+
name.text === '__VLS_defaults' &&
|
|
1670
|
+
node.initializer &&
|
|
1671
|
+
typescript.isObjectLiteralExpression(node.initializer)) {
|
|
1672
|
+
// Found __VLS_defaults, look for the property
|
|
1673
|
+
for (const prop of node.initializer.properties) {
|
|
1674
|
+
if (typescript.isPropertyAssignment(prop)) {
|
|
1675
|
+
const propName = prop.name;
|
|
1676
|
+
if (typescript.isIdentifier(propName) && propName.text === propertyName) {
|
|
1677
|
+
result = prop.initializer;
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
typescript.forEachChild(node, visitor);
|
|
1685
|
+
};
|
|
1686
|
+
typescript.forEachChild(sourceFile, visitor);
|
|
1687
|
+
return result;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
exports.VueAttributeExtractor = VueAttributeExtractor;
|
|
1691
|
+
//# sourceMappingURL=VueAttributeExtractor.js.map
|