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

antebudimir / eslint-plugin-vanilla-extract / 15878762783

25 Jun 2025 02:12PM UTC coverage: 91.396% (-7.9%) from 99.254%
15878762783

Pull #4

github

web-flow
Merge 734d15aed into 35875fbb3
Pull Request #4: Add Wrapper Function Support with Reference Tracking

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