uilint-eslint 0.2.23 → 0.2.27

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.
Files changed (77) hide show
  1. package/dist/index.d.ts +380 -2
  2. package/dist/index.js +2075 -8
  3. package/dist/index.js.map +1 -1
  4. package/dist/rules/consistent-dark-mode.js.map +1 -1
  5. package/dist/rules/consistent-spacing.js.map +1 -1
  6. package/dist/rules/enforce-absolute-imports.js.map +1 -1
  7. package/dist/rules/no-any-in-props.js.map +1 -1
  8. package/dist/rules/no-arbitrary-tailwind.js.map +1 -1
  9. package/dist/rules/no-direct-store-import.js.map +1 -1
  10. package/dist/rules/no-mixed-component-libraries.js.map +1 -1
  11. package/dist/rules/no-prop-drilling-depth.js.map +1 -1
  12. package/dist/rules/no-secrets-in-code.js.map +1 -1
  13. package/dist/rules/no-semantic-duplicates.js +443 -0
  14. package/dist/rules/no-semantic-duplicates.js.map +1 -0
  15. package/dist/rules/prefer-zustand-state-management.js.map +1 -1
  16. package/dist/rules/require-input-validation.js.map +1 -1
  17. package/dist/rules/require-test-coverage.js +1705 -0
  18. package/dist/rules/require-test-coverage.js.map +1 -0
  19. package/dist/rules/semantic-vision.js.map +1 -1
  20. package/dist/rules/semantic.js.map +1 -1
  21. package/dist/rules/zustand-use-selectors.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/index.ts +94 -0
  24. package/src/rule-registry.ts +6 -0
  25. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/coverage/coverage-final.json +76 -0
  26. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/Component.test.tsx +8 -0
  27. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/Component.tsx +8 -0
  28. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/NonJsxFile.tsx +5 -0
  29. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/WellTestedComponent.test.tsx +8 -0
  30. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/WellTestedComponent.tsx +4 -0
  31. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/useHook.ts +14 -0
  32. package/src/rules/__fixtures__/coverage/with-aggregate-coverage/src/utils.ts +8 -0
  33. package/src/rules/__fixtures__/coverage/with-full-coverage/coverage/coverage-final.json +19 -0
  34. package/src/rules/__fixtures__/coverage/with-full-coverage/src/utils.test.ts +15 -0
  35. package/src/rules/__fixtures__/coverage/with-full-coverage/src/utils.ts +15 -0
  36. package/src/rules/__fixtures__/coverage/with-git-changes/coverage/coverage-final.json +22 -0
  37. package/src/rules/__fixtures__/coverage/with-git-changes/src/modified.ts +21 -0
  38. package/src/rules/__fixtures__/coverage/with-jsx-coverage/coverage/coverage-final.json +70 -0
  39. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/ComponentWithHandlers.test.tsx +10 -0
  40. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/ComponentWithHandlers.tsx +34 -0
  41. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/WellTestedComponent.test.tsx +10 -0
  42. package/src/rules/__fixtures__/coverage/with-jsx-coverage/src/WellTestedComponent.tsx +16 -0
  43. package/src/rules/__fixtures__/coverage/with-no-coverage-data/src/utils.ts +7 -0
  44. package/src/rules/__fixtures__/coverage/with-no-tests/coverage/coverage-final.json +15 -0
  45. package/src/rules/__fixtures__/coverage/with-no-tests/src/hasTest.test.ts +7 -0
  46. package/src/rules/__fixtures__/coverage/with-no-tests/src/hasTest.ts +7 -0
  47. package/src/rules/__fixtures__/coverage/with-no-tests/src/noTest.ts +11 -0
  48. package/src/rules/__fixtures__/coverage/with-partial-coverage/coverage/coverage-final.json +51 -0
  49. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/covered.test.ts +12 -0
  50. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/covered.ts +11 -0
  51. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/uncovered.test.ts +8 -0
  52. package/src/rules/__fixtures__/coverage/with-partial-coverage/src/uncovered.ts +28 -0
  53. package/src/rules/no-mixed-component-libraries.test.ts +2 -2
  54. package/src/rules/no-semantic-duplicates.test.ts +668 -0
  55. package/src/rules/no-semantic-duplicates.ts +618 -0
  56. package/src/rules/require-test-coverage.test.ts +631 -0
  57. package/src/rules/require-test-coverage.ts +985 -0
  58. package/src/utils/__fixtures__/dep-graph/circular/a.ts +6 -0
  59. package/src/utils/__fixtures__/dep-graph/circular/b.ts +6 -0
  60. package/src/utils/__fixtures__/dep-graph/external/component.tsx +7 -0
  61. package/src/utils/__fixtures__/dep-graph/external/local.ts +6 -0
  62. package/src/utils/__fixtures__/dep-graph/re-exports/button.tsx +9 -0
  63. package/src/utils/__fixtures__/dep-graph/re-exports/consumer.tsx +6 -0
  64. package/src/utils/__fixtures__/dep-graph/re-exports/index.ts +3 -0
  65. package/src/utils/__fixtures__/dep-graph/simple/api.ts +4 -0
  66. package/src/utils/__fixtures__/dep-graph/simple/component.tsx +8 -0
  67. package/src/utils/__fixtures__/dep-graph/simple/hook.ts +7 -0
  68. package/src/utils/__fixtures__/dep-graph/simple/utils.ts +4 -0
  69. package/src/utils/coverage-aggregator.test.ts +480 -0
  70. package/src/utils/coverage-aggregator.ts +285 -0
  71. package/src/utils/create-rule.ts +10 -0
  72. package/src/utils/dependency-graph.test.ts +256 -0
  73. package/src/utils/dependency-graph.ts +252 -0
  74. package/src/utils/file-categorizer.test.ts +393 -0
  75. package/src/utils/file-categorizer.ts +293 -0
  76. package/src/utils/jsx-coverage-analyzer.test.ts +2777 -0
  77. package/src/utils/jsx-coverage-analyzer.ts +1099 -0
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
2
2
  import { Linter } from 'eslint';
3
- import { ESLintUtils } from '@typescript-eslint/utils';
3
+ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
4
4
 
5
5
  /**
6
6
  * Rule creation helper using @typescript-eslint/utils
@@ -70,6 +70,15 @@ interface RuleMeta {
70
70
  * - Configuration options explained
71
71
  */
72
72
  docs: string;
73
+ /**
74
+ * Internal utility dependencies that this rule requires.
75
+ * When the rule is copied to a target project, these utilities
76
+ * will be transformed to import from "uilint-eslint" instead
77
+ * of relative paths.
78
+ *
79
+ * Example: ["coverage-aggregator", "dependency-graph"]
80
+ */
81
+ internalDependencies?: string[];
73
82
  }
74
83
  /**
75
84
  * Helper to define rule metadata with type safety
@@ -236,6 +245,315 @@ declare function getRuleDocs(id: string): string | undefined;
236
245
  */
237
246
  declare function getAllRuleIds(): string[];
238
247
 
248
+ /**
249
+ * File Categorizer
250
+ *
251
+ * Categorizes TypeScript/React files by their role in the codebase.
252
+ * Used for smart weighting in coverage aggregation.
253
+ *
254
+ * Categories:
255
+ * - core (1.0): hooks, components, services, stores - critical logic
256
+ * - utility (0.5): formatters, validators, helpers - supporting code
257
+ * - constant (0.25): config, constants, enums - static data
258
+ * - type (0): .d.ts files, type-only exports - no runtime impact
259
+ */
260
+ type FileCategory = "core" | "utility" | "constant" | "type";
261
+ interface FileCategoryResult {
262
+ category: FileCategory;
263
+ weight: number;
264
+ reason: string;
265
+ }
266
+ /**
267
+ * Categorize a TypeScript/React file by its role
268
+ */
269
+ declare function categorizeFile(filePath: string, _projectRoot: string): FileCategoryResult;
270
+
271
+ /**
272
+ * Coverage Aggregator
273
+ *
274
+ * Calculates weighted aggregate coverage for a component by tracing its
275
+ * dependencies and combining their coverage using smart weighting.
276
+ *
277
+ * Weighting strategy:
278
+ * - core (1.0): hooks, components, services - critical logic
279
+ * - utility (0.5): formatters, validators - supporting code
280
+ * - constant (0.25): config, constants - static data
281
+ * - type (0): .d.ts, type-only - no runtime impact
282
+ */
283
+
284
+ /**
285
+ * Istanbul coverage JSON format (matching require-test-coverage.ts)
286
+ */
287
+ interface IstanbulCoverage {
288
+ [filePath: string]: {
289
+ path: string;
290
+ statementMap: {
291
+ [key: string]: {
292
+ start: {
293
+ line: number;
294
+ column: number;
295
+ };
296
+ end: {
297
+ line: number;
298
+ column: number;
299
+ };
300
+ };
301
+ };
302
+ fnMap: {
303
+ [key: string]: {
304
+ name: string;
305
+ decl: {
306
+ start: {
307
+ line: number;
308
+ column: number;
309
+ };
310
+ end: {
311
+ line: number;
312
+ column: number;
313
+ };
314
+ };
315
+ loc: {
316
+ start: {
317
+ line: number;
318
+ column: number;
319
+ };
320
+ end: {
321
+ line: number;
322
+ column: number;
323
+ };
324
+ };
325
+ };
326
+ };
327
+ branchMap: {
328
+ [key: string]: {
329
+ loc: {
330
+ start: {
331
+ line: number;
332
+ column: number;
333
+ };
334
+ end: {
335
+ line: number;
336
+ column: number;
337
+ };
338
+ };
339
+ type: string;
340
+ locations: Array<{
341
+ start: {
342
+ line: number;
343
+ column: number;
344
+ };
345
+ end: {
346
+ line: number;
347
+ column: number;
348
+ };
349
+ }>;
350
+ };
351
+ };
352
+ s: {
353
+ [key: string]: number;
354
+ };
355
+ f: {
356
+ [key: string]: number;
357
+ };
358
+ b: {
359
+ [key: string]: number[];
360
+ };
361
+ };
362
+ }
363
+ /**
364
+ * Coverage information for a single file
365
+ */
366
+ interface FileCoverageInfo {
367
+ filePath: string;
368
+ category: FileCategory;
369
+ weight: number;
370
+ statements: {
371
+ covered: number;
372
+ total: number;
373
+ };
374
+ percentage: number;
375
+ }
376
+ /**
377
+ * Aggregated coverage result for a component
378
+ */
379
+ interface AggregatedCoverage {
380
+ /** The component file that was analyzed */
381
+ componentFile: string;
382
+ /** Coverage percentage for just the component file */
383
+ componentCoverage: number;
384
+ /** Weighted aggregate coverage across component + all dependencies */
385
+ aggregateCoverage: number;
386
+ /** Total number of files analyzed (component + dependencies) */
387
+ totalFiles: number;
388
+ /** Detailed coverage info for each file */
389
+ filesAnalyzed: FileCoverageInfo[];
390
+ /** Files with 0% coverage */
391
+ uncoveredFiles: string[];
392
+ /** The file with lowest coverage (excluding 0% files) */
393
+ lowestCoverageFile: {
394
+ path: string;
395
+ percentage: number;
396
+ } | null;
397
+ }
398
+ /**
399
+ * Calculate aggregate coverage for a component and its dependencies
400
+ *
401
+ * @param componentFile - Absolute path to the component file
402
+ * @param projectRoot - Project root directory
403
+ * @param coverageData - Istanbul coverage data
404
+ * @returns Aggregated coverage information
405
+ */
406
+ declare function aggregateCoverage(componentFile: string, projectRoot: string, coverageData: IstanbulCoverage): AggregatedCoverage;
407
+
408
+ /**
409
+ * Dependency Graph Builder
410
+ *
411
+ * Builds a dependency graph by tracing all imports from an entry file.
412
+ * Used for calculating aggregate test coverage across a component and its dependencies.
413
+ *
414
+ * Key behaviors:
415
+ * - Traces transitive dependencies (full depth)
416
+ * - Excludes node_modules (external packages)
417
+ * - Handles circular dependencies via visited set
418
+ * - Follows re-exports to actual source files
419
+ * - Caches results for performance
420
+ */
421
+ interface DependencyGraph {
422
+ /** The entry file that was analyzed */
423
+ root: string;
424
+ /** All transitive dependencies (absolute paths, project files only) */
425
+ allDependencies: Set<string>;
426
+ }
427
+ /**
428
+ * Build a dependency graph starting from an entry file
429
+ *
430
+ * @param entryFile - Absolute path to the entry file
431
+ * @param projectRoot - Project root directory (used for determining project boundaries)
432
+ * @returns DependencyGraph with all transitive dependencies
433
+ */
434
+ declare function buildDependencyGraph(entryFile: string, projectRoot: string): DependencyGraph;
435
+
436
+ /**
437
+ * JSX Coverage Analyzer
438
+ *
439
+ * Analyzes JSX elements to determine test coverage for interactive elements
440
+ * like event handlers. Uses Istanbul coverage data to check if the code
441
+ * associated with JSX elements has been executed during tests.
442
+ *
443
+ * Phase 1: Core functions for statement-level coverage analysis
444
+ * Phase 2: Event handler extraction and analysis
445
+ * Phase 3: Conditional parent analysis + Component-level aggregation (partial TODO)
446
+ * Phase 4: Import dependency coverage + ESLint rule reporting (partial TODO)
447
+ */
448
+
449
+ /**
450
+ * Istanbul coverage data for a single file
451
+ */
452
+ type IstanbulFileCoverage = IstanbulCoverage[string];
453
+ /**
454
+ * Source location with start and end positions
455
+ */
456
+ interface SourceLocation {
457
+ start: {
458
+ line: number;
459
+ column: number;
460
+ };
461
+ end: {
462
+ line: number;
463
+ column: number;
464
+ };
465
+ }
466
+ /**
467
+ * Coverage statistics for a code region
468
+ */
469
+ interface CoverageStats {
470
+ /** Number of statements that were executed at least once */
471
+ covered: number;
472
+ /** Total number of statements in the region */
473
+ total: number;
474
+ /** Coverage percentage (0-100) */
475
+ percentage: number;
476
+ }
477
+ /**
478
+ * Coverage result for a single JSX element
479
+ */
480
+ interface JSXCoverageResult {
481
+ /** The data-loc attribute value for this element */
482
+ dataLoc: string;
483
+ /** Whether this element has any event handlers */
484
+ hasEventHandlers: boolean;
485
+ /** Names of event handlers found (e.g., ["onClick", "onSubmit"]) */
486
+ eventHandlerNames: string[];
487
+ /** Coverage statistics for statements within this element */
488
+ coverage: CoverageStats;
489
+ /** Whether the element is considered "covered" (percentage > 0) */
490
+ isCovered: boolean;
491
+ }
492
+ /**
493
+ * Creates a "file:line:column" format string for data-loc attribute
494
+ *
495
+ * @param filePath - Absolute or relative path to the file
496
+ * @param loc - Source location with start position
497
+ * @returns Formatted string like "src/Button.tsx:15:4"
498
+ */
499
+ declare function buildDataLoc(filePath: string, loc: SourceLocation): string;
500
+ /**
501
+ * Find statement IDs that overlap with the given source location
502
+ *
503
+ * A statement overlaps if its line range intersects with the location's
504
+ * line range. Column-level precision is not used for overlap detection.
505
+ *
506
+ * @param loc - The source location to check
507
+ * @param fileCoverage - Istanbul coverage data for the file
508
+ * @returns Set of statement IDs (keys from statementMap) that overlap
509
+ */
510
+ declare function findStatementsInRange(loc: SourceLocation, fileCoverage: IstanbulFileCoverage): Set<string>;
511
+ /**
512
+ * Calculate coverage statistics from a set of statement IDs
513
+ *
514
+ * @param statementIds - Set of statement IDs to check
515
+ * @param fileCoverage - Istanbul coverage data for the file
516
+ * @returns Coverage statistics with covered count, total, and percentage
517
+ */
518
+ declare function calculateCoverageFromStatements(statementIds: Set<string>, fileCoverage: IstanbulFileCoverage): CoverageStats;
519
+ /**
520
+ * Find coverage data for a file with path normalization
521
+ *
522
+ * Handles various path formats:
523
+ * - Absolute paths
524
+ * - Relative paths with or without leading slash
525
+ * - Paths that may differ in their base directory
526
+ *
527
+ * @param coverage - Full Istanbul coverage data
528
+ * @param filePath - The file path to find coverage for
529
+ * @returns File coverage data if found, undefined otherwise
530
+ */
531
+ declare function findCoverageForFile(coverage: IstanbulCoverage, filePath: string): IstanbulFileCoverage | undefined;
532
+ /**
533
+ * Check if a JSX attribute is an event handler (starts with "on" followed by uppercase)
534
+ *
535
+ * Event handlers follow the pattern: onClick, onSubmit, onChange, etc.
536
+ * This excludes spread attributes and non-event props like "only" or "once".
537
+ *
538
+ * @param attr - JSX attribute or spread attribute
539
+ * @returns true if the attribute is an event handler
540
+ */
541
+ declare function isEventHandlerAttribute(attr: TSESTree.JSXAttribute | TSESTree.JSXSpreadAttribute): boolean;
542
+ /**
543
+ * Analyze a JSX element for test coverage
544
+ *
545
+ * Main entry point that combines all the above functions to produce
546
+ * a complete coverage analysis for a single JSX element.
547
+ *
548
+ * @param jsxNode - The JSX element node from the AST
549
+ * @param filePath - Path to the file containing this element
550
+ * @param coverage - Istanbul coverage data
551
+ * @param ancestors - Optional ancestor nodes for resolving handler references
552
+ * @param projectRoot - Optional project root for resolving import paths
553
+ * @returns Coverage result for the JSX element
554
+ */
555
+ declare function analyzeJSXElementCoverage(jsxNode: TSESTree.JSXElement, filePath: string, coverage: IstanbulCoverage, ancestors?: TSESTree.Node[], projectRoot?: string): JSXCoverageResult;
556
+
239
557
  /**
240
558
  * All available rules
241
559
  */
@@ -329,6 +647,36 @@ declare const rules: {
329
647
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
330
648
  name: string;
331
649
  };
650
+ "no-semantic-duplicates": _typescript_eslint_utils_ts_eslint.RuleModule<"semanticDuplicate" | "noIndex", [{
651
+ threshold?: number;
652
+ indexPath?: string;
653
+ minLines?: number;
654
+ }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
655
+ name: string;
656
+ };
657
+ "require-test-coverage": _typescript_eslint_utils_ts_eslint.RuleModule<"noTestFile" | "noCoverage" | "belowThreshold" | "noCoverageData" | "belowAggregateThreshold" | "jsxBelowThreshold", [{
658
+ coveragePath?: string;
659
+ threshold?: number;
660
+ thresholdsByPattern?: Array<{
661
+ pattern: string;
662
+ threshold: number;
663
+ }>;
664
+ severity?: {
665
+ noTestFile?: "error" | "warn" | "off";
666
+ noCoverage?: "error" | "warn" | "off";
667
+ belowThreshold?: "error" | "warn" | "off";
668
+ };
669
+ testPatterns?: string[];
670
+ ignorePatterns?: string[];
671
+ mode?: "all" | "changed";
672
+ baseBranch?: string;
673
+ aggregateThreshold?: number;
674
+ aggregateSeverity?: "error" | "warn" | "off";
675
+ jsxThreshold?: number;
676
+ jsxSeverity?: "error" | "warn" | "off";
677
+ }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
678
+ name: string;
679
+ };
332
680
  };
333
681
  /**
334
682
  * Plugin metadata
@@ -435,6 +783,36 @@ declare const plugin: {
435
783
  }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
436
784
  name: string;
437
785
  };
786
+ "no-semantic-duplicates": _typescript_eslint_utils_ts_eslint.RuleModule<"semanticDuplicate" | "noIndex", [{
787
+ threshold?: number;
788
+ indexPath?: string;
789
+ minLines?: number;
790
+ }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
791
+ name: string;
792
+ };
793
+ "require-test-coverage": _typescript_eslint_utils_ts_eslint.RuleModule<"noTestFile" | "noCoverage" | "belowThreshold" | "noCoverageData" | "belowAggregateThreshold" | "jsxBelowThreshold", [{
794
+ coveragePath?: string;
795
+ threshold?: number;
796
+ thresholdsByPattern?: Array<{
797
+ pattern: string;
798
+ threshold: number;
799
+ }>;
800
+ severity?: {
801
+ noTestFile?: "error" | "warn" | "off";
802
+ noCoverage?: "error" | "warn" | "off";
803
+ belowThreshold?: "error" | "warn" | "off";
804
+ };
805
+ testPatterns?: string[];
806
+ ignorePatterns?: string[];
807
+ mode?: "all" | "changed";
808
+ baseBranch?: string;
809
+ aggregateThreshold?: number;
810
+ aggregateSeverity?: "error" | "warn" | "off";
811
+ jsxThreshold?: number;
812
+ jsxSeverity?: "error" | "warn" | "off";
813
+ }], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
814
+ name: string;
815
+ };
438
816
  };
439
817
  };
440
818
  /**
@@ -455,4 +833,4 @@ interface UILintESLint {
455
833
  */
456
834
  declare const uilintEslint: UILintESLint;
457
835
 
458
- export { type CacheEntry, type CacheStore, type CachedIssue, type LibraryName, type OptionFieldSchema, type RuleMeta, type RuleMeta as RuleMetadata, type RuleOptionSchema, type UILintESLint, clearCache$1 as clearCache, clearCacheEntry, clearCache as clearImportGraphCache, configs, createRule, uilintEslint as default, defineRuleMeta, findStyleguidePath, getAllRuleIds, getCacheEntry, getComponentLibrary, getRuleDocs, getRuleMetadata, getRulesByCategory, getStyleguide, hashContent, hashContentSync, loadCache, loadStyleguide, meta, plugin, ruleRegistry, rules, saveCache, setCacheEntry };
836
+ export { type AggregatedCoverage, type CacheEntry, type CacheStore, type CachedIssue, type CoverageStats, type DependencyGraph, type FileCategory, type FileCategoryResult, type FileCoverageInfo, type IstanbulCoverage, type IstanbulFileCoverage, type JSXCoverageResult, type LibraryName, type OptionFieldSchema, type RuleMeta, type RuleMeta as RuleMetadata, type RuleOptionSchema, type SourceLocation, type UILintESLint, aggregateCoverage, analyzeJSXElementCoverage, buildDataLoc, buildDependencyGraph, calculateCoverageFromStatements, categorizeFile, clearCache$1 as clearCache, clearCacheEntry, clearCache as clearImportGraphCache, configs, createRule, uilintEslint as default, defineRuleMeta, findCoverageForFile, findStatementsInRange, findStyleguidePath, getAllRuleIds, getCacheEntry, getComponentLibrary, getRuleDocs, getRuleMetadata, getRulesByCategory, getStyleguide, hashContent, hashContentSync, isEventHandlerAttribute, loadCache, loadStyleguide, meta, plugin, ruleRegistry, rules, saveCache, setCacheEntry };