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

antebudimir / eslint-plugin-vanilla-extract / 25061938259

28 Apr 2026 03:26PM UTC coverage: 92.66% (+0.02%) from 92.645%
25061938259

Pull #9

github

web-flow
Merge da5c46801 into aec1bf7d5
Pull Request #9: feat: improve wrapper linting reliability with global settings

1111 of 1258 branches covered (88.31%)

Branch coverage included in aggregate %.

38 of 38 new or added lines in 2 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

4330 of 4614 relevant lines covered (93.84%)

346.3 hits per line

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

73.15
/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
  private style: Set<string>;
31
  private recipe: Set<string>;
32

33
  constructor(options?: { style?: string[]; recipe?: string[] }) {
1✔
34
    this.trackedFunctions = {
1,153✔
35
      styleFunctions: new Set(),
1,153✔
36
      recipeFunctions: new Set(),
1,153✔
37
      fontFaceFunctions: new Set(),
1,153✔
38
      globalFunctions: new Set(),
1,153✔
39
      keyframeFunctions: new Set(),
1,153✔
40
    };
1,153✔
41
    this.style = new Set(options?.style ?? []);
1,153✔
42
    this.recipe = new Set(options?.recipe ?? []);
1,153✔
43
  }
1,153✔
44

45
  /**
46
   * Processes import declarations to track vanilla-extract functions
47
   */
48
  processImportDeclaration(node: TSESTree.ImportDeclaration): void {
1✔
49
    const source = node.source.value;
1,241✔
50

51
    if (typeof source !== 'string') {
1,241!
UNCOV
52
      return;
×
UNCOV
53
    }
×
54

55
    const isVanillaExtractImport = this.isVanillaExtractSource(source);
1,241✔
56

57
    node.specifiers.forEach((specifier) => {
1,241✔
58
      if (specifier.type === 'ImportSpecifier') {
1,301✔
59
        const importedName =
1,301✔
60
          specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value;
1,301!
61
        const localName = specifier.local.name;
1,301✔
62
        const customWrapper = this.getCustomWrapper(importedName, localName);
1,301✔
63

64
        let trackedImportName: string;
1,301✔
65

66
        if (isVanillaExtractImport) {
1,301✔
67
          trackedImportName = importedName;
1,247✔
68
        } else {
1,301✔
69
          if (!customWrapper) {
54✔
70
            return;
18✔
71
          }
18✔
72
          trackedImportName = customWrapper;
36✔
73
        }
36✔
74

75
        const reference: ImportReference = {
1,283✔
76
          source,
1,283✔
77
          importedName: trackedImportName,
1,283✔
78
          localName,
1,283✔
79
        };
1,283✔
80

81
        this.imports.set(localName, reference);
1,283✔
82
        this.categorizeFunction(localName, trackedImportName);
1,283✔
83
      }
1,283✔
84
    });
1,241✔
85
  }
1,241✔
86

87
  /**
88
   * Processes variable declarations to track re-assignments and destructuring
89
   */
90
  processVariableDeclarator(node: TSESTree.VariableDeclarator): void {
1✔
91
    // Handle destructuring assignments like: const { style, recipe } = vanillaExtract;
92
    if (node.id.type === 'ObjectPattern' && node.init?.type === 'Identifier') {
1,092!
93
      const sourceIdentifier = node.init.name;
×
94
      const sourceReference = this.imports.get(sourceIdentifier);
×
95

96
      if (sourceReference && this.isVanillaExtractSource(sourceReference.source)) {
×
97
        node.id.properties.forEach((property) => {
×
98
          if (
×
99
            property.type === 'Property' &&
×
100
            property.key.type === 'Identifier' &&
×
101
            property.value.type === 'Identifier'
×
102
          ) {
×
103
            const importedName = property.key.name;
×
104
            const localName = property.value.name;
×
105

106
            const reference: ImportReference = {
×
107
              source: sourceReference.source,
×
108
              importedName,
×
109
              localName,
×
110
            };
×
111

112
            this.imports.set(localName, reference);
×
113
            this.categorizeFunction(localName, importedName);
×
114
          }
×
115
        });
×
116
      }
×
117
    }
×
118

119
    // Handle simple assignments like: const myStyle = style;
120
    if (node.id.type === 'Identifier' && node.init?.type === 'Identifier') {
1,092!
121
      const sourceReference = this.imports.get(node.init.name);
×
122
      if (sourceReference) {
×
123
        this.imports.set(node.id.name, sourceReference);
×
124
        this.categorizeFunction(node.id.name, sourceReference.importedName);
×
125
      }
×
126
    }
×
127

128
    // Handle arrow function assignments that wrap vanilla-extract functions
129
    if (node.id.type === 'Identifier' && node.init?.type === 'ArrowFunctionExpression') {
1,092✔
130
      this.analyzeWrapperFunction(node.id.name, node.init);
69✔
131
    }
69✔
132
  }
1,092✔
133

134
  /**
135
   * Processes function declarations to detect wrapper functions
136
   */
137
  processFunctionDeclaration(node: TSESTree.FunctionDeclaration): void {
1✔
138
    if (node.id?.name) {
4✔
139
      this.analyzeWrapperFunction(node.id.name, node);
4✔
140
    }
4✔
141
  }
4✔
142

143
  /**
144
   * Analyzes a function to see if it wraps a vanilla-extract function
145
   */
146
  private analyzeWrapperFunction(
1✔
147
    functionName: string,
73✔
148
    functionNode: TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration,
73✔
149
  ): void {
73✔
150
    const body = functionNode.body;
73✔
151

152
    // Handle arrow functions with expression body
153
    if (functionNode.type === 'ArrowFunctionExpression' && body.type !== 'BlockStatement') {
73✔
154
      this.analyzeWrapperExpression(functionName, body);
69✔
155
      return;
69✔
156
    }
69✔
157

158
    // Handle functions with block statement body
159
    if (body.type === 'BlockStatement') {
4✔
160
      this.traverseBlockForVanillaExtractCalls(functionName, body);
4✔
161
    }
4✔
162
  }
73✔
163

164
  /**
165
   * Analyzes a wrapper function expression to detect vanilla-extract calls and parameter mapping
166
   */
167
  private analyzeWrapperExpression(wrapperName: string, expression: TSESTree.Node): void {
1✔
168
    if (expression.type === 'CallExpression' && expression.callee.type === 'Identifier') {
69✔
169
      const calledFunction = expression.callee.name;
69✔
170
      if (this.isTrackedFunction(calledFunction)) {
69✔
171
        const originalName = this.getOriginalName(calledFunction);
69✔
172
        if (originalName) {
69✔
173
          // For now, create a simple wrapper info
174
          const wrapperInfo: WrapperFunctionInfo = {
69✔
175
            originalFunction: originalName,
69✔
176
            parameterMapping: 1, // layerStyle uses second parameter as the style object
69✔
177
          };
69✔
178
          this.wrapperFunctions.set(wrapperName, wrapperInfo);
69✔
179
          this.categorizeFunction(wrapperName, originalName);
69✔
180
        }
69✔
181
      }
69✔
182
    }
69✔
183
  }
69✔
184

185
  /**
186
   * Checks if a node is a vanilla-extract function call
187
   */
188
  private checkForVanillaExtractCall(wrapperName: string, node: TSESTree.Node): void {
1✔
189
    if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
4!
190
      const calledFunction = node.callee.name;
×
191
      if (this.isTrackedFunction(calledFunction)) {
×
192
        const originalName = this.getOriginalName(calledFunction);
×
193
        if (originalName) {
×
194
          const wrapperInfo: WrapperFunctionInfo = {
×
195
            originalFunction: originalName,
×
196
            parameterMapping: 0, // Default to first parameter
×
197
          };
×
198
          this.wrapperFunctions.set(wrapperName, wrapperInfo);
×
199
          this.categorizeFunction(wrapperName, originalName);
×
200
        }
×
201
      }
×
202
    }
×
203
  }
4✔
204

205
  /**
206
   * Traverses a block statement to find vanilla-extract calls
207
   */
208
  private traverseBlockForVanillaExtractCalls(wrapperName: string, block: TSESTree.BlockStatement): void {
1✔
209
    for (const statement of block.body) {
4✔
210
      if (statement.type === 'ReturnStatement' && statement.argument) {
4✔
211
        this.checkForVanillaExtractCall(wrapperName, statement.argument);
4✔
212
      } else if (statement.type === 'ExpressionStatement') {
4!
213
        this.checkForVanillaExtractCall(wrapperName, statement.expression);
×
214
      }
×
215
    }
4✔
216
  }
4✔
217

218
  /**
219
   * Checks if a function name corresponds to a tracked vanilla-extract function
220
   */
221
  isTrackedFunction(functionName: string): boolean {
1✔
222
    return this.imports.has(functionName) || this.wrapperFunctions.has(functionName);
1,499✔
223
  }
1,499✔
224

225
  /**
226
   * Gets the category of a tracked function
227
   */
228
  getFunctionCategory(functionName: string): keyof TrackedFunctions | null {
1✔
229
    if (this.trackedFunctions.styleFunctions.has(functionName)) {
×
230
      return 'styleFunctions';
×
231
    }
×
232
    if (this.trackedFunctions.recipeFunctions.has(functionName)) {
×
233
      return 'recipeFunctions';
×
234
    }
×
235
    if (this.trackedFunctions.fontFaceFunctions.has(functionName)) {
×
236
      return 'fontFaceFunctions';
×
237
    }
×
238
    if (this.trackedFunctions.globalFunctions.has(functionName)) {
×
239
      return 'globalFunctions';
×
240
    }
×
241
    if (this.trackedFunctions.keyframeFunctions.has(functionName)) {
×
242
      return 'keyframeFunctions';
×
243
    }
×
244
    return null;
×
245
  }
×
246

247
  /**
248
   * Gets the original imported name for a local function name
249
   */
250
  getOriginalName(localName: string): string | null {
1✔
251
    const reference = this.imports.get(localName);
1,391✔
252
    if (reference) {
1,391✔
253
      return reference.importedName;
1,330✔
254
    }
1,330✔
255

256
    // Check if it's a wrapper function
257
    const wrapperInfo = this.wrapperFunctions.get(localName);
61✔
258
    return wrapperInfo?.originalFunction ?? null;
1,391!
259
  }
1,391✔
260

261
  /**
262
   * Gets wrapper function information
263
   */
264
  getWrapperInfo(functionName: string): WrapperFunctionInfo | null {
1✔
265
    return this.wrapperFunctions.get(functionName) ?? null;
650✔
266
  }
650✔
267

268
  /**
269
   * Gets all tracked functions by category
270
   */
271
  getTrackedFunctions(): TrackedFunctions {
1✔
272
    return this.trackedFunctions;
×
273
  }
×
274

275
  /**
276
   * Resets the tracker state (useful for processing multiple files)
277
   */
278
  reset(): void {
1✔
279
    this.imports.clear();
×
280
    this.wrapperFunctions.clear();
×
281
    this.trackedFunctions.styleFunctions.clear();
×
282
    this.trackedFunctions.recipeFunctions.clear();
×
283
    this.trackedFunctions.fontFaceFunctions.clear();
×
284
    this.trackedFunctions.globalFunctions.clear();
×
285
    this.trackedFunctions.keyframeFunctions.clear();
×
286
  }
×
287

288
  private isVanillaExtractSource(source: string): boolean {
1✔
289
    return (
1,241✔
290
      source === '@vanilla-extract/css' ||
1,241✔
291
      source === '@vanilla-extract/recipes' ||
247✔
292
      source.startsWith('@vanilla-extract/')
98✔
293
    );
294
  }
1,241✔
295

296
  private getCustomWrapper(importedName: string, localName: string): 'style' | 'recipe' | null {
1✔
297
    if (this.style.has(importedName) || this.style.has(localName)) {
1,301✔
298
      return 'style';
24✔
299
    }
24✔
300

301
    if (this.recipe.has(importedName) || this.recipe.has(localName)) {
1,301✔
302
      return 'recipe';
12✔
303
    }
12✔
304

305
    return null;
1,265✔
306
  }
1,301✔
307

308
  private categorizeFunction(localName: string, importedName: string): void {
1✔
309
    switch (importedName) {
1,352✔
310
      case 'style':
1,352✔
311
      case 'styleVariants':
1,352✔
312
        this.trackedFunctions.styleFunctions.add(localName);
823✔
313
        break;
823✔
314
      case 'recipe':
1,352✔
315
        this.trackedFunctions.recipeFunctions.add(localName);
193✔
316
        break;
193✔
317
      case 'fontFace':
1,352✔
318
      case 'globalFontFace':
1,352✔
319
        this.trackedFunctions.fontFaceFunctions.add(localName);
114✔
320
        break;
114✔
321
      case 'globalStyle':
1,352✔
322
      case 'globalKeyframes':
1,352✔
323
        this.trackedFunctions.globalFunctions.add(localName);
102✔
324
        break;
102✔
325
      case 'keyframes':
1,352✔
326
        this.trackedFunctions.keyframeFunctions.add(localName);
32✔
327
        break;
32✔
328
    }
1,352✔
329
  }
1,352✔
330
}
1✔
331

332
/**
333
 * Creates a visitor that tracks vanilla-extract imports and bindings
334
 */
335
export function createReferenceTrackingVisitor(tracker: ReferenceTracker): Rule.RuleListener {
1✔
336
  return {
1,153✔
337
    ImportDeclaration(node: Rule.Node) {
1,153✔
338
      tracker.processImportDeclaration(node as TSESTree.ImportDeclaration);
1,241✔
339
    },
1,241✔
340

341
    VariableDeclarator(node: Rule.Node) {
1,153✔
342
      tracker.processVariableDeclarator(node as TSESTree.VariableDeclarator);
1,092✔
343
    },
1,092✔
344

345
    FunctionDeclaration(node: Rule.Node) {
1,153✔
346
      tracker.processFunctionDeclaration(node as TSESTree.FunctionDeclaration);
4✔
347
    },
4✔
348
  };
1,153✔
349
}
1,153✔
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