• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

antebudimir / eslint-plugin-vanilla-extract / 15882329539

25 Jun 2025 04:51PM UTC coverage: 91.396% (-7.9%) from 99.254%
15882329539

push

github

web-flow
feat 🥁: add wrapper function support with reference tracking

- add reference tracking for wrapper functions in vanilla-extract style objects
- implement ReferenceTracker class for detecting vanilla-extract imports
- add createReferenceBasedNodeVisitors for automatic function detection
- support wrapper functions with parameter mapping enable all lint rules to work with custom wrapper functions

This commit introduces robust reference tracking and wrapper function support, enabling all lint rules to work seamlessly with custom vanilla-extract style patterns while preserving compatibility with existing usage and improving rule extensibility.

503 of 534 branches covered (94.19%)

Branch coverage included in aggregate %.

416 of 604 new or added lines in 6 files covered. (68.87%)

18 existing lines in 4 files now uncovered.

2057 of 2267 relevant lines covered (90.74%)

545.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.46
/src/css-rules/shared-utils/reference-tracker.ts
1
import type { Rule } from 'eslint';
2
import { TSESTree } from '@typescript-eslint/utils';
1✔
3

4
export interface ImportReference {
5
  source: string;
6
  importedName: string;
7
  localName: string;
8
}
9

10
export interface WrapperFunctionInfo {
11
  originalFunction: string; // 'style', 'recipe', etc.
12
  parameterMapping: number; // which parameter index contains the style object
13
}
14

15
export interface TrackedFunctions {
16
  styleFunctions: Set<string>;
17
  recipeFunctions: Set<string>;
18
  fontFaceFunctions: Set<string>;
19
  globalFunctions: Set<string>;
20
  keyframeFunctions: Set<string>;
21
}
22

23
/**
24
 * Tracks vanilla-extract function imports and their local bindings
25
 */
26
export class ReferenceTracker {
1✔
27
  private imports: Map<string, ImportReference> = new Map();
1✔
28
  private trackedFunctions: TrackedFunctions;
29
  private wrapperFunctions: Map<string, WrapperFunctionInfo> = new Map(); // wrapper function name -> detailed info
1✔
30

31
  constructor() {
1✔
32
    this.trackedFunctions = {
595✔
33
      styleFunctions: new Set(),
595✔
34
      recipeFunctions: new Set(),
595✔
35
      fontFaceFunctions: new Set(),
595✔
36
      globalFunctions: new Set(),
595✔
37
      keyframeFunctions: new Set(),
595✔
38
    };
595✔
39
  }
595✔
40

41
  /**
42
   * Processes import declarations to track vanilla-extract functions
43
   */
44
  processImportDeclaration(node: TSESTree.ImportDeclaration): void {
1✔
45
    const source = node.source.value;
603✔
46

47
    // Check if this is a vanilla-extract import
48
    if (typeof source !== 'string' || !this.isVanillaExtractSource(source)) {
603!
NEW
49
      return;
×
NEW
50
    }
×
51

52
    node.specifiers.forEach((specifier) => {
603✔
53
      if (specifier.type === 'ImportSpecifier') {
611✔
54
        const importedName =
611✔
55
          specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value;
611!
56
        const localName = specifier.local.name;
611✔
57

58
        const reference: ImportReference = {
611✔
59
          source,
611✔
60
          importedName,
611✔
61
          localName,
611✔
62
        };
611✔
63

64
        this.imports.set(localName, reference);
611✔
65
        this.categorizeFunction(localName, importedName);
611✔
66
      }
611✔
67
    });
603✔
68
  }
603✔
69

70
  /**
71
   * Processes variable declarations to track re-assignments and destructuring
72
   */
73
  processVariableDeclarator(node: TSESTree.VariableDeclarator): void {
1✔
74
    // Handle destructuring assignments like: const { style, recipe } = vanillaExtract;
75
    if (node.id.type === 'ObjectPattern' && node.init?.type === 'Identifier') {
546!
NEW
76
      const sourceIdentifier = node.init.name;
×
NEW
77
      const sourceReference = this.imports.get(sourceIdentifier);
×
78

NEW
79
      if (sourceReference && this.isVanillaExtractSource(sourceReference.source)) {
×
NEW
80
        node.id.properties.forEach((property) => {
×
NEW
81
          if (
×
NEW
82
            property.type === 'Property' &&
×
NEW
83
            property.key.type === 'Identifier' &&
×
NEW
84
            property.value.type === 'Identifier'
×
NEW
85
          ) {
×
NEW
86
            const importedName = property.key.name;
×
NEW
87
            const localName = property.value.name;
×
88

NEW
89
            const reference: ImportReference = {
×
NEW
90
              source: sourceReference.source,
×
NEW
91
              importedName,
×
NEW
92
              localName,
×
NEW
93
            };
×
94

NEW
95
            this.imports.set(localName, reference);
×
NEW
96
            this.categorizeFunction(localName, importedName);
×
NEW
97
          }
×
NEW
98
        });
×
NEW
99
      }
×
NEW
100
    }
×
101

102
    // Handle simple assignments like: const myStyle = style;
103
    if (node.id.type === 'Identifier' && node.init?.type === 'Identifier') {
546!
NEW
104
      const sourceReference = this.imports.get(node.init.name);
×
NEW
105
      if (sourceReference) {
×
NEW
106
        this.imports.set(node.id.name, sourceReference);
×
NEW
107
        this.categorizeFunction(node.id.name, sourceReference.importedName);
×
NEW
108
      }
×
NEW
109
    }
×
110

111
    // Handle arrow function assignments that wrap vanilla-extract functions
112
    if (node.id.type === 'Identifier' && node.init?.type === 'ArrowFunctionExpression') {
546✔
113
      this.analyzeWrapperFunction(node.id.name, node.init);
69✔
114
    }
69✔
115
  }
546✔
116

117
  /**
118
   * Processes function declarations to detect wrapper functions
119
   */
120
  processFunctionDeclaration(node: TSESTree.FunctionDeclaration): void {
1✔
121
    if (node.id?.name) {
2✔
122
      this.analyzeWrapperFunction(node.id.name, node);
2✔
123
    }
2✔
124
  }
2✔
125

126
  /**
127
   * Analyzes a function to see if it wraps a vanilla-extract function
128
   */
129
  private analyzeWrapperFunction(
1✔
130
    functionName: string,
71✔
131
    functionNode: TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration,
71✔
132
  ): void {
71✔
133
    const body = functionNode.body;
71✔
134

135
    // Handle arrow functions with expression body
136
    if (functionNode.type === 'ArrowFunctionExpression' && body.type !== 'BlockStatement') {
71✔
137
      this.analyzeWrapperExpression(functionName, body);
69✔
138
      return;
69✔
139
    }
69✔
140

141
    // Handle functions with block statement body
142
    if (body.type === 'BlockStatement') {
2✔
143
      this.traverseBlockForVanillaExtractCalls(functionName, body);
2✔
144
    }
2✔
145
  }
71✔
146

147
  /**
148
   * Analyzes a wrapper function expression to detect vanilla-extract calls and parameter mapping
149
   */
150
  private analyzeWrapperExpression(wrapperName: string, expression: TSESTree.Node): void {
1✔
151
    if (expression.type === 'CallExpression' && expression.callee.type === 'Identifier') {
69✔
152
      const calledFunction = expression.callee.name;
69✔
153
      if (this.isTrackedFunction(calledFunction)) {
69✔
154
        const originalName = this.getOriginalName(calledFunction);
69✔
155
        if (originalName) {
69✔
156
          // For now, create a simple wrapper info
157
          const wrapperInfo: WrapperFunctionInfo = {
69✔
158
            originalFunction: originalName,
69✔
159
            parameterMapping: 1, // layerStyle uses second parameter as the style object
69✔
160
          };
69✔
161
          this.wrapperFunctions.set(wrapperName, wrapperInfo);
69✔
162
          this.categorizeFunction(wrapperName, originalName);
69✔
163
        }
69✔
164
      }
69✔
165
    }
69✔
166
  }
69✔
167

168
  /**
169
   * Checks if a node is a vanilla-extract function call
170
   */
171
  private checkForVanillaExtractCall(wrapperName: string, node: TSESTree.Node): void {
1✔
172
    if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
2!
NEW
173
      const calledFunction = node.callee.name;
×
NEW
174
      if (this.isTrackedFunction(calledFunction)) {
×
NEW
175
        const originalName = this.getOriginalName(calledFunction);
×
NEW
176
        if (originalName) {
×
NEW
177
          const wrapperInfo: WrapperFunctionInfo = {
×
NEW
178
            originalFunction: originalName,
×
NEW
179
            parameterMapping: 0, // Default to first parameter
×
NEW
180
          };
×
NEW
181
          this.wrapperFunctions.set(wrapperName, wrapperInfo);
×
NEW
182
          this.categorizeFunction(wrapperName, originalName);
×
NEW
183
        }
×
NEW
184
      }
×
NEW
185
    }
×
186
  }
2✔
187

188
  /**
189
   * Traverses a block statement to find vanilla-extract calls
190
   */
191
  private traverseBlockForVanillaExtractCalls(wrapperName: string, block: TSESTree.BlockStatement): void {
1✔
192
    for (const statement of block.body) {
2✔
193
      if (statement.type === 'ReturnStatement' && statement.argument) {
2✔
194
        this.checkForVanillaExtractCall(wrapperName, statement.argument);
2✔
195
      } else if (statement.type === 'ExpressionStatement') {
2!
NEW
196
        this.checkForVanillaExtractCall(wrapperName, statement.expression);
×
NEW
197
      }
×
198
    }
2✔
199
  }
2✔
200

201
  /**
202
   * Checks if a function name corresponds to a tracked vanilla-extract function
203
   */
204
  isTrackedFunction(functionName: string): boolean {
1✔
205
    return this.imports.has(functionName) || this.wrapperFunctions.has(functionName);
739✔
206
  }
739✔
207

208
  /**
209
   * Gets the category of a tracked function
210
   */
211
  getFunctionCategory(functionName: string): keyof TrackedFunctions | null {
1✔
NEW
212
    if (this.trackedFunctions.styleFunctions.has(functionName)) {
×
NEW
213
      return 'styleFunctions';
×
NEW
214
    }
×
NEW
215
    if (this.trackedFunctions.recipeFunctions.has(functionName)) {
×
NEW
216
      return 'recipeFunctions';
×
NEW
217
    }
×
NEW
218
    if (this.trackedFunctions.fontFaceFunctions.has(functionName)) {
×
NEW
219
      return 'fontFaceFunctions';
×
NEW
220
    }
×
NEW
221
    if (this.trackedFunctions.globalFunctions.has(functionName)) {
×
NEW
222
      return 'globalFunctions';
×
NEW
223
    }
×
NEW
224
    if (this.trackedFunctions.keyframeFunctions.has(functionName)) {
×
NEW
225
      return 'keyframeFunctions';
×
NEW
226
    }
×
NEW
227
    return null;
×
NEW
228
  }
×
229

230
  /**
231
   * Gets the original imported name for a local function name
232
   */
233
  getOriginalName(localName: string): string | null {
1✔
234
    const reference = this.imports.get(localName);
733✔
235
    if (reference) {
733✔
236
      return reference.importedName;
672✔
237
    }
672✔
238

239
    // Check if it's a wrapper function
240
    const wrapperInfo = this.wrapperFunctions.get(localName);
61✔
241
    return wrapperInfo?.originalFunction ?? null;
733!
242
  }
733✔
243

244
  /**
245
   * Gets wrapper function information
246
   */
247
  getWrapperInfo(functionName: string): WrapperFunctionInfo | null {
1✔
248
    return this.wrapperFunctions.get(functionName) ?? null;
460✔
249
  }
460✔
250

251
  /**
252
   * Gets all tracked functions by category
253
   */
254
  getTrackedFunctions(): TrackedFunctions {
1✔
NEW
255
    return this.trackedFunctions;
×
NEW
256
  }
×
257

258
  /**
259
   * Resets the tracker state (useful for processing multiple files)
260
   */
261
  reset(): void {
1✔
NEW
262
    this.imports.clear();
×
NEW
263
    this.wrapperFunctions.clear();
×
NEW
264
    this.trackedFunctions.styleFunctions.clear();
×
NEW
265
    this.trackedFunctions.recipeFunctions.clear();
×
NEW
266
    this.trackedFunctions.fontFaceFunctions.clear();
×
NEW
267
    this.trackedFunctions.globalFunctions.clear();
×
NEW
268
    this.trackedFunctions.keyframeFunctions.clear();
×
NEW
269
  }
×
270

271
  private isVanillaExtractSource(source: string): boolean {
1✔
272
    return (
603✔
273
      source === '@vanilla-extract/css' ||
603✔
274
      source === '@vanilla-extract/recipes' ||
73!
NEW
275
      source.startsWith('@vanilla-extract/')
×
276
    );
277
  }
603✔
278

279
  private categorizeFunction(localName: string, importedName: string): void {
1✔
280
    switch (importedName) {
680✔
281
      case 'style':
680✔
282
      case 'styleVariants':
680✔
283
        this.trackedFunctions.styleFunctions.add(localName);
439✔
284
        break;
439✔
285
      case 'recipe':
680✔
286
        this.trackedFunctions.recipeFunctions.add(localName);
85✔
287
        break;
85✔
288
      case 'fontFace':
680✔
289
      case 'globalFontFace':
680✔
290
        this.trackedFunctions.fontFaceFunctions.add(localName);
70✔
291
        break;
70✔
292
      case 'globalStyle':
680✔
293
      case 'globalKeyframes':
680✔
294
        this.trackedFunctions.globalFunctions.add(localName);
62✔
295
        break;
62✔
296
      case 'keyframes':
680✔
297
        this.trackedFunctions.keyframeFunctions.add(localName);
24✔
298
        break;
24✔
299
    }
680✔
300
  }
680✔
301
}
1✔
302

303
/**
304
 * Creates a visitor that tracks vanilla-extract imports and bindings
305
 */
306
export function createReferenceTrackingVisitor(tracker: ReferenceTracker): Rule.RuleListener {
1✔
307
  return {
595✔
308
    ImportDeclaration(node: Rule.Node) {
595✔
309
      tracker.processImportDeclaration(node as TSESTree.ImportDeclaration);
603✔
310
    },
603✔
311

312
    VariableDeclarator(node: Rule.Node) {
595✔
313
      tracker.processVariableDeclarator(node as TSESTree.VariableDeclarator);
546✔
314
    },
546✔
315

316
    FunctionDeclaration(node: Rule.Node) {
595✔
317
      tracker.processFunctionDeclaration(node as TSESTree.FunctionDeclaration);
2✔
318
    },
2✔
319
  };
595✔
320
}
595✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc