uilint-coverage 0.2.150

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.
@@ -0,0 +1,1948 @@
1
+ // src/eslint-rules/require-test-coverage/index.ts
2
+ import { createRule, defineRuleMeta } from "uilint-eslint";
3
+ import { existsSync as existsSync4, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
4
+ import { dirname as dirname2, join as join2, basename as basename2, relative } from "path";
5
+ import { execSync } from "child_process";
6
+
7
+ // src/eslint-rules/require-test-coverage/lib/file-categorizer.ts
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { basename } from "path";
10
+ import { parse } from "@typescript-eslint/typescript-estree";
11
+ function categorizeFile(filePath, _projectRoot) {
12
+ const fileName = basename(filePath);
13
+ if (filePath.endsWith(".d.ts")) {
14
+ return {
15
+ category: "type",
16
+ weight: 0,
17
+ reason: "TypeScript declaration file (.d.ts)"
18
+ };
19
+ }
20
+ if (/^use[A-Z]/.test(fileName)) {
21
+ return {
22
+ category: "core",
23
+ weight: 1,
24
+ reason: "React hook (use* pattern)"
25
+ };
26
+ }
27
+ if (/\.service\.(ts|tsx)$/.test(fileName)) {
28
+ return {
29
+ category: "core",
30
+ weight: 1,
31
+ reason: "Service file (*.service.ts pattern)"
32
+ };
33
+ }
34
+ if (/\.store\.(ts|tsx)$/.test(fileName)) {
35
+ return {
36
+ category: "core",
37
+ weight: 1,
38
+ reason: "Store file (*.store.ts pattern)"
39
+ };
40
+ }
41
+ if (/\.api\.(ts|tsx)$/.test(fileName)) {
42
+ return {
43
+ category: "core",
44
+ weight: 1,
45
+ reason: "API file (*.api.ts pattern)"
46
+ };
47
+ }
48
+ if (!existsSync(filePath)) {
49
+ return {
50
+ category: "utility",
51
+ weight: 0.5,
52
+ reason: "File not found, defaulting to utility"
53
+ };
54
+ }
55
+ let ast;
56
+ try {
57
+ const content = readFileSync(filePath, "utf-8");
58
+ ast = parse(content, {
59
+ jsx: true,
60
+ loc: true,
61
+ range: true
62
+ });
63
+ } catch {
64
+ return {
65
+ category: "utility",
66
+ weight: 0.5,
67
+ reason: "Failed to parse file, defaulting to utility"
68
+ };
69
+ }
70
+ const analysis = analyzeExports(ast);
71
+ if (analysis.hasOnlyTypeExports) {
72
+ return {
73
+ category: "type",
74
+ weight: 0,
75
+ reason: "File contains only type/interface exports"
76
+ };
77
+ }
78
+ if (analysis.hasJSX) {
79
+ return {
80
+ category: "core",
81
+ weight: 1,
82
+ reason: "React component (contains JSX)"
83
+ };
84
+ }
85
+ if (analysis.hasOnlyConstantExports) {
86
+ return {
87
+ category: "constant",
88
+ weight: 0.25,
89
+ reason: "File contains only constant/enum exports"
90
+ };
91
+ }
92
+ return {
93
+ category: "utility",
94
+ weight: 0.5,
95
+ reason: "General utility file with function exports"
96
+ };
97
+ }
98
+ function analyzeExports(ast) {
99
+ let hasFunctionExports = false;
100
+ let hasConstExports = false;
101
+ let hasTypeExports = false;
102
+ let hasJSX = false;
103
+ function visit(node) {
104
+ if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "JSXText") {
105
+ hasJSX = true;
106
+ }
107
+ if (node.type === "ExportNamedDeclaration") {
108
+ const decl = node.declaration;
109
+ if (node.exportKind === "type" || decl?.type === "TSTypeAliasDeclaration" || decl?.type === "TSInterfaceDeclaration") {
110
+ hasTypeExports = true;
111
+ } else if (decl?.type === "FunctionDeclaration" || decl?.type === "VariableDeclaration" && decl.declarations.some(
112
+ (d) => d.init?.type === "ArrowFunctionExpression" || d.init?.type === "FunctionExpression"
113
+ )) {
114
+ hasFunctionExports = true;
115
+ } else if (decl?.type === "VariableDeclaration" || decl?.type === "TSEnumDeclaration") {
116
+ if (decl.type === "VariableDeclaration") {
117
+ const hasNonFunctionInit = decl.declarations.some(
118
+ (d) => d.init && d.init.type !== "ArrowFunctionExpression" && d.init.type !== "FunctionExpression"
119
+ );
120
+ if (hasNonFunctionInit) {
121
+ hasConstExports = true;
122
+ }
123
+ } else {
124
+ hasConstExports = true;
125
+ }
126
+ } else if (!decl && node.specifiers.length > 0) {
127
+ hasFunctionExports = true;
128
+ }
129
+ }
130
+ if (node.type === "ExportDefaultDeclaration") {
131
+ const decl = node.declaration;
132
+ if (decl.type === "FunctionDeclaration" || decl.type === "ArrowFunctionExpression" || decl.type === "FunctionExpression") {
133
+ hasFunctionExports = true;
134
+ } else if (decl.type === "ClassDeclaration") {
135
+ hasFunctionExports = true;
136
+ } else {
137
+ hasConstExports = true;
138
+ }
139
+ }
140
+ for (const key of Object.keys(node)) {
141
+ const child = node[key];
142
+ if (child && typeof child === "object") {
143
+ if (Array.isArray(child)) {
144
+ for (const item of child) {
145
+ if (item && typeof item === "object" && "type" in item) {
146
+ visit(item);
147
+ }
148
+ }
149
+ } else if ("type" in child) {
150
+ visit(child);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ for (const node of ast.body) {
156
+ visit(node);
157
+ }
158
+ const hasOnlyTypeExports = hasTypeExports && !hasFunctionExports && !hasConstExports;
159
+ const hasOnlyConstantExports = hasConstExports && !hasFunctionExports && !hasTypeExports;
160
+ return {
161
+ hasOnlyTypeExports,
162
+ hasOnlyConstantExports,
163
+ hasJSX,
164
+ hasFunctionExports,
165
+ hasConstExports,
166
+ hasTypeExports
167
+ };
168
+ }
169
+
170
+ // src/eslint-rules/require-test-coverage/lib/dependency-graph.ts
171
+ import { existsSync as existsSync3, statSync } from "fs";
172
+
173
+ // src/eslint-rules/require-test-coverage/lib/export-resolver.ts
174
+ import { ResolverFactory } from "oxc-resolver";
175
+ import { parse as parse2 } from "@typescript-eslint/typescript-estree";
176
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
177
+ import { dirname, join } from "path";
178
+ var resolverFactory = null;
179
+ var astCache = /* @__PURE__ */ new Map();
180
+ var resolvedPathCache = /* @__PURE__ */ new Map();
181
+ function getResolverFactory() {
182
+ if (!resolverFactory) {
183
+ resolverFactory = new ResolverFactory({
184
+ extensions: [".tsx", ".ts", ".jsx", ".js"],
185
+ mainFields: ["module", "main"],
186
+ conditionNames: ["import", "require", "node", "default"],
187
+ // Enable TypeScript path resolution
188
+ tsconfig: {
189
+ configFile: "tsconfig.json",
190
+ references: "auto"
191
+ }
192
+ });
193
+ }
194
+ return resolverFactory;
195
+ }
196
+ function resolveImportPath(importSource, fromFile) {
197
+ const cacheKey = `${fromFile}::${importSource}`;
198
+ if (resolvedPathCache.has(cacheKey)) {
199
+ return resolvedPathCache.get(cacheKey) ?? null;
200
+ }
201
+ if (importSource.startsWith("react") || importSource.startsWith("next") || !importSource.startsWith(".") && !importSource.startsWith("@/") && !importSource.startsWith("~/")) {
202
+ if (importSource.includes("@mui/") || importSource.includes("@chakra-ui/") || importSource.includes("antd") || importSource.includes("@radix-ui/")) {
203
+ resolvedPathCache.set(cacheKey, null);
204
+ return null;
205
+ }
206
+ resolvedPathCache.set(cacheKey, null);
207
+ return null;
208
+ }
209
+ try {
210
+ const factory = getResolverFactory();
211
+ const fromDir = dirname(fromFile);
212
+ const result = factory.sync(fromDir, importSource);
213
+ if (result.path) {
214
+ resolvedPathCache.set(cacheKey, result.path);
215
+ return result.path;
216
+ }
217
+ } catch {
218
+ const resolved = manualResolve(importSource, fromFile);
219
+ resolvedPathCache.set(cacheKey, resolved);
220
+ return resolved;
221
+ }
222
+ resolvedPathCache.set(cacheKey, null);
223
+ return null;
224
+ }
225
+ function manualResolve(importSource, fromFile) {
226
+ const fromDir = dirname(fromFile);
227
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
228
+ if (importSource.startsWith("@/")) {
229
+ const projectRoot = findProjectRoot(fromFile);
230
+ if (projectRoot) {
231
+ const relativePath = importSource.slice(2);
232
+ for (const ext of extensions) {
233
+ const candidate = join(projectRoot, relativePath + ext);
234
+ if (existsSync2(candidate)) {
235
+ return candidate;
236
+ }
237
+ const indexCandidate = join(projectRoot, relativePath, `index${ext}`);
238
+ if (existsSync2(indexCandidate)) {
239
+ return indexCandidate;
240
+ }
241
+ }
242
+ }
243
+ }
244
+ if (importSource.startsWith(".")) {
245
+ for (const ext of extensions) {
246
+ const candidate = join(fromDir, importSource + ext);
247
+ if (existsSync2(candidate)) {
248
+ return candidate;
249
+ }
250
+ const indexCandidate = join(fromDir, importSource, `index${ext}`);
251
+ if (existsSync2(indexCandidate)) {
252
+ return indexCandidate;
253
+ }
254
+ }
255
+ }
256
+ return null;
257
+ }
258
+ function findProjectRoot(fromFile) {
259
+ let dir = dirname(fromFile);
260
+ const root = "/";
261
+ while (dir !== root) {
262
+ if (existsSync2(join(dir, "tsconfig.json"))) {
263
+ return dir;
264
+ }
265
+ if (existsSync2(join(dir, "package.json"))) {
266
+ return dir;
267
+ }
268
+ dir = dirname(dir);
269
+ }
270
+ return null;
271
+ }
272
+ function parseFile(filePath) {
273
+ if (astCache.has(filePath)) {
274
+ return astCache.get(filePath);
275
+ }
276
+ try {
277
+ const content = readFileSync2(filePath, "utf-8");
278
+ const ast = parse2(content, {
279
+ jsx: true,
280
+ loc: true,
281
+ range: true
282
+ });
283
+ astCache.set(filePath, ast);
284
+ return ast;
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+
290
+ // src/eslint-rules/require-test-coverage/lib/dependency-graph.ts
291
+ var dependencyCache = /* @__PURE__ */ new Map();
292
+ function buildDependencyGraph(entryFile, projectRoot) {
293
+ const cached = dependencyCache.get(entryFile);
294
+ if (cached) {
295
+ try {
296
+ const currentMtime = statSync(entryFile).mtimeMs;
297
+ if (currentMtime === cached.mtime) {
298
+ return cached.graph;
299
+ }
300
+ } catch {
301
+ }
302
+ }
303
+ const allDependencies = /* @__PURE__ */ new Set();
304
+ const visited = /* @__PURE__ */ new Set();
305
+ collectDependencies(entryFile, projectRoot, allDependencies, visited);
306
+ const graph = {
307
+ root: entryFile,
308
+ allDependencies
309
+ };
310
+ try {
311
+ const mtime = statSync(entryFile).mtimeMs;
312
+ dependencyCache.set(entryFile, { graph, mtime });
313
+ } catch {
314
+ }
315
+ return graph;
316
+ }
317
+ function collectDependencies(filePath, projectRoot, allDependencies, visited) {
318
+ if (visited.has(filePath)) {
319
+ return;
320
+ }
321
+ visited.add(filePath);
322
+ const imports = extractImports(filePath);
323
+ for (const importSource of imports) {
324
+ const resolvedPath = resolveImportPath(importSource, filePath);
325
+ if (!resolvedPath) {
326
+ continue;
327
+ }
328
+ if (resolvedPath.includes("node_modules")) {
329
+ continue;
330
+ }
331
+ if (!resolvedPath.startsWith(projectRoot)) {
332
+ continue;
333
+ }
334
+ if (visited.has(resolvedPath)) {
335
+ continue;
336
+ }
337
+ allDependencies.add(resolvedPath);
338
+ collectDependencies(resolvedPath, projectRoot, allDependencies, visited);
339
+ }
340
+ }
341
+ function extractImports(filePath) {
342
+ if (!existsSync3(filePath)) {
343
+ return [];
344
+ }
345
+ const ast = parseFile(filePath);
346
+ if (!ast) {
347
+ return [];
348
+ }
349
+ const imports = [];
350
+ for (const node of ast.body) {
351
+ if (node.type === "ImportDeclaration" && node.source.value) {
352
+ imports.push(node.source.value);
353
+ }
354
+ if (node.type === "ExportNamedDeclaration" && node.source?.value) {
355
+ imports.push(node.source.value);
356
+ }
357
+ if (node.type === "ExportAllDeclaration" && node.source.value) {
358
+ imports.push(node.source.value);
359
+ }
360
+ }
361
+ const dynamicImports = extractDynamicImports(ast);
362
+ imports.push(...dynamicImports);
363
+ return imports;
364
+ }
365
+ function extractDynamicImports(ast) {
366
+ const imports = [];
367
+ function visit(node) {
368
+ if (node.type === "ImportExpression" && node.source.type === "Literal" && typeof node.source.value === "string") {
369
+ imports.push(node.source.value);
370
+ }
371
+ for (const key of Object.keys(node)) {
372
+ const child = node[key];
373
+ if (child && typeof child === "object") {
374
+ if (Array.isArray(child)) {
375
+ for (const item of child) {
376
+ if (item && typeof item === "object" && "type" in item) {
377
+ visit(item);
378
+ }
379
+ }
380
+ } else if ("type" in child) {
381
+ visit(child);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ for (const node of ast.body) {
387
+ visit(node);
388
+ }
389
+ return imports;
390
+ }
391
+
392
+ // src/eslint-rules/require-test-coverage/lib/coverage-aggregator.ts
393
+ function aggregateCoverage(componentFile, projectRoot, coverageData) {
394
+ const graph = buildDependencyGraph(componentFile, projectRoot);
395
+ const allFiles = /* @__PURE__ */ new Set([componentFile, ...graph.allDependencies]);
396
+ const filesAnalyzed = [];
397
+ const uncoveredFiles = [];
398
+ let lowestCoverageFile = null;
399
+ let componentCoverageInfo = null;
400
+ for (const filePath of allFiles) {
401
+ const coverageInfo = getFileCoverage(filePath, projectRoot, coverageData);
402
+ filesAnalyzed.push(coverageInfo);
403
+ if (filePath === componentFile) {
404
+ componentCoverageInfo = coverageInfo;
405
+ }
406
+ if (coverageInfo.percentage === 0 && coverageInfo.statements.total > 0) {
407
+ uncoveredFiles.push(filePath);
408
+ }
409
+ if (coverageInfo.percentage > 0 && coverageInfo.weight > 0 && coverageInfo.statements.total > 0) {
410
+ if (!lowestCoverageFile || coverageInfo.percentage < lowestCoverageFile.percentage) {
411
+ lowestCoverageFile = {
412
+ path: filePath,
413
+ percentage: coverageInfo.percentage
414
+ };
415
+ }
416
+ }
417
+ }
418
+ const aggregateCoverageValue = calculateWeightedCoverage(filesAnalyzed);
419
+ return {
420
+ componentFile,
421
+ componentCoverage: componentCoverageInfo?.percentage ?? 0,
422
+ aggregateCoverage: aggregateCoverageValue,
423
+ totalFiles: filesAnalyzed.length,
424
+ filesAnalyzed,
425
+ uncoveredFiles,
426
+ lowestCoverageFile
427
+ };
428
+ }
429
+ function getFileCoverage(filePath, projectRoot, coverageData) {
430
+ const categoryResult = categorizeFile(filePath, projectRoot);
431
+ const fileCoverage = findCoverageForFile(filePath, coverageData, projectRoot);
432
+ if (!fileCoverage) {
433
+ return {
434
+ filePath,
435
+ category: categoryResult.category,
436
+ weight: categoryResult.weight,
437
+ statements: { covered: 0, total: 0 },
438
+ percentage: 0
439
+ };
440
+ }
441
+ const statementHits = fileCoverage.s;
442
+ const totalStatements = Object.keys(statementHits).length;
443
+ const coveredStatements = Object.values(statementHits).filter(
444
+ (hits) => hits > 0
445
+ ).length;
446
+ const percentage = totalStatements > 0 ? coveredStatements / totalStatements * 100 : 0;
447
+ return {
448
+ filePath,
449
+ category: categoryResult.category,
450
+ weight: categoryResult.weight,
451
+ statements: { covered: coveredStatements, total: totalStatements },
452
+ percentage: Math.round(percentage * 100) / 100
453
+ // Round to 2 decimal places
454
+ };
455
+ }
456
+ function findCoverageForFile(filePath, coverageData, projectRoot) {
457
+ if (coverageData[filePath]) {
458
+ return coverageData[filePath];
459
+ }
460
+ const relativePath = filePath.startsWith(projectRoot) ? filePath.slice(projectRoot.length) : filePath;
461
+ const pathVariants = [
462
+ relativePath,
463
+ relativePath.startsWith("/") ? relativePath.slice(1) : `/${relativePath}`,
464
+ relativePath.startsWith("/") ? relativePath : `/${relativePath}`
465
+ ];
466
+ for (const variant of pathVariants) {
467
+ if (coverageData[variant]) {
468
+ return coverageData[variant];
469
+ }
470
+ }
471
+ for (const [coveragePath, coverage] of Object.entries(coverageData)) {
472
+ if (coveragePath.endsWith(relativePath) || coveragePath.endsWith(relativePath.slice(1))) {
473
+ return coverage;
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ function calculateWeightedCoverage(files) {
479
+ let totalWeightedStatements = 0;
480
+ let totalWeightedCovered = 0;
481
+ for (const file of files) {
482
+ if (file.weight === 0) {
483
+ continue;
484
+ }
485
+ if (file.statements.total === 0) {
486
+ continue;
487
+ }
488
+ const weightedTotal = file.statements.total * file.weight;
489
+ const weightedCovered = file.statements.covered * file.weight;
490
+ totalWeightedStatements += weightedTotal;
491
+ totalWeightedCovered += weightedCovered;
492
+ }
493
+ if (totalWeightedStatements === 0) {
494
+ return 0;
495
+ }
496
+ const percentage = totalWeightedCovered / totalWeightedStatements * 100;
497
+ return Math.round(percentage * 100) / 100;
498
+ }
499
+
500
+ // src/eslint-rules/require-test-coverage/lib/jsx-coverage-analyzer.ts
501
+ function buildDataLoc(filePath, loc) {
502
+ return `${filePath}:${loc.start.line}:${loc.start.column}`;
503
+ }
504
+ function findStatementsInRange(loc, fileCoverage) {
505
+ const overlappingStatements = /* @__PURE__ */ new Set();
506
+ for (const [statementId, statementLoc] of Object.entries(
507
+ fileCoverage.statementMap
508
+ )) {
509
+ const statementStart = statementLoc.start.line;
510
+ const statementEnd = statementLoc.end.line;
511
+ const locStart = loc.start.line;
512
+ const locEnd = loc.end.line;
513
+ if (statementStart <= locEnd && locStart <= statementEnd) {
514
+ overlappingStatements.add(statementId);
515
+ }
516
+ }
517
+ return overlappingStatements;
518
+ }
519
+ function calculateCoverageFromStatements(statementIds, fileCoverage) {
520
+ if (statementIds.size === 0) {
521
+ return { covered: 0, total: 0, percentage: 0 };
522
+ }
523
+ let covered = 0;
524
+ const total = statementIds.size;
525
+ for (const statementId of statementIds) {
526
+ const hitCount = fileCoverage.s[statementId];
527
+ if (hitCount !== void 0 && hitCount > 0) {
528
+ covered++;
529
+ }
530
+ }
531
+ const percentage = total > 0 ? Math.round(covered / total * 100) : 0;
532
+ return { covered, total, percentage };
533
+ }
534
+ function findCoverageForFile2(coverage, filePath) {
535
+ if (coverage[filePath]) {
536
+ return coverage[filePath];
537
+ }
538
+ const normalizedPath = filePath.replace(/^\/+/, "");
539
+ const pathVariants = [
540
+ normalizedPath,
541
+ `/${normalizedPath}`,
542
+ filePath
543
+ ];
544
+ for (const variant of pathVariants) {
545
+ if (coverage[variant]) {
546
+ return coverage[variant];
547
+ }
548
+ }
549
+ for (const [coveragePath, fileCoverage] of Object.entries(coverage)) {
550
+ const normalizedCoveragePath = coveragePath.replace(/^\/+/, "");
551
+ if (normalizedCoveragePath.endsWith(normalizedPath) || normalizedPath.endsWith(normalizedCoveragePath)) {
552
+ return fileCoverage;
553
+ }
554
+ }
555
+ return void 0;
556
+ }
557
+ function isEventHandlerAttribute(attr) {
558
+ if (attr.type === "JSXSpreadAttribute") {
559
+ return false;
560
+ }
561
+ if (attr.name.type !== "JSXIdentifier") {
562
+ return false;
563
+ }
564
+ const name = attr.name.name;
565
+ return /^on[A-Z]/.test(name);
566
+ }
567
+ function analyzeJSXElementCoverage(jsxNode, filePath, coverage, ancestors = [], projectRoot) {
568
+ const loc = jsxNode.loc;
569
+ const dataLoc = buildDataLoc(filePath, loc);
570
+ const eventHandlerNames = [];
571
+ for (const attr of jsxNode.openingElement.attributes) {
572
+ if (isEventHandlerAttribute(attr) && attr.type === "JSXAttribute") {
573
+ if (attr.name.type === "JSXIdentifier") {
574
+ eventHandlerNames.push(attr.name.name);
575
+ }
576
+ }
577
+ }
578
+ const hasEventHandlers = eventHandlerNames.length > 0;
579
+ const fileCoverage = findCoverageForFile2(coverage, filePath);
580
+ if (!fileCoverage) {
581
+ return {
582
+ dataLoc,
583
+ hasEventHandlers,
584
+ eventHandlerNames,
585
+ coverage: { covered: 0, total: 0, percentage: 0 },
586
+ isCovered: false
587
+ };
588
+ }
589
+ const statementIds = findStatementsInRange(loc, fileCoverage);
590
+ if (hasEventHandlers) {
591
+ const handlerStatementIds = getHandlerStatements(
592
+ jsxNode,
593
+ fileCoverage,
594
+ ancestors
595
+ );
596
+ for (const stmtId of handlerStatementIds) {
597
+ statementIds.add(stmtId);
598
+ }
599
+ }
600
+ const conditionalAncestor = findConditionalAncestor(jsxNode, ancestors);
601
+ if (conditionalAncestor) {
602
+ const conditionalStatementIds = getConditionalStatements(
603
+ conditionalAncestor,
604
+ fileCoverage
605
+ );
606
+ for (const stmtId of conditionalStatementIds) {
607
+ statementIds.add(stmtId);
608
+ }
609
+ }
610
+ const localCoverage = calculateCoverageFromStatements(
611
+ statementIds,
612
+ fileCoverage
613
+ );
614
+ let importCoverage = { covered: 0, total: 0 };
615
+ if (projectRoot && ancestors.length > 0) {
616
+ const importPaths = findImportsUsedInJSX(jsxNode, ancestors);
617
+ if (importPaths.size > 0) {
618
+ importCoverage = aggregateImportCoverage(
619
+ importPaths,
620
+ coverage,
621
+ projectRoot,
622
+ filePath
623
+ );
624
+ }
625
+ }
626
+ const totalCovered = localCoverage.covered + importCoverage.covered;
627
+ const totalStatements = localCoverage.total + importCoverage.total;
628
+ const combinedPercentage = totalStatements > 0 ? Math.round(totalCovered / totalStatements * 100) : 0;
629
+ const coverageStats = {
630
+ covered: totalCovered,
631
+ total: totalStatements,
632
+ percentage: combinedPercentage
633
+ };
634
+ return {
635
+ dataLoc,
636
+ hasEventHandlers,
637
+ eventHandlerNames,
638
+ coverage: coverageStats,
639
+ isCovered: coverageStats.percentage > 0
640
+ };
641
+ }
642
+ function extractEventHandlerExpression(attr) {
643
+ if (!attr.value) {
644
+ return null;
645
+ }
646
+ if (attr.value.type === "Literal") {
647
+ return null;
648
+ }
649
+ if (attr.value.type === "JSXExpressionContainer") {
650
+ const expression = attr.value.expression;
651
+ if (expression.type === "JSXEmptyExpression") {
652
+ return null;
653
+ }
654
+ return expression;
655
+ }
656
+ return null;
657
+ }
658
+ function findHandlerFunctionDeclaration(identifier, ancestors) {
659
+ const targetName = identifier.name;
660
+ for (const ancestor of ancestors) {
661
+ if (ancestor.type === "FunctionDeclaration" && ancestor.id?.name === targetName) {
662
+ return ancestor.body;
663
+ }
664
+ if (ancestor.type === "VariableDeclaration") {
665
+ for (const declarator of ancestor.declarations) {
666
+ if (declarator.id.type === "Identifier" && declarator.id.name === targetName && declarator.init) {
667
+ if (declarator.init.type === "ArrowFunctionExpression" || declarator.init.type === "FunctionExpression") {
668
+ return declarator.init.body;
669
+ }
670
+ }
671
+ }
672
+ }
673
+ if (ancestor.type === "BlockStatement" || ancestor.type === "Program") {
674
+ const body = ancestor.type === "Program" ? ancestor.body : ancestor.body;
675
+ for (const statement of body) {
676
+ if (statement.type === "FunctionDeclaration" && statement.id?.name === targetName) {
677
+ return statement.body;
678
+ }
679
+ if (statement.type === "VariableDeclaration") {
680
+ for (const declarator of statement.declarations) {
681
+ if (declarator.id.type === "Identifier" && declarator.id.name === targetName && declarator.init) {
682
+ if (declarator.init.type === "ArrowFunctionExpression" || declarator.init.type === "FunctionExpression") {
683
+ return declarator.init.body;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ if (statement.type === "ExportNamedDeclaration" && statement.declaration) {
689
+ if (statement.declaration.type === "FunctionDeclaration" && statement.declaration.id?.name === targetName) {
690
+ return statement.declaration.body;
691
+ }
692
+ if (statement.declaration.type === "VariableDeclaration") {
693
+ for (const declarator of statement.declaration.declarations) {
694
+ if (declarator.id.type === "Identifier" && declarator.id.name === targetName && declarator.init) {
695
+ if (declarator.init.type === "ArrowFunctionExpression" || declarator.init.type === "FunctionExpression") {
696
+ return declarator.init.body;
697
+ }
698
+ }
699
+ }
700
+ }
701
+ }
702
+ }
703
+ }
704
+ if (ancestor.type === "ArrowFunctionExpression" || ancestor.type === "FunctionExpression" || ancestor.type === "FunctionDeclaration") {
705
+ const funcBody = ancestor.body;
706
+ if (funcBody.type === "BlockStatement") {
707
+ for (const statement of funcBody.body) {
708
+ if (statement.type === "FunctionDeclaration" && statement.id?.name === targetName) {
709
+ return statement.body;
710
+ }
711
+ if (statement.type === "VariableDeclaration") {
712
+ for (const declarator of statement.declarations) {
713
+ if (declarator.id.type === "Identifier" && declarator.id.name === targetName && declarator.init) {
714
+ if (declarator.init.type === "ArrowFunctionExpression" || declarator.init.type === "FunctionExpression") {
715
+ return declarator.init.body;
716
+ }
717
+ }
718
+ }
719
+ }
720
+ }
721
+ }
722
+ }
723
+ }
724
+ return null;
725
+ }
726
+ function getHandlerStatements(jsxNode, fileCoverage, ancestors = []) {
727
+ const handlerStatements = /* @__PURE__ */ new Set();
728
+ for (const attr of jsxNode.openingElement.attributes) {
729
+ if (!isEventHandlerAttribute(attr) || attr.type !== "JSXAttribute") {
730
+ continue;
731
+ }
732
+ const expression = extractEventHandlerExpression(attr);
733
+ if (!expression) {
734
+ continue;
735
+ }
736
+ if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression") {
737
+ const body = expression.body;
738
+ if (body.loc) {
739
+ const bodyStatements = findStatementsInRange(body.loc, fileCoverage);
740
+ for (const stmtId of bodyStatements) {
741
+ handlerStatements.add(stmtId);
742
+ }
743
+ }
744
+ continue;
745
+ }
746
+ if (expression.type === "Identifier") {
747
+ const functionBody = findHandlerFunctionDeclaration(expression, ancestors);
748
+ if (functionBody && functionBody.loc) {
749
+ const bodyStatements = findStatementsInRange(functionBody.loc, fileCoverage);
750
+ for (const stmtId of bodyStatements) {
751
+ handlerStatements.add(stmtId);
752
+ }
753
+ }
754
+ continue;
755
+ }
756
+ if (expression.type === "CallExpression" && expression.loc) {
757
+ const callStatements = findStatementsInRange(expression.loc, fileCoverage);
758
+ for (const stmtId of callStatements) {
759
+ handlerStatements.add(stmtId);
760
+ }
761
+ continue;
762
+ }
763
+ if (expression.type === "MemberExpression" && expression.loc) {
764
+ const memberStatements = findStatementsInRange(expression.loc, fileCoverage);
765
+ for (const stmtId of memberStatements) {
766
+ handlerStatements.add(stmtId);
767
+ }
768
+ }
769
+ }
770
+ return handlerStatements;
771
+ }
772
+ function findConditionalAncestor(node, ancestors) {
773
+ for (const ancestor of ancestors) {
774
+ if (ancestor.type === "LogicalExpression" && ancestor.operator === "&&") {
775
+ return ancestor;
776
+ }
777
+ if (ancestor.type === "ConditionalExpression") {
778
+ return ancestor;
779
+ }
780
+ if (ancestor.type === "JSXElement" && ancestor !== node) {
781
+ break;
782
+ }
783
+ if (ancestor.type === "ArrowFunctionExpression" || ancestor.type === "FunctionExpression" || ancestor.type === "FunctionDeclaration") {
784
+ break;
785
+ }
786
+ }
787
+ return null;
788
+ }
789
+ function getConditionalStatements(conditional, fileCoverage) {
790
+ const conditionStatements = /* @__PURE__ */ new Set();
791
+ if (conditional.type === "LogicalExpression") {
792
+ const condition = conditional.left;
793
+ if (condition.loc) {
794
+ const statements = findStatementsInRange(condition.loc, fileCoverage);
795
+ for (const stmtId of statements) {
796
+ conditionStatements.add(stmtId);
797
+ }
798
+ }
799
+ } else if (conditional.type === "ConditionalExpression") {
800
+ const condition = conditional.test;
801
+ if (condition.loc) {
802
+ const statements = findStatementsInRange(condition.loc, fileCoverage);
803
+ for (const stmtId of statements) {
804
+ conditionStatements.add(stmtId);
805
+ }
806
+ }
807
+ }
808
+ return conditionStatements;
809
+ }
810
+ function collectIdentifiersFromNode(node, identifiers) {
811
+ switch (node.type) {
812
+ case "Identifier":
813
+ identifiers.add(node.name);
814
+ break;
815
+ case "JSXIdentifier":
816
+ identifiers.add(node.name);
817
+ break;
818
+ case "JSXExpressionContainer":
819
+ if (node.expression.type !== "JSXEmptyExpression") {
820
+ collectIdentifiersFromNode(node.expression, identifiers);
821
+ }
822
+ break;
823
+ case "JSXElement":
824
+ collectIdentifiersFromNode(node.openingElement, identifiers);
825
+ for (const child of node.children) {
826
+ collectIdentifiersFromNode(child, identifiers);
827
+ }
828
+ break;
829
+ case "JSXOpeningElement":
830
+ collectIdentifiersFromNode(node.name, identifiers);
831
+ for (const attr of node.attributes) {
832
+ collectIdentifiersFromNode(attr, identifiers);
833
+ }
834
+ break;
835
+ case "JSXAttribute":
836
+ if (node.value) {
837
+ collectIdentifiersFromNode(node.value, identifiers);
838
+ }
839
+ break;
840
+ case "JSXSpreadAttribute":
841
+ collectIdentifiersFromNode(node.argument, identifiers);
842
+ break;
843
+ case "JSXMemberExpression":
844
+ collectIdentifiersFromNode(node.object, identifiers);
845
+ break;
846
+ case "CallExpression":
847
+ collectIdentifiersFromNode(node.callee, identifiers);
848
+ for (const arg of node.arguments) {
849
+ collectIdentifiersFromNode(arg, identifiers);
850
+ }
851
+ break;
852
+ case "MemberExpression":
853
+ collectIdentifiersFromNode(node.object, identifiers);
854
+ break;
855
+ case "ArrowFunctionExpression":
856
+ case "FunctionExpression":
857
+ collectIdentifiersFromNode(node.body, identifiers);
858
+ break;
859
+ case "BlockStatement":
860
+ for (const statement of node.body) {
861
+ collectIdentifiersFromNode(statement, identifiers);
862
+ }
863
+ break;
864
+ case "ExpressionStatement":
865
+ collectIdentifiersFromNode(node.expression, identifiers);
866
+ break;
867
+ case "ReturnStatement":
868
+ if (node.argument) {
869
+ collectIdentifiersFromNode(node.argument, identifiers);
870
+ }
871
+ break;
872
+ case "BinaryExpression":
873
+ case "LogicalExpression":
874
+ collectIdentifiersFromNode(node.left, identifiers);
875
+ collectIdentifiersFromNode(node.right, identifiers);
876
+ break;
877
+ case "ConditionalExpression":
878
+ collectIdentifiersFromNode(node.test, identifiers);
879
+ collectIdentifiersFromNode(node.consequent, identifiers);
880
+ collectIdentifiersFromNode(node.alternate, identifiers);
881
+ break;
882
+ case "UnaryExpression":
883
+ collectIdentifiersFromNode(node.argument, identifiers);
884
+ break;
885
+ case "TemplateLiteral":
886
+ for (const expr of node.expressions) {
887
+ collectIdentifiersFromNode(expr, identifiers);
888
+ }
889
+ break;
890
+ case "ArrayExpression":
891
+ for (const element of node.elements) {
892
+ if (element) {
893
+ collectIdentifiersFromNode(element, identifiers);
894
+ }
895
+ }
896
+ break;
897
+ case "ObjectExpression":
898
+ for (const prop of node.properties) {
899
+ collectIdentifiersFromNode(prop, identifiers);
900
+ }
901
+ break;
902
+ case "Property":
903
+ collectIdentifiersFromNode(node.value, identifiers);
904
+ break;
905
+ case "SpreadElement":
906
+ collectIdentifiersFromNode(node.argument, identifiers);
907
+ break;
908
+ case "JSXText":
909
+ case "JSXFragment":
910
+ case "Literal":
911
+ break;
912
+ default:
913
+ break;
914
+ }
915
+ }
916
+ function findImportsUsedInJSX(jsxNode, ancestors) {
917
+ const importPaths = /* @__PURE__ */ new Set();
918
+ const usedIdentifiers = /* @__PURE__ */ new Set();
919
+ collectIdentifiersFromNode(jsxNode, usedIdentifiers);
920
+ let programNode = null;
921
+ for (const ancestor of ancestors) {
922
+ if (ancestor.type === "Program") {
923
+ programNode = ancestor;
924
+ break;
925
+ }
926
+ }
927
+ if (!programNode) {
928
+ return importPaths;
929
+ }
930
+ const importedIdentifiers = /* @__PURE__ */ new Map();
931
+ for (const statement of programNode.body) {
932
+ if (statement.type === "ImportDeclaration") {
933
+ const source = statement.source.value;
934
+ if (typeof source !== "string") {
935
+ continue;
936
+ }
937
+ for (const specifier of statement.specifiers) {
938
+ switch (specifier.type) {
939
+ case "ImportDefaultSpecifier":
940
+ importedIdentifiers.set(specifier.local.name, source);
941
+ break;
942
+ case "ImportSpecifier":
943
+ importedIdentifiers.set(specifier.local.name, source);
944
+ break;
945
+ case "ImportNamespaceSpecifier":
946
+ importedIdentifiers.set(specifier.local.name, source);
947
+ break;
948
+ }
949
+ }
950
+ }
951
+ }
952
+ for (const identifier of usedIdentifiers) {
953
+ const importSource = importedIdentifiers.get(identifier);
954
+ if (importSource) {
955
+ importPaths.add(importSource);
956
+ }
957
+ }
958
+ return importPaths;
959
+ }
960
+ function resolveImportPath2(importPath, currentFilePath, projectRoot) {
961
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
962
+ return null;
963
+ }
964
+ const lastSlashIndex = currentFilePath.lastIndexOf("/");
965
+ const currentDir = lastSlashIndex >= 0 ? currentFilePath.slice(0, lastSlashIndex) : projectRoot;
966
+ let resolvedPath;
967
+ if (importPath.startsWith("/")) {
968
+ resolvedPath = projectRoot + importPath;
969
+ } else {
970
+ const parts = currentDir.split("/").filter(Boolean);
971
+ const importParts = importPath.split("/");
972
+ for (const part of importParts) {
973
+ if (part === ".") {
974
+ continue;
975
+ } else if (part === "..") {
976
+ parts.pop();
977
+ } else {
978
+ parts.push(part);
979
+ }
980
+ }
981
+ resolvedPath = "/" + parts.join("/");
982
+ }
983
+ const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
984
+ for (const ext of extensions) {
985
+ const fullPath = resolvedPath + ext;
986
+ if (ext === "" && (resolvedPath.endsWith(".ts") || resolvedPath.endsWith(".tsx") || resolvedPath.endsWith(".js") || resolvedPath.endsWith(".jsx"))) {
987
+ return resolvedPath;
988
+ }
989
+ if (ext !== "") {
990
+ return fullPath;
991
+ }
992
+ }
993
+ return resolvedPath;
994
+ }
995
+ function aggregateImportCoverage(importPaths, coverage, projectRoot, currentFilePath = "") {
996
+ let totalCovered = 0;
997
+ let totalStatements = 0;
998
+ for (const importPath of importPaths) {
999
+ const resolvedPath = resolveImportPath2(importPath, currentFilePath, projectRoot);
1000
+ if (!resolvedPath) {
1001
+ continue;
1002
+ }
1003
+ const fileCoverage = findCoverageForFile2(coverage, resolvedPath);
1004
+ if (!fileCoverage) {
1005
+ const rawCoverage = findCoverageForFile2(coverage, importPath);
1006
+ if (!rawCoverage) {
1007
+ continue;
1008
+ }
1009
+ const statementCount2 = Object.keys(rawCoverage.s).length;
1010
+ const coveredCount2 = Object.values(rawCoverage.s).filter((hits) => hits > 0).length;
1011
+ totalStatements += statementCount2;
1012
+ totalCovered += coveredCount2;
1013
+ continue;
1014
+ }
1015
+ const statementCount = Object.keys(fileCoverage.s).length;
1016
+ const coveredCount = Object.values(fileCoverage.s).filter((hits) => hits > 0).length;
1017
+ totalStatements += statementCount;
1018
+ totalCovered += coveredCount;
1019
+ }
1020
+ return { covered: totalCovered, total: totalStatements };
1021
+ }
1022
+
1023
+ // src/eslint-rules/require-test-coverage/lib/chunk-analyzer.ts
1024
+ function containsJSX(node, visited = /* @__PURE__ */ new WeakSet()) {
1025
+ if (!node || typeof node !== "object") return false;
1026
+ if (visited.has(node)) return false;
1027
+ visited.add(node);
1028
+ if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "JSXText") {
1029
+ return true;
1030
+ }
1031
+ const childKeys = [
1032
+ "body",
1033
+ "declarations",
1034
+ "declaration",
1035
+ "expression",
1036
+ "expressions",
1037
+ "argument",
1038
+ "arguments",
1039
+ "callee",
1040
+ "elements",
1041
+ "properties",
1042
+ "value",
1043
+ "init",
1044
+ "consequent",
1045
+ "alternate",
1046
+ "test",
1047
+ "left",
1048
+ "right",
1049
+ "object",
1050
+ "property",
1051
+ "children",
1052
+ "openingElement",
1053
+ "closingElement"
1054
+ ];
1055
+ for (const key of childKeys) {
1056
+ const child = node[key];
1057
+ if (child && typeof child === "object") {
1058
+ if (Array.isArray(child)) {
1059
+ for (const item of child) {
1060
+ if (item && typeof item === "object" && "type" in item) {
1061
+ if (containsJSX(item, visited)) return true;
1062
+ }
1063
+ }
1064
+ } else if ("type" in child) {
1065
+ if (containsJSX(child, visited)) return true;
1066
+ }
1067
+ }
1068
+ }
1069
+ return false;
1070
+ }
1071
+ function categorizeChunk(name, functionBody, isInStoreFile) {
1072
+ if (/^use[A-Z]/.test(name)) {
1073
+ return { category: "hook", isReactRelated: true };
1074
+ }
1075
+ if (isInStoreFile || /Store$/.test(name)) {
1076
+ return { category: "store", isReactRelated: false };
1077
+ }
1078
+ if (/^handle[A-Z]/.test(name) || /^on[A-Z]/.test(name)) {
1079
+ return { category: "handler", isReactRelated: true };
1080
+ }
1081
+ if (functionBody && containsJSX(functionBody)) {
1082
+ return { category: "component", isReactRelated: true };
1083
+ }
1084
+ return { category: "utility", isReactRelated: false };
1085
+ }
1086
+ function findFnId(name, loc, fileCoverage) {
1087
+ if (!fileCoverage) return null;
1088
+ for (const [fnId, fnInfo] of Object.entries(fileCoverage.fnMap)) {
1089
+ if (fnInfo.name === name) {
1090
+ return fnId;
1091
+ }
1092
+ if (fnInfo.decl.start.line === loc.start.line || fnInfo.loc.start.line === loc.start.line) {
1093
+ return fnId;
1094
+ }
1095
+ }
1096
+ return null;
1097
+ }
1098
+ function calculateChunkCoverage(fnId, loc, fileCoverage) {
1099
+ if (!fileCoverage) {
1100
+ return {
1101
+ functionCalled: false,
1102
+ statementsCovered: 0,
1103
+ statementsTotal: 0,
1104
+ percentage: 0
1105
+ };
1106
+ }
1107
+ const functionCalled = fnId !== null && (fileCoverage.f[fnId] ?? 0) > 0;
1108
+ const statementIds = findStatementsInRange(loc, fileCoverage);
1109
+ const stats = calculateCoverageFromStatements(statementIds, fileCoverage);
1110
+ return {
1111
+ functionCalled,
1112
+ statementsCovered: stats.covered,
1113
+ statementsTotal: stats.total,
1114
+ percentage: stats.percentage
1115
+ };
1116
+ }
1117
+ function getDeclarationLoc(loc) {
1118
+ return {
1119
+ start: loc.start,
1120
+ end: { line: loc.start.line, column: 999 }
1121
+ };
1122
+ }
1123
+ function collectExportedFunctions(ast) {
1124
+ const exports = [];
1125
+ for (const node of ast.body) {
1126
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "FunctionDeclaration" && node.declaration.id) {
1127
+ const loc = node.declaration.loc;
1128
+ exports.push({
1129
+ name: node.declaration.id.name,
1130
+ node: node.declaration,
1131
+ loc,
1132
+ declarationLoc: getDeclarationLoc(loc),
1133
+ body: node.declaration.body
1134
+ });
1135
+ }
1136
+ if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
1137
+ for (const decl of node.declaration.declarations) {
1138
+ if (decl.id.type === "Identifier" && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
1139
+ const loc = decl.init.loc;
1140
+ const declarationLoc = getDeclarationLoc(decl.loc);
1141
+ exports.push({
1142
+ name: decl.id.name,
1143
+ node: decl.init,
1144
+ loc,
1145
+ declarationLoc,
1146
+ body: decl.init.body
1147
+ });
1148
+ }
1149
+ }
1150
+ }
1151
+ if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "FunctionDeclaration") {
1152
+ const name = node.declaration.id?.name ?? "default";
1153
+ const loc = node.declaration.loc;
1154
+ exports.push({
1155
+ name,
1156
+ node: node.declaration,
1157
+ loc,
1158
+ declarationLoc: getDeclarationLoc(loc),
1159
+ body: node.declaration.body
1160
+ });
1161
+ }
1162
+ if (node.type === "ExportDefaultDeclaration" && (node.declaration.type === "ArrowFunctionExpression" || node.declaration.type === "FunctionExpression")) {
1163
+ const loc = node.declaration.loc;
1164
+ exports.push({
1165
+ name: "default",
1166
+ node: node.declaration,
1167
+ loc,
1168
+ declarationLoc: getDeclarationLoc(loc),
1169
+ body: node.declaration.body
1170
+ });
1171
+ }
1172
+ }
1173
+ return exports;
1174
+ }
1175
+ function analyzeChunks(ast, filePath, fileCoverage) {
1176
+ const isInStoreFile = /\.store\.(ts|tsx)$/.test(filePath);
1177
+ const exportedFunctions = collectExportedFunctions(ast);
1178
+ const results = [];
1179
+ for (const exported of exportedFunctions) {
1180
+ const { category, isReactRelated } = categorizeChunk(
1181
+ exported.name,
1182
+ exported.body,
1183
+ isInStoreFile
1184
+ );
1185
+ const fnId = findFnId(exported.name, exported.loc, fileCoverage);
1186
+ const coverage = calculateChunkCoverage(fnId, exported.loc, fileCoverage);
1187
+ results.push({
1188
+ name: exported.name,
1189
+ category,
1190
+ isReactRelated,
1191
+ loc: exported.loc,
1192
+ declarationLoc: exported.declarationLoc,
1193
+ fnId,
1194
+ isExport: true,
1195
+ coverage
1196
+ });
1197
+ }
1198
+ return results;
1199
+ }
1200
+ function getChunkThreshold(chunk, options) {
1201
+ const strictThreshold = options.chunkThreshold ?? 80;
1202
+ const relaxedThreshold = options.relaxedThreshold ?? 50;
1203
+ if (!options.focusNonReact) {
1204
+ return strictThreshold;
1205
+ }
1206
+ if (chunk.category === "component" || chunk.category === "handler") {
1207
+ return relaxedThreshold;
1208
+ }
1209
+ return strictThreshold;
1210
+ }
1211
+
1212
+ // src/eslint-rules/require-test-coverage/index.ts
1213
+ function simpleGlobMatch(pattern, path) {
1214
+ const normalizedPath = path.replace(/\\/g, "/");
1215
+ const normalizedPattern = pattern.replace(/\\/g, "/");
1216
+ const regexStr = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*");
1217
+ const regex = new RegExp(`^${regexStr}$`);
1218
+ return regex.test(normalizedPath);
1219
+ }
1220
+ var meta = defineRuleMeta({
1221
+ id: "require-test-coverage",
1222
+ version: "1.0.0",
1223
+ name: "Require Test Coverage",
1224
+ description: "Enforce that source files have adequate test coverage",
1225
+ defaultSeverity: "warn",
1226
+ category: "coverage",
1227
+ icon: "\u{1F9EA}",
1228
+ hint: "Ensures code has tests",
1229
+ defaultEnabled: true,
1230
+ isDirectoryBased: true,
1231
+ plugin: "coverage",
1232
+ eslintImport: "uilint-coverage/eslint-rules/require-test-coverage",
1233
+ npmDependencies: ["@vitest/coverage-v8"],
1234
+ requirements: [
1235
+ {
1236
+ type: "coverage",
1237
+ description: "Requires test coverage data",
1238
+ setupHint: "Run tests with coverage: npm test -- --coverage"
1239
+ }
1240
+ ],
1241
+ defaultOptions: [
1242
+ {
1243
+ coveragePath: "coverage/coverage-final.json",
1244
+ threshold: 80,
1245
+ thresholdsByPattern: [],
1246
+ severity: {
1247
+ noCoverage: "error",
1248
+ belowThreshold: "warn"
1249
+ },
1250
+ testPatterns: [
1251
+ ".test.ts",
1252
+ ".test.tsx",
1253
+ ".spec.ts",
1254
+ ".spec.tsx",
1255
+ "__tests__/"
1256
+ ],
1257
+ ignorePatterns: ["**/*.d.ts", "**/index.ts"],
1258
+ mode: "all",
1259
+ baseBranch: "main"
1260
+ }
1261
+ ],
1262
+ optionSchema: {
1263
+ fields: [
1264
+ {
1265
+ key: "threshold",
1266
+ label: "Coverage threshold",
1267
+ type: "number",
1268
+ defaultValue: 80,
1269
+ description: "Minimum coverage percentage required (0-100)"
1270
+ },
1271
+ {
1272
+ key: "coveragePath",
1273
+ label: "Coverage file path",
1274
+ type: "text",
1275
+ defaultValue: "coverage/coverage-final.json",
1276
+ description: "Path to Istanbul coverage JSON file"
1277
+ },
1278
+ {
1279
+ key: "mode",
1280
+ label: "Mode",
1281
+ type: "select",
1282
+ defaultValue: "all",
1283
+ options: [
1284
+ { value: "all", label: "Check all code" },
1285
+ { value: "changed", label: "Only check changed lines" }
1286
+ ],
1287
+ description: "Whether to check all code or only git-changed lines"
1288
+ },
1289
+ {
1290
+ key: "chunkCoverage",
1291
+ label: "Enable chunk-level coverage",
1292
+ type: "boolean",
1293
+ defaultValue: true,
1294
+ description: "Report coverage for individual functions instead of file level"
1295
+ },
1296
+ {
1297
+ key: "focusNonReact",
1298
+ label: "Focus on non-React code",
1299
+ type: "boolean",
1300
+ defaultValue: false,
1301
+ description: "Apply strict thresholds to utilities/stores/hooks, relaxed to components"
1302
+ },
1303
+ {
1304
+ key: "chunkThreshold",
1305
+ label: "Chunk coverage threshold",
1306
+ type: "number",
1307
+ defaultValue: 80,
1308
+ description: "Minimum coverage for utility/hook/store chunks (0-100)"
1309
+ },
1310
+ {
1311
+ key: "relaxedThreshold",
1312
+ label: "Relaxed threshold for React code",
1313
+ type: "number",
1314
+ defaultValue: 50,
1315
+ description: "Threshold for components/handlers when focusNonReact is enabled"
1316
+ },
1317
+ {
1318
+ key: "minStatements",
1319
+ label: "Minimum statements for coverage",
1320
+ type: "number",
1321
+ defaultValue: 5,
1322
+ description: "Files with fewer statements are exempt from coverage requirements"
1323
+ }
1324
+ ]
1325
+ },
1326
+ docs: `
1327
+ ## What it does
1328
+
1329
+ Enforces that source files have test coverage above a configurable threshold.
1330
+ It checks for:
1331
+ - Existence of corresponding test files
1332
+ - Coverage data in Istanbul JSON format
1333
+ - Statement coverage percentage meeting the threshold
1334
+
1335
+ ## Why it's useful
1336
+
1337
+ - **Quality Assurance**: Ensures critical code is tested
1338
+ - **Catch Regressions**: Prevents merging untested changes
1339
+ - **Configurable**: Different thresholds for different file patterns
1340
+ - **Git Integration**: Can focus only on changed lines
1341
+
1342
+ ## Configuration
1343
+
1344
+ \`\`\`js
1345
+ // eslint.config.js
1346
+ "uilint/require-test-coverage": ["warn", {
1347
+ coveragePath: "coverage/coverage-final.json",
1348
+ threshold: 80,
1349
+ thresholdsByPattern: [
1350
+ { pattern: "**/utils/*.ts", threshold: 90 },
1351
+ { pattern: "**/generated/**", threshold: 0 },
1352
+ ],
1353
+ severity: {
1354
+ noCoverage: "error",
1355
+ belowThreshold: "warn",
1356
+ },
1357
+ testPatterns: [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx", "__tests__/"],
1358
+ ignorePatterns: ["**/*.d.ts", "**/index.ts"],
1359
+ mode: "all", // or "changed"
1360
+ baseBranch: "main" // for "changed" mode
1361
+ }]
1362
+ \`\`\`
1363
+
1364
+ ## Examples
1365
+
1366
+ ### Below threshold:
1367
+ \`\`\`ts
1368
+ // src/api.ts - 40% coverage (threshold: 80%)
1369
+ export function fetchData() { ... } // Warning: Coverage below threshold
1370
+ \`\`\`
1371
+ `
1372
+ });
1373
+ var coverageCache = null;
1374
+ function clearCoverageCache() {
1375
+ coverageCache = null;
1376
+ }
1377
+ function findProjectRoot2(startPath) {
1378
+ let current = startPath;
1379
+ let lastPackageJson = null;
1380
+ while (current !== dirname2(current)) {
1381
+ if (existsSync4(join2(current, "package.json"))) {
1382
+ lastPackageJson = current;
1383
+ }
1384
+ if (existsSync4(join2(current, "coverage"))) {
1385
+ return current;
1386
+ }
1387
+ current = dirname2(current);
1388
+ }
1389
+ return lastPackageJson || startPath;
1390
+ }
1391
+ function loadCoverage(projectRoot, coveragePath) {
1392
+ const fullPath = join2(projectRoot, coveragePath);
1393
+ if (!existsSync4(fullPath)) {
1394
+ return null;
1395
+ }
1396
+ try {
1397
+ const stat = statSync2(fullPath);
1398
+ const mtime = stat.mtimeMs;
1399
+ if (coverageCache && coverageCache.projectRoot === projectRoot && coverageCache.coveragePath === coveragePath && coverageCache.mtime === mtime) {
1400
+ return coverageCache.data;
1401
+ }
1402
+ const content = readFileSync3(fullPath, "utf-8");
1403
+ const data = JSON.parse(content);
1404
+ coverageCache = {
1405
+ projectRoot,
1406
+ coveragePath,
1407
+ mtime,
1408
+ data
1409
+ };
1410
+ return data;
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ }
1415
+ function calculateCoverage(fileCoverage) {
1416
+ const statements = fileCoverage.s;
1417
+ const keys = Object.keys(statements);
1418
+ if (keys.length === 0) {
1419
+ return 100;
1420
+ }
1421
+ const covered = keys.filter((key) => statements[key] > 0).length;
1422
+ return Math.round(covered / keys.length * 100);
1423
+ }
1424
+ function shouldIgnore(filePath, ignorePatterns) {
1425
+ for (const pattern of ignorePatterns) {
1426
+ if (simpleGlobMatch(pattern, filePath)) {
1427
+ return true;
1428
+ }
1429
+ }
1430
+ return false;
1431
+ }
1432
+ function getThreshold(filePath, globalThreshold, thresholdsByPattern) {
1433
+ for (const { pattern, threshold } of thresholdsByPattern) {
1434
+ if (simpleGlobMatch(pattern, filePath)) {
1435
+ return threshold;
1436
+ }
1437
+ }
1438
+ return globalThreshold;
1439
+ }
1440
+ function getChangedLines(projectRoot, filePath, baseBranch) {
1441
+ try {
1442
+ const relPath = relative(projectRoot, filePath);
1443
+ const diff = execSync(`git diff ${baseBranch}...HEAD -- "${relPath}"`, {
1444
+ cwd: projectRoot,
1445
+ encoding: "utf-8",
1446
+ stdio: ["pipe", "pipe", "pipe"]
1447
+ });
1448
+ const changedLines = /* @__PURE__ */ new Set();
1449
+ const lines = diff.split("\n");
1450
+ let currentLine = 0;
1451
+ for (const line of lines) {
1452
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
1453
+ if (hunkMatch) {
1454
+ currentLine = parseInt(hunkMatch[1], 10);
1455
+ continue;
1456
+ }
1457
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1458
+ changedLines.add(currentLine);
1459
+ currentLine++;
1460
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
1461
+ } else if (!line.startsWith("\\")) {
1462
+ currentLine++;
1463
+ }
1464
+ }
1465
+ return changedLines;
1466
+ } catch {
1467
+ return null;
1468
+ }
1469
+ }
1470
+ function calculateChangedLinesCoverage(fileCoverage, changedLines) {
1471
+ const statementMap = fileCoverage.statementMap;
1472
+ const statements = fileCoverage.s;
1473
+ let relevantStatements = 0;
1474
+ let coveredStatements = 0;
1475
+ for (const [key, location] of Object.entries(statementMap)) {
1476
+ let isRelevant = false;
1477
+ for (let line = location.start.line; line <= location.end.line; line++) {
1478
+ if (changedLines.has(line)) {
1479
+ isRelevant = true;
1480
+ break;
1481
+ }
1482
+ }
1483
+ if (isRelevant) {
1484
+ relevantStatements++;
1485
+ if (statements[key] > 0) {
1486
+ coveredStatements++;
1487
+ }
1488
+ }
1489
+ }
1490
+ if (relevantStatements === 0) {
1491
+ return 100;
1492
+ }
1493
+ return Math.round(coveredStatements / relevantStatements * 100);
1494
+ }
1495
+ function findCoverageForFile3(coverage, filePath, projectRoot) {
1496
+ if (coverage[filePath]) {
1497
+ return coverage[filePath];
1498
+ }
1499
+ const relPath = relative(projectRoot, filePath);
1500
+ if (coverage[relPath]) {
1501
+ return coverage[relPath];
1502
+ }
1503
+ const withSlash = "/" + relPath;
1504
+ if (coverage[withSlash]) {
1505
+ return coverage[withSlash];
1506
+ }
1507
+ const srcMatch = relPath.match(/src\/.+$/);
1508
+ if (srcMatch) {
1509
+ const srcPath = "/" + srcMatch[0];
1510
+ if (coverage[srcPath]) {
1511
+ return coverage[srcPath];
1512
+ }
1513
+ }
1514
+ return null;
1515
+ }
1516
+ var require_test_coverage_default = createRule({
1517
+ name: "require-test-coverage",
1518
+ meta: {
1519
+ type: "suggestion",
1520
+ docs: {
1521
+ description: "Enforce that source files have adequate test coverage"
1522
+ },
1523
+ messages: {
1524
+ noCoverage: "No coverage data found for '{{fileName}}' in coverage report",
1525
+ belowThreshold: "Coverage for '{{fileName}}' is {{coverage}}%, below threshold of {{threshold}}%",
1526
+ noCoverageData: "Coverage data not found at '{{coveragePath}}'. Run tests with coverage first.",
1527
+ belowAggregateThreshold: "Aggregate coverage ({{coverage}}%) is below threshold ({{threshold}}%). Includes {{fileCount}} files. Lowest: {{lowestFile}} ({{lowestCoverage}}%)",
1528
+ jsxBelowThreshold: "<{{tagName}}> element coverage is {{coverage}}%, below threshold of {{threshold}}%",
1529
+ chunkBelowThreshold: "{{category}} '{{name}}' has {{coverage}}% coverage, below {{threshold}}% threshold",
1530
+ untestedFunction: "Function '{{name}}' ({{category}}) is not covered by tests"
1531
+ },
1532
+ schema: [
1533
+ {
1534
+ type: "object",
1535
+ properties: {
1536
+ coveragePath: {
1537
+ type: "string",
1538
+ description: "Path to coverage JSON file"
1539
+ },
1540
+ threshold: {
1541
+ type: "number",
1542
+ minimum: 0,
1543
+ maximum: 100,
1544
+ description: "Coverage threshold percentage"
1545
+ },
1546
+ thresholdsByPattern: {
1547
+ type: "array",
1548
+ items: {
1549
+ type: "object",
1550
+ properties: {
1551
+ pattern: { type: "string" },
1552
+ threshold: { type: "number", minimum: 0, maximum: 100 }
1553
+ },
1554
+ required: ["pattern", "threshold"],
1555
+ additionalProperties: false
1556
+ },
1557
+ description: "Pattern-specific thresholds"
1558
+ },
1559
+ severity: {
1560
+ type: "object",
1561
+ properties: {
1562
+ noCoverage: { type: "string", enum: ["error", "warn", "off"] },
1563
+ belowThreshold: {
1564
+ type: "string",
1565
+ enum: ["error", "warn", "off"]
1566
+ }
1567
+ },
1568
+ additionalProperties: false
1569
+ },
1570
+ testPatterns: {
1571
+ type: "array",
1572
+ items: { type: "string" },
1573
+ description: "Patterns to detect test files"
1574
+ },
1575
+ ignorePatterns: {
1576
+ type: "array",
1577
+ items: { type: "string" },
1578
+ description: "Glob patterns for files to ignore"
1579
+ },
1580
+ mode: {
1581
+ type: "string",
1582
+ enum: ["all", "changed"],
1583
+ description: "Check all code or only changed lines"
1584
+ },
1585
+ baseBranch: {
1586
+ type: "string",
1587
+ description: "Base branch for changed mode"
1588
+ },
1589
+ aggregateThreshold: {
1590
+ type: "number",
1591
+ minimum: 0,
1592
+ maximum: 100,
1593
+ description: "Aggregate coverage threshold for components (includes dependencies)"
1594
+ },
1595
+ aggregateSeverity: {
1596
+ type: "string",
1597
+ enum: ["error", "warn", "off"],
1598
+ description: "Severity for aggregate coverage check"
1599
+ },
1600
+ jsxThreshold: {
1601
+ type: "number",
1602
+ minimum: 0,
1603
+ maximum: 100,
1604
+ description: "JSX element coverage threshold percentage (includes event handlers)"
1605
+ },
1606
+ jsxSeverity: {
1607
+ type: "string",
1608
+ enum: ["error", "warn", "off"],
1609
+ description: "Severity for JSX element coverage check"
1610
+ },
1611
+ chunkCoverage: {
1612
+ type: "boolean",
1613
+ description: "Enable chunk-level coverage reporting (replaces file-level)"
1614
+ },
1615
+ chunkThreshold: {
1616
+ type: "number",
1617
+ minimum: 0,
1618
+ maximum: 100,
1619
+ description: "Threshold for strict categories (utility/hook/store)"
1620
+ },
1621
+ focusNonReact: {
1622
+ type: "boolean",
1623
+ description: "Focus on non-React code with relaxed thresholds for components"
1624
+ },
1625
+ relaxedThreshold: {
1626
+ type: "number",
1627
+ minimum: 0,
1628
+ maximum: 100,
1629
+ description: "Threshold for relaxed categories (component/handler)"
1630
+ },
1631
+ chunkSeverity: {
1632
+ type: "string",
1633
+ enum: ["error", "warn", "off"],
1634
+ description: "Severity for chunk coverage check"
1635
+ },
1636
+ minStatements: {
1637
+ type: "number",
1638
+ minimum: 0,
1639
+ description: "Files with fewer statements are exempt from coverage requirements"
1640
+ }
1641
+ },
1642
+ additionalProperties: false
1643
+ }
1644
+ ]
1645
+ },
1646
+ defaultOptions: [
1647
+ {
1648
+ coveragePath: "coverage/coverage-final.json",
1649
+ threshold: 80,
1650
+ thresholdsByPattern: [],
1651
+ severity: {
1652
+ noCoverage: "error",
1653
+ belowThreshold: "warn"
1654
+ },
1655
+ testPatterns: [
1656
+ ".test.ts",
1657
+ ".test.tsx",
1658
+ ".spec.ts",
1659
+ ".spec.tsx",
1660
+ "__tests__/"
1661
+ ],
1662
+ ignorePatterns: ["**/*.d.ts", "**/index.ts"],
1663
+ mode: "all",
1664
+ baseBranch: "main",
1665
+ aggregateThreshold: 70,
1666
+ aggregateSeverity: "warn",
1667
+ jsxThreshold: 50,
1668
+ jsxSeverity: "warn",
1669
+ chunkCoverage: true,
1670
+ chunkThreshold: 80,
1671
+ focusNonReact: false,
1672
+ relaxedThreshold: 50,
1673
+ chunkSeverity: "warn",
1674
+ minStatements: 5
1675
+ }
1676
+ ],
1677
+ create(context) {
1678
+ const options = context.options[0] || {};
1679
+ const coveragePath = options.coveragePath ?? "coverage/coverage-final.json";
1680
+ const threshold = options.threshold ?? 80;
1681
+ const thresholdsByPattern = options.thresholdsByPattern ?? [];
1682
+ const severity = {
1683
+ noCoverage: options.severity?.noCoverage ?? "error",
1684
+ belowThreshold: options.severity?.belowThreshold ?? "warn"
1685
+ };
1686
+ const aggregateThreshold = options.aggregateThreshold ?? 70;
1687
+ const aggregateSeverity = options.aggregateSeverity ?? "warn";
1688
+ const testPatterns = options.testPatterns ?? [
1689
+ ".test.ts",
1690
+ ".test.tsx",
1691
+ ".spec.ts",
1692
+ ".spec.tsx",
1693
+ "__tests__/"
1694
+ ];
1695
+ const ignorePatterns = options.ignorePatterns ?? [
1696
+ "**/*.d.ts",
1697
+ "**/index.ts"
1698
+ ];
1699
+ const mode = options.mode ?? "all";
1700
+ const baseBranch = options.baseBranch ?? "main";
1701
+ const jsxThreshold = options.jsxThreshold ?? 50;
1702
+ const jsxSeverity = options.jsxSeverity ?? "warn";
1703
+ const chunkCoverage = options.chunkCoverage ?? true;
1704
+ const chunkThreshold = options.chunkThreshold ?? 80;
1705
+ const focusNonReact = options.focusNonReact ?? false;
1706
+ const relaxedThreshold = options.relaxedThreshold ?? 50;
1707
+ const chunkSeverity = options.chunkSeverity ?? "warn";
1708
+ const minStatements = options.minStatements ?? 5;
1709
+ const filename = context.filename || context.getFilename();
1710
+ const projectRoot = findProjectRoot2(dirname2(filename));
1711
+ const relPath = relative(projectRoot, filename);
1712
+ if (shouldIgnore(relPath, ignorePatterns)) {
1713
+ return {};
1714
+ }
1715
+ if (testPatterns.some(
1716
+ (p) => filename.includes(p.replace("__tests__/", "__tests__"))
1717
+ )) {
1718
+ return {};
1719
+ }
1720
+ let reported = false;
1721
+ const jsxElements = [];
1722
+ return {
1723
+ // Collect JSX elements for element-level coverage analysis
1724
+ JSXElement(node) {
1725
+ const ancestors = context.sourceCode?.getAncestors?.(node) ?? [];
1726
+ jsxElements.push({ node, ancestors });
1727
+ },
1728
+ "Program:exit"(node) {
1729
+ if (reported) return;
1730
+ reported = true;
1731
+ const coverage = loadCoverage(projectRoot, coveragePath);
1732
+ if (!coverage) {
1733
+ if (severity.noCoverage !== "off") {
1734
+ context.report({
1735
+ node,
1736
+ messageId: "noCoverageData",
1737
+ data: {
1738
+ coveragePath
1739
+ }
1740
+ });
1741
+ }
1742
+ return;
1743
+ }
1744
+ const fileCoverage = findCoverageForFile3(
1745
+ coverage,
1746
+ filename,
1747
+ projectRoot
1748
+ );
1749
+ if (!fileCoverage) {
1750
+ return;
1751
+ }
1752
+ const statementCount = Object.keys(fileCoverage.s).length;
1753
+ if (statementCount < minStatements) {
1754
+ return;
1755
+ }
1756
+ let coveragePercent;
1757
+ if (mode === "changed") {
1758
+ const changedLines = getChangedLines(
1759
+ projectRoot,
1760
+ filename,
1761
+ baseBranch
1762
+ );
1763
+ if (changedLines && changedLines.size > 0) {
1764
+ coveragePercent = calculateChangedLinesCoverage(
1765
+ fileCoverage,
1766
+ changedLines
1767
+ );
1768
+ } else {
1769
+ coveragePercent = calculateCoverage(fileCoverage);
1770
+ }
1771
+ } else {
1772
+ coveragePercent = calculateCoverage(fileCoverage);
1773
+ }
1774
+ const fileThreshold = getThreshold(
1775
+ relPath,
1776
+ threshold,
1777
+ thresholdsByPattern
1778
+ );
1779
+ if (!chunkCoverage && severity.belowThreshold !== "off" && coveragePercent < fileThreshold) {
1780
+ context.report({
1781
+ node,
1782
+ messageId: "belowThreshold",
1783
+ data: {
1784
+ fileName: basename2(filename),
1785
+ coverage: String(coveragePercent),
1786
+ threshold: String(fileThreshold)
1787
+ }
1788
+ });
1789
+ }
1790
+ if (chunkCoverage && chunkSeverity !== "off" && fileCoverage) {
1791
+ const chunks = analyzeChunks(
1792
+ context.sourceCode.ast,
1793
+ filename,
1794
+ fileCoverage
1795
+ );
1796
+ for (const chunk of chunks) {
1797
+ const chunkThresholdValue = getChunkThreshold(chunk, {
1798
+ focusNonReact,
1799
+ chunkThreshold,
1800
+ relaxedThreshold
1801
+ });
1802
+ if (chunk.coverage.percentage < chunkThresholdValue) {
1803
+ const messageId = chunk.coverage.functionCalled ? "chunkBelowThreshold" : "untestedFunction";
1804
+ context.report({
1805
+ loc: chunk.declarationLoc,
1806
+ messageId,
1807
+ data: {
1808
+ name: chunk.name,
1809
+ category: chunk.category,
1810
+ coverage: String(chunk.coverage.percentage),
1811
+ threshold: String(chunkThresholdValue)
1812
+ }
1813
+ });
1814
+ }
1815
+ }
1816
+ }
1817
+ if (aggregateSeverity !== "off" && (filename.endsWith(".tsx") || filename.endsWith(".jsx"))) {
1818
+ const hasJSX = checkForJSX(context.sourceCode.ast);
1819
+ if (hasJSX) {
1820
+ const aggregateResult = aggregateCoverage(
1821
+ filename,
1822
+ projectRoot,
1823
+ coverage
1824
+ );
1825
+ if (aggregateResult.aggregateCoverage < aggregateThreshold) {
1826
+ const lowestFile = aggregateResult.lowestCoverageFile;
1827
+ context.report({
1828
+ node,
1829
+ messageId: "belowAggregateThreshold",
1830
+ data: {
1831
+ coverage: String(
1832
+ Math.round(aggregateResult.aggregateCoverage)
1833
+ ),
1834
+ threshold: String(aggregateThreshold),
1835
+ fileCount: String(aggregateResult.totalFiles),
1836
+ lowestFile: lowestFile ? basename2(lowestFile.path) : "N/A",
1837
+ lowestCoverage: lowestFile ? String(Math.round(lowestFile.percentage)) : "N/A"
1838
+ }
1839
+ });
1840
+ }
1841
+ }
1842
+ }
1843
+ if (jsxSeverity !== "off" && jsxElements.length > 0 && coverage) {
1844
+ const fileRelPath = relPath.startsWith("/") ? relPath : `/${relPath}`;
1845
+ for (const { node: jsxNode, ancestors } of jsxElements) {
1846
+ const hasEventHandlers = jsxNode.openingElement.attributes.some(
1847
+ (attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && /^on[A-Z]/.test(attr.name.name)
1848
+ );
1849
+ if (!hasEventHandlers) {
1850
+ continue;
1851
+ }
1852
+ const result = analyzeJSXElementCoverage(
1853
+ jsxNode,
1854
+ fileRelPath,
1855
+ coverage,
1856
+ ancestors,
1857
+ projectRoot
1858
+ );
1859
+ if (result.coverage.percentage < jsxThreshold) {
1860
+ const openingElement = jsxNode.openingElement;
1861
+ let tagName = "unknown";
1862
+ if (openingElement.name.type === "JSXIdentifier") {
1863
+ tagName = openingElement.name.name;
1864
+ } else if (openingElement.name.type === "JSXMemberExpression") {
1865
+ let current = openingElement.name;
1866
+ const parts = [];
1867
+ while (current.type === "JSXMemberExpression") {
1868
+ if (current.property.type === "JSXIdentifier") {
1869
+ parts.unshift(current.property.name);
1870
+ }
1871
+ current = current.object;
1872
+ }
1873
+ if (current.type === "JSXIdentifier") {
1874
+ parts.unshift(current.name);
1875
+ }
1876
+ tagName = parts.join(".");
1877
+ }
1878
+ context.report({
1879
+ node: jsxNode,
1880
+ messageId: "jsxBelowThreshold",
1881
+ data: {
1882
+ tagName,
1883
+ coverage: String(result.coverage.percentage),
1884
+ threshold: String(jsxThreshold),
1885
+ dataLoc: result.dataLoc
1886
+ }
1887
+ });
1888
+ }
1889
+ }
1890
+ }
1891
+ }
1892
+ };
1893
+ }
1894
+ });
1895
+ function checkForJSX(ast, visited = /* @__PURE__ */ new WeakSet()) {
1896
+ if (!ast || typeof ast !== "object") return false;
1897
+ if (visited.has(ast)) return false;
1898
+ visited.add(ast);
1899
+ const node = ast;
1900
+ if (node.type === "JSXElement" || node.type === "JSXFragment" || node.type === "JSXText") {
1901
+ return true;
1902
+ }
1903
+ const childKeys = [
1904
+ "body",
1905
+ "declarations",
1906
+ "declaration",
1907
+ "expression",
1908
+ "expressions",
1909
+ "argument",
1910
+ "arguments",
1911
+ "callee",
1912
+ "elements",
1913
+ "properties",
1914
+ "value",
1915
+ "init",
1916
+ "consequent",
1917
+ "alternate",
1918
+ "test",
1919
+ "left",
1920
+ "right",
1921
+ "object",
1922
+ "property",
1923
+ "children",
1924
+ "openingElement",
1925
+ "closingElement",
1926
+ "attributes"
1927
+ ];
1928
+ for (const key of childKeys) {
1929
+ const child = node[key];
1930
+ if (child && typeof child === "object") {
1931
+ if (Array.isArray(child)) {
1932
+ for (const item of child) {
1933
+ if (checkForJSX(item, visited)) return true;
1934
+ }
1935
+ } else {
1936
+ if (checkForJSX(child, visited)) return true;
1937
+ }
1938
+ }
1939
+ }
1940
+ return false;
1941
+ }
1942
+
1943
+ export {
1944
+ meta,
1945
+ clearCoverageCache,
1946
+ require_test_coverage_default
1947
+ };
1948
+ //# sourceMappingURL=chunk-MSJUSFNN.js.map