react-anti-pattern-sniffer 0.1.0

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 (97) hide show
  1. package/.snifferrc.json +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/bin/react-sniff.js +3 -0
  5. package/dist/src/cli/arg-parser.d.ts +10 -0
  6. package/dist/src/cli/arg-parser.d.ts.map +1 -0
  7. package/dist/src/cli/arg-parser.js +81 -0
  8. package/dist/src/cli/arg-parser.js.map +1 -0
  9. package/dist/src/cli/config-loader.d.ts +11 -0
  10. package/dist/src/cli/config-loader.d.ts.map +1 -0
  11. package/dist/src/cli/config-loader.js +140 -0
  12. package/dist/src/cli/config-loader.js.map +1 -0
  13. package/dist/src/cli/help.d.ts +3 -0
  14. package/dist/src/cli/help.d.ts.map +1 -0
  15. package/dist/src/cli/help.js +59 -0
  16. package/dist/src/cli/help.js.map +1 -0
  17. package/dist/src/cli/index.d.ts +2 -0
  18. package/dist/src/cli/index.d.ts.map +1 -0
  19. package/dist/src/cli/index.js +107 -0
  20. package/dist/src/cli/index.js.map +1 -0
  21. package/dist/src/core/file-discoverer.d.ts +8 -0
  22. package/dist/src/core/file-discoverer.d.ts.map +1 -0
  23. package/dist/src/core/file-discoverer.js +151 -0
  24. package/dist/src/core/file-discoverer.js.map +1 -0
  25. package/dist/src/core/orchestrator.d.ts +13 -0
  26. package/dist/src/core/orchestrator.d.ts.map +1 -0
  27. package/dist/src/core/orchestrator.js +176 -0
  28. package/dist/src/core/orchestrator.js.map +1 -0
  29. package/dist/src/core/sniffer-ignore.d.ts +25 -0
  30. package/dist/src/core/sniffer-ignore.d.ts.map +1 -0
  31. package/dist/src/core/sniffer-ignore.js +91 -0
  32. package/dist/src/core/sniffer-ignore.js.map +1 -0
  33. package/dist/src/core/sniffer-registry.d.ts +8 -0
  34. package/dist/src/core/sniffer-registry.d.ts.map +1 -0
  35. package/dist/src/core/sniffer-registry.js +64 -0
  36. package/dist/src/core/sniffer-registry.js.map +1 -0
  37. package/dist/src/core/worker-pool.d.ts +27 -0
  38. package/dist/src/core/worker-pool.d.ts.map +1 -0
  39. package/dist/src/core/worker-pool.js +176 -0
  40. package/dist/src/core/worker-pool.js.map +1 -0
  41. package/dist/src/core/worker-runner.d.ts +2 -0
  42. package/dist/src/core/worker-runner.d.ts.map +1 -0
  43. package/dist/src/core/worker-runner.js +52 -0
  44. package/dist/src/core/worker-runner.js.map +1 -0
  45. package/dist/src/output/formatter.d.ts +3 -0
  46. package/dist/src/output/formatter.d.ts.map +1 -0
  47. package/dist/src/output/formatter.js +13 -0
  48. package/dist/src/output/formatter.js.map +1 -0
  49. package/dist/src/output/json-renderer.d.ts +3 -0
  50. package/dist/src/output/json-renderer.d.ts.map +1 -0
  51. package/dist/src/output/json-renderer.js +49 -0
  52. package/dist/src/output/json-renderer.js.map +1 -0
  53. package/dist/src/output/markdown-renderer.d.ts +3 -0
  54. package/dist/src/output/markdown-renderer.d.ts.map +1 -0
  55. package/dist/src/output/markdown-renderer.js +70 -0
  56. package/dist/src/output/markdown-renderer.js.map +1 -0
  57. package/dist/src/plugins/plugin-loader.d.ts +7 -0
  58. package/dist/src/plugins/plugin-loader.d.ts.map +1 -0
  59. package/dist/src/plugins/plugin-loader.js +47 -0
  60. package/dist/src/plugins/plugin-loader.js.map +1 -0
  61. package/dist/src/plugins/plugin-sandbox.d.ts +3 -0
  62. package/dist/src/plugins/plugin-sandbox.d.ts.map +1 -0
  63. package/dist/src/plugins/plugin-sandbox.js +105 -0
  64. package/dist/src/plugins/plugin-sandbox.js.map +1 -0
  65. package/dist/src/plugins/plugin-validator.d.ts +14 -0
  66. package/dist/src/plugins/plugin-validator.d.ts.map +1 -0
  67. package/dist/src/plugins/plugin-validator.js +92 -0
  68. package/dist/src/plugins/plugin-validator.js.map +1 -0
  69. package/dist/src/sniffers/god-hook-sniffer.d.ts +12 -0
  70. package/dist/src/sniffers/god-hook-sniffer.d.ts.map +1 -0
  71. package/dist/src/sniffers/god-hook-sniffer.js +109 -0
  72. package/dist/src/sniffers/god-hook-sniffer.js.map +1 -0
  73. package/dist/src/sniffers/prop-drilling-sniffer.d.ts +5 -0
  74. package/dist/src/sniffers/prop-drilling-sniffer.d.ts.map +1 -0
  75. package/dist/src/sniffers/prop-drilling-sniffer.js +145 -0
  76. package/dist/src/sniffers/prop-drilling-sniffer.js.map +1 -0
  77. package/dist/src/sniffers/prop-explosion-sniffer.d.ts +4 -0
  78. package/dist/src/sniffers/prop-explosion-sniffer.d.ts.map +1 -0
  79. package/dist/src/sniffers/prop-explosion-sniffer.js +134 -0
  80. package/dist/src/sniffers/prop-explosion-sniffer.js.map +1 -0
  81. package/dist/src/sniffers/sniffer-interface.d.ts +88 -0
  82. package/dist/src/sniffers/sniffer-interface.d.ts.map +1 -0
  83. package/dist/src/sniffers/sniffer-interface.js +18 -0
  84. package/dist/src/sniffers/sniffer-interface.js.map +1 -0
  85. package/dist/src/tui/interactive-viewer.d.ts +7 -0
  86. package/dist/src/tui/interactive-viewer.d.ts.map +1 -0
  87. package/dist/src/tui/interactive-viewer.js +453 -0
  88. package/dist/src/tui/interactive-viewer.js.map +1 -0
  89. package/dist/src/utils/logger.d.ts +11 -0
  90. package/dist/src/utils/logger.d.ts.map +1 -0
  91. package/dist/src/utils/logger.js +90 -0
  92. package/dist/src/utils/logger.js.map +1 -0
  93. package/dist/src/utils/regex-helpers.d.ts +53 -0
  94. package/dist/src/utils/regex-helpers.d.ts.map +1 -0
  95. package/dist/src/utils/regex-helpers.js +275 -0
  96. package/dist/src/utils/regex-helpers.js.map +1 -0
  97. package/package.json +40 -0
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapSnifferForSandbox = wrapSnifferForSandbox;
4
+ const DETECTION_FIELDS = [
5
+ 'snifferName',
6
+ 'filePath',
7
+ 'line',
8
+ 'column',
9
+ 'message',
10
+ 'severity',
11
+ 'suggestion',
12
+ 'details',
13
+ ];
14
+ const REQUIRED_DETECTION_FIELDS = [
15
+ 'snifferName',
16
+ 'filePath',
17
+ 'line',
18
+ 'message',
19
+ 'severity',
20
+ 'suggestion',
21
+ ];
22
+ function stripDetection(raw) {
23
+ const stripped = {};
24
+ for (const field of DETECTION_FIELDS) {
25
+ if (field in raw) {
26
+ stripped[field] = raw[field];
27
+ }
28
+ }
29
+ return stripped;
30
+ }
31
+ function validateDetection(detection, index) {
32
+ if (detection === null || detection === undefined || typeof detection !== 'object') {
33
+ return `Detection at index ${index} must be a non-null object`;
34
+ }
35
+ const det = detection;
36
+ for (const field of REQUIRED_DETECTION_FIELDS) {
37
+ if (!(field in det)) {
38
+ return `Detection at index ${index} is missing required field "${field}"`;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ function wrapSnifferForSandbox(snifferModule) {
44
+ return {
45
+ name: snifferModule.name,
46
+ description: snifferModule.description,
47
+ meta: snifferModule.meta,
48
+ detect(fileContent, filePath, config) {
49
+ try {
50
+ // Freeze the config object before passing to detect
51
+ const frozenConfig = Object.freeze({ ...config });
52
+ const result = snifferModule.detect(fileContent, filePath, frozenConfig);
53
+ // Validate the return value is an array
54
+ if (!Array.isArray(result)) {
55
+ return [
56
+ {
57
+ snifferName: snifferModule.name,
58
+ filePath,
59
+ line: 0,
60
+ column: 0,
61
+ message: `Plugin "${snifferModule.name}" detect() did not return an array`,
62
+ severity: 'error',
63
+ suggestion: 'Fix the plugin to return an array of Detection objects',
64
+ },
65
+ ];
66
+ }
67
+ // Validate each Detection and strip unexpected properties
68
+ const sanitized = [];
69
+ for (let i = 0; i < result.length; i++) {
70
+ const validationError = validateDetection(result[i], i);
71
+ if (validationError !== null) {
72
+ sanitized.push({
73
+ snifferName: snifferModule.name,
74
+ filePath,
75
+ line: 0,
76
+ column: 0,
77
+ message: `Plugin "${snifferModule.name}": ${validationError}`,
78
+ severity: 'error',
79
+ suggestion: 'Fix the plugin to return valid Detection objects',
80
+ });
81
+ }
82
+ else {
83
+ sanitized.push(stripDetection(result[i]));
84
+ }
85
+ }
86
+ return sanitized;
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ return [
91
+ {
92
+ snifferName: snifferModule.name,
93
+ filePath,
94
+ line: 0,
95
+ column: 0,
96
+ message: `Plugin "${snifferModule.name}" threw an error: ${message}`,
97
+ severity: 'error',
98
+ suggestion: 'Check the plugin for runtime errors',
99
+ },
100
+ ];
101
+ }
102
+ },
103
+ };
104
+ }
105
+ //# sourceMappingURL=plugin-sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-sandbox.js","sourceRoot":"","sources":["../../../src/plugins/plugin-sandbox.ts"],"names":[],"mappings":";;AA+CA,sDA+DC;AA5GD,MAAM,gBAAgB,GAAmC;IACvD,aAAa;IACb,UAAU;IACV,MAAM;IACN,QAAQ;IACR,SAAS;IACT,UAAU;IACV,YAAY;IACZ,SAAS;CACV,CAAC;AAEF,MAAM,yBAAyB,GAAmC;IAChE,aAAa;IACb,UAAU;IACV,MAAM;IACN,SAAS;IACT,UAAU;IACV,YAAY;CACb,CAAC;AAEF,SAAS,cAAc,CAAC,GAA4B;IAClD,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,KAAK,MAAM,KAAK,IAAI,gBAAgB,EAAE,CAAC;QACrC,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;YACjB,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,OAAO,QAAgC,CAAC;AAC1C,CAAC;AAED,SAAS,iBAAiB,CAAC,SAAkB,EAAE,KAAa;IAC1D,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QACnF,OAAO,sBAAsB,KAAK,4BAA4B,CAAC;IACjE,CAAC;IAED,MAAM,GAAG,GAAG,SAAoC,CAAC;IACjD,KAAK,MAAM,KAAK,IAAI,yBAAyB,EAAE,CAAC;QAC9C,IAAI,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,EAAE,CAAC;YACpB,OAAO,sBAAsB,KAAK,+BAA+B,KAAK,GAAG,CAAC;QAC5E,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAgB,qBAAqB,CAAC,aAA4B;IAChE,OAAO;QACL,IAAI,EAAE,aAAa,CAAC,IAAI;QACxB,WAAW,EAAE,aAAa,CAAC,WAAW;QACtC,IAAI,EAAE,aAAa,CAAC,IAAI;QACxB,MAAM,CAAC,WAAmB,EAAE,QAAgB,EAAE,MAA+B;YAC3E,IAAI,CAAC;gBACH,oDAAoD;gBACpD,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;gBAElD,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;gBAEzE,wCAAwC;gBACxC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC3B,OAAO;wBACL;4BACE,WAAW,EAAE,aAAa,CAAC,IAAI;4BAC/B,QAAQ;4BACR,IAAI,EAAE,CAAC;4BACP,MAAM,EAAE,CAAC;4BACT,OAAO,EAAE,WAAW,aAAa,CAAC,IAAI,oCAAoC;4BAC1E,QAAQ,EAAE,OAAO;4BACjB,UAAU,EAAE,wDAAwD;yBACrE;qBACF,CAAC;gBACJ,CAAC;gBAED,0DAA0D;gBAC1D,MAAM,SAAS,GAAgB,EAAE,CAAC;gBAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACvC,MAAM,eAAe,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;oBACxD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;wBAC7B,SAAS,CAAC,IAAI,CAAC;4BACb,WAAW,EAAE,aAAa,CAAC,IAAI;4BAC/B,QAAQ;4BACR,IAAI,EAAE,CAAC;4BACP,MAAM,EAAE,CAAC;4BACT,OAAO,EAAE,WAAW,aAAa,CAAC,IAAI,MAAM,eAAe,EAAE;4BAC7D,QAAQ,EAAE,OAAO;4BACjB,UAAU,EAAE,kDAAkD;yBAC/D,CAAC,CAAC;oBACL,CAAC;yBAAM,CAAC;wBACN,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAuC,CAAC,CAAC,CAAC;oBAClF,CAAC;gBACH,CAAC;gBAED,OAAO,SAAS,CAAC;YACnB,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,OAAO;oBACL;wBACE,WAAW,EAAE,aAAa,CAAC,IAAI;wBAC/B,QAAQ;wBACR,IAAI,EAAE,CAAC;wBACP,MAAM,EAAE,CAAC;wBACT,OAAO,EAAE,WAAW,aAAa,CAAC,IAAI,qBAAqB,OAAO,EAAE;wBACpE,QAAQ,EAAE,OAAO;wBACjB,UAAU,EAAE,qCAAqC;qBAClD;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { SnifferExport } from '../sniffers/sniffer-interface.js';
2
+ export declare function validatePluginExports(pluginModule: unknown): {
3
+ valid: boolean;
4
+ errors: string[];
5
+ };
6
+ export declare function validatePluginSecurity(pluginPath: string): {
7
+ safe: boolean;
8
+ warnings: string[];
9
+ };
10
+ export declare function runPluginSmokeTest(pluginModule: SnifferExport): {
11
+ passed: boolean;
12
+ error: string | null;
13
+ };
14
+ //# sourceMappingURL=plugin-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-validator.d.ts","sourceRoot":"","sources":["../../../src/plugins/plugin-validator.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAGtE,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAyCjG;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAkChG;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAWzG"}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validatePluginExports = validatePluginExports;
4
+ exports.validatePluginSecurity = validatePluginSecurity;
5
+ exports.runPluginSmokeTest = runPluginSmokeTest;
6
+ const node_fs_1 = require("node:fs");
7
+ const sniffer_interface_js_1 = require("../sniffers/sniffer-interface.js");
8
+ function validatePluginExports(pluginModule) {
9
+ const errors = [];
10
+ if (pluginModule === null || pluginModule === undefined || typeof pluginModule !== 'object') {
11
+ errors.push('Plugin module must be a non-null object');
12
+ return { valid: false, errors };
13
+ }
14
+ const mod = pluginModule;
15
+ // Check each key in REQUIRED_EXPORT_SCHEMA exists and has the correct type
16
+ for (const [key, expectedType] of Object.entries(sniffer_interface_js_1.REQUIRED_EXPORT_SCHEMA)) {
17
+ if (!(key in mod)) {
18
+ errors.push(`Missing required export: "${key}"`);
19
+ }
20
+ else if (typeof mod[key] !== expectedType) {
21
+ errors.push(`Export "${key}" must be of type "${expectedType}", got "${typeof mod[key]}"`);
22
+ }
23
+ else if (key === 'name' && mod[key].trim() === '') {
24
+ errors.push(`Export "name" must not be an empty string`);
25
+ }
26
+ }
27
+ // Check meta sub-fields match REQUIRED_META_SCHEMA
28
+ if ('meta' in mod && typeof mod.meta === 'object' && mod.meta !== null) {
29
+ const meta = mod.meta;
30
+ for (const [key, expectedType] of Object.entries(sniffer_interface_js_1.REQUIRED_META_SCHEMA)) {
31
+ if (!(key in meta)) {
32
+ errors.push(`Missing required meta field: "${key}"`);
33
+ }
34
+ else if (typeof meta[key] !== expectedType) {
35
+ errors.push(`Meta field "${key}" must be of type "${expectedType}", got "${typeof meta[key]}"`);
36
+ }
37
+ }
38
+ }
39
+ // Check detect function has .length >= 2 (at least fileContent, filePath params)
40
+ if ('detect' in mod && typeof mod.detect === 'function') {
41
+ if (mod.detect.length < 2) {
42
+ errors.push(`detect function must accept at least 2 parameters (fileContent, filePath), got ${mod.detect.length}`);
43
+ }
44
+ }
45
+ return { valid: errors.length === 0, errors };
46
+ }
47
+ function validatePluginSecurity(pluginPath) {
48
+ const warnings = [];
49
+ let source;
50
+ try {
51
+ source = (0, node_fs_1.readFileSync)(pluginPath, 'utf-8');
52
+ }
53
+ catch {
54
+ warnings.push(`Could not read plugin file: ${pluginPath}`);
55
+ return { safe: false, warnings };
56
+ }
57
+ const dangerousPatterns = [
58
+ { pattern: /\beval\s*\(/, reason: 'dangerous code execution (eval)' },
59
+ { pattern: /\bnew\s+Function\s*\(/, reason: 'dynamic code creation (new Function)' },
60
+ { pattern: /\bFunction\s*\(/, reason: 'dynamic code creation (Function)' },
61
+ { pattern: /require\s*\(\s*['"`]child_process['"`]\s*\)/, reason: 'process spawning (child_process)' },
62
+ { pattern: /require\s*\(\s*['"`]net['"`]\s*\)/, reason: 'network access (net)' },
63
+ { pattern: /require\s*\(\s*['"`]http['"`]\s*\)/, reason: 'network access (http)' },
64
+ { pattern: /require\s*\(\s*['"`]https['"`]\s*\)/, reason: 'network access (https)' },
65
+ { pattern: /require\s*\(\s*['"`]dgram['"`]\s*\)/, reason: 'network access (dgram)' },
66
+ { pattern: /require\s*\(\s*['"`]cluster['"`]\s*\)/, reason: 'cluster access (cluster)' },
67
+ { pattern: /\bprocess\.exit\b/, reason: 'can kill the host process (process.exit)' },
68
+ { pattern: /\bprocess\.kill\b/, reason: 'can kill processes (process.kill)' },
69
+ { pattern: /\bglobal\.\w+\s*=/, reason: 'global pollution (global.* assignment)' },
70
+ { pattern: /\bglobalThis\.\w+\s*=/, reason: 'global pollution (globalThis.* assignment)' },
71
+ ];
72
+ for (const { pattern, reason } of dangerousPatterns) {
73
+ if (pattern.test(source)) {
74
+ warnings.push(`Potentially dangerous pattern found: ${reason}`);
75
+ }
76
+ }
77
+ return { safe: warnings.length === 0, warnings };
78
+ }
79
+ function runPluginSmokeTest(pluginModule) {
80
+ try {
81
+ const result = pluginModule.detect('', 'test.jsx', {});
82
+ if (!Array.isArray(result)) {
83
+ return { passed: false, error: `detect() must return an array, got ${typeof result}` };
84
+ }
85
+ return { passed: true, error: null };
86
+ }
87
+ catch (err) {
88
+ const message = err instanceof Error ? err.message : String(err);
89
+ return { passed: false, error: `detect() threw an exception: ${message}` };
90
+ }
91
+ }
92
+ //# sourceMappingURL=plugin-validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-validator.js","sourceRoot":"","sources":["../../../src/plugins/plugin-validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * God Hook Sniffer
3
+ *
4
+ * Detects custom hooks that do too much — too many useState, useEffect,
5
+ * or total hook calls — suggesting they should be split into smaller,
6
+ * focused hooks with single responsibilities.
7
+ */
8
+ import type { SnifferExport } from './sniffer-interface.js';
9
+ declare const sniffer: SnifferExport;
10
+ export default sniffer;
11
+ export { sniffer as godHookSniffer };
12
+ //# sourceMappingURL=god-hook-sniffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"god-hook-sniffer.d.ts","sourceRoot":"","sources":["../../../src/sniffers/god-hook-sniffer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAa,aAAa,EAAY,MAAM,wBAAwB,CAAC;AAiIjF,QAAA,MAAM,OAAO,EAAE,aAad,CAAC;AAEF,eAAe,OAAO,CAAC;AACvB,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,CAAC"}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * God Hook Sniffer
4
+ *
5
+ * Detects custom hooks that do too much — too many useState, useEffect,
6
+ * or total hook calls — suggesting they should be split into smaller,
7
+ * focused hooks with single responsibilities.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.godHookSniffer = void 0;
11
+ const regex_helpers_js_1 = require("../utils/regex-helpers.js");
12
+ const DEFAULT_CONFIG = {
13
+ maxUseState: 4,
14
+ maxUseEffect: 3,
15
+ maxTotalHooks: 10,
16
+ severity: 'warning',
17
+ };
18
+ function resolveConfig(config) {
19
+ return {
20
+ maxUseState: typeof config.maxUseState === 'number' ? config.maxUseState : DEFAULT_CONFIG.maxUseState,
21
+ maxUseEffect: typeof config.maxUseEffect === 'number' ? config.maxUseEffect : DEFAULT_CONFIG.maxUseEffect,
22
+ maxTotalHooks: typeof config.maxTotalHooks === 'number' ? config.maxTotalHooks : DEFAULT_CONFIG.maxTotalHooks,
23
+ severity: typeof config.severity === 'string' ? config.severity : DEFAULT_CONFIG.severity,
24
+ };
25
+ }
26
+ function detect(fileContent, filePath, config) {
27
+ const detections = [];
28
+ const cfg = resolveConfig(config);
29
+ // Reset the global regex before use
30
+ const hookDeclRegex = new RegExp(regex_helpers_js_1.CUSTOM_HOOK_DECL.source, regex_helpers_js_1.CUSTOM_HOOK_DECL.flags);
31
+ let match;
32
+ while ((match = hookDeclRegex.exec(fileContent)) !== null) {
33
+ const hookName = match[1] || match[2];
34
+ const matchIndex = match.index;
35
+ // Find the opening brace of the hook body
36
+ const braceIndex = (0, regex_helpers_js_1.findOpeningBrace)(fileContent, matchIndex + match[0].length);
37
+ if (braceIndex === -1)
38
+ continue;
39
+ // Extract the full hook body
40
+ const body = (0, regex_helpers_js_1.extractBracedBlock)(fileContent, braceIndex);
41
+ if (!body)
42
+ continue;
43
+ // Strip comments and strings to avoid false positives
44
+ const strippedBody = (0, regex_helpers_js_1.stripCommentsAndStrings)(body);
45
+ // Count individual hook calls
46
+ const useStateCount = (0, regex_helpers_js_1.countMatches)(strippedBody, regex_helpers_js_1.USE_STATE);
47
+ const useEffectCount = (0, regex_helpers_js_1.countMatches)(strippedBody, regex_helpers_js_1.USE_EFFECT);
48
+ const useCallbackCount = (0, regex_helpers_js_1.countMatches)(strippedBody, regex_helpers_js_1.USE_CALLBACK);
49
+ const useMemoCount = (0, regex_helpers_js_1.countMatches)(strippedBody, regex_helpers_js_1.USE_MEMO);
50
+ const useRefCount = (0, regex_helpers_js_1.countMatches)(strippedBody, regex_helpers_js_1.USE_REF);
51
+ const useLayoutEffectCount = (0, regex_helpers_js_1.countMatches)(strippedBody, regex_helpers_js_1.USE_LAYOUT_EFFECT);
52
+ const totalHooks = useStateCount +
53
+ useEffectCount +
54
+ useCallbackCount +
55
+ useMemoCount +
56
+ useRefCount +
57
+ useLayoutEffectCount;
58
+ // Check thresholds
59
+ const exceedsUseState = useStateCount > cfg.maxUseState;
60
+ const exceedsUseEffect = useEffectCount > cfg.maxUseEffect;
61
+ const exceedsTotalHooks = totalHooks > cfg.maxTotalHooks;
62
+ if (exceedsUseState || exceedsUseEffect || exceedsTotalHooks) {
63
+ const line = (0, regex_helpers_js_1.getLineNumber)(fileContent, matchIndex);
64
+ detections.push({
65
+ snifferName: 'god-hook',
66
+ filePath,
67
+ line,
68
+ column: 1,
69
+ message: `Hook "${hookName}" has ${useStateCount} useState, ${useEffectCount} useEffect, ${totalHooks} total hook calls`,
70
+ severity: cfg.severity,
71
+ suggestion: `**Consider splitting \`${hookName}\`:**\n` +
72
+ `- Extract related state + effects into focused sub-hooks\n` +
73
+ `- Each hook should have a single responsibility\n` +
74
+ `- Use \`useReducer\` for complex related state\n` +
75
+ `- Extract pure data transformations outside hooks`,
76
+ details: {
77
+ hookName,
78
+ useStateCount,
79
+ useEffectCount,
80
+ useCallbackCount,
81
+ useMemoCount,
82
+ useRefCount,
83
+ totalHooks,
84
+ thresholds: {
85
+ maxUseState: cfg.maxUseState,
86
+ maxUseEffect: cfg.maxUseEffect,
87
+ maxTotalHooks: cfg.maxTotalHooks,
88
+ },
89
+ },
90
+ });
91
+ }
92
+ }
93
+ return detections;
94
+ }
95
+ const sniffer = {
96
+ name: 'god-hook',
97
+ description: 'Detects custom hooks that have grown too large with too many useState, useEffect, or total hook calls, suggesting they be split into focused sub-hooks.',
98
+ meta: {
99
+ name: 'god-hook',
100
+ description: 'Detects custom hooks that have grown too large with too many useState, useEffect, or total hook calls, suggesting they be split into focused sub-hooks.',
101
+ category: 'hooks',
102
+ severity: 'warning',
103
+ defaultConfig: { ...DEFAULT_CONFIG },
104
+ },
105
+ detect,
106
+ };
107
+ exports.godHookSniffer = sniffer;
108
+ exports.default = sniffer;
109
+ //# sourceMappingURL=god-hook-sniffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"god-hook-sniffer.js","sourceRoot":"","sources":["../../../src/sniffers/god-hook-sniffer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { SnifferExport } from './sniffer-interface.js';
2
+ declare const sniffer: SnifferExport;
3
+ export default sniffer;
4
+ export { sniffer as propDrillingSniffer };
5
+ //# sourceMappingURL=prop-drilling-sniffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop-drilling-sniffer.d.ts","sourceRoot":"","sources":["../../../src/sniffers/prop-drilling-sniffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,aAAa,EAAY,MAAM,wBAAwB,CAAC;AAwJjF,QAAA,MAAM,OAAO,EAAE,aA4Bd,CAAC;AAEF,eAAe,OAAO,CAAC;AACvB,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.propDrillingSniffer = void 0;
4
+ const regex_helpers_js_1 = require("../utils/regex-helpers.js");
5
+ const DEFAULT_WHITELISTED_PROPS = [
6
+ 'className',
7
+ 'style',
8
+ 'children',
9
+ 'key',
10
+ 'ref',
11
+ 'id',
12
+ 'data-testid',
13
+ ];
14
+ /**
15
+ * Build a suggestion message for a prop-drilling detection.
16
+ */
17
+ function buildSuggestion(componentName, propName) {
18
+ return (`**Possible prop drilling in \`${componentName}\`:**\n` +
19
+ `- Use React Context to provide \`${propName}\` to deeper components\n` +
20
+ '- Use component composition (children/render props)\n' +
21
+ '- Consider a state management solution');
22
+ }
23
+ /**
24
+ * Count how many times a prop name appears as a JSX pass-through pattern
25
+ * in the given stripped source. A pass-through pattern is:
26
+ * propName={propName}
27
+ * inside a PascalCase JSX element (child component).
28
+ */
29
+ function countPassThroughOccurrences(strippedBody, propName) {
30
+ // Match patterns like: propName={propName} inside JSX of a child component.
31
+ // The propName on the left is the JSX attribute, the one on the right is the value.
32
+ // We need word boundaries to avoid partial matches.
33
+ const pattern = new RegExp(`\\b${escapeRegex(propName)}\\s*=\\s*\\{\\s*${escapeRegex(propName)}\\s*\\}`, 'g');
34
+ const matches = strippedBody.match(pattern);
35
+ return matches ? matches.length : 0;
36
+ }
37
+ /**
38
+ * Count all occurrences of a prop name as a standalone identifier
39
+ * (word boundary on both sides) in the stripped body.
40
+ * Excludes the destructured parameter declaration itself.
41
+ */
42
+ function countAllOccurrences(strippedBody, propName) {
43
+ const pattern = new RegExp(`\\b${escapeRegex(propName)}\\b`, 'g');
44
+ const matches = strippedBody.match(pattern);
45
+ return matches ? matches.length : 0;
46
+ }
47
+ /**
48
+ * Escape special regex characters in a string.
49
+ */
50
+ function escapeRegex(str) {
51
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
+ }
53
+ /**
54
+ * Detect props that are received by a component via destructuring
55
+ * and only forwarded to child components without any local usage.
56
+ */
57
+ function detectPassThroughProps(fileContent, filePath, whitelistedProps, severity) {
58
+ const detections = [];
59
+ const whitelistSet = new Set(whitelistedProps);
60
+ // Work on the original source for position tracking, but use stripped for analysis
61
+ const regex = new RegExp(regex_helpers_js_1.FUNCTIONAL_COMPONENT_DECL.source, regex_helpers_js_1.FUNCTIONAL_COMPONENT_DECL.flags);
62
+ let match;
63
+ while ((match = regex.exec(fileContent)) !== null) {
64
+ const componentName = match[1] || match[2] || match[3];
65
+ if (!componentName)
66
+ continue;
67
+ // Extract destructured props from the component declaration
68
+ const afterMatch = fileContent.substring(match.index);
69
+ const destructuredMatch = regex_helpers_js_1.DESTRUCTURED_PROPS.exec(afterMatch);
70
+ if (!destructuredMatch)
71
+ continue;
72
+ const propsString = destructuredMatch[1];
73
+ const propNames = (0, regex_helpers_js_1.parseDestructuredProps)(propsString);
74
+ if (propNames.length === 0)
75
+ continue;
76
+ // Find the component body (the opening brace of the function body)
77
+ // We need to find the brace after the parameter list / arrow / return type
78
+ const declEnd = match.index + afterMatch.indexOf(destructuredMatch[0]) + destructuredMatch[0].length;
79
+ const braceIndex = (0, regex_helpers_js_1.findOpeningBrace)(fileContent, declEnd);
80
+ if (braceIndex === -1)
81
+ continue;
82
+ const body = (0, regex_helpers_js_1.extractBracedBlock)(fileContent, braceIndex);
83
+ if (!body)
84
+ continue;
85
+ // Strip comments and strings from the body for accurate analysis
86
+ const strippedBody = (0, regex_helpers_js_1.stripCommentsAndStrings)(body);
87
+ const line = (0, regex_helpers_js_1.getLineNumber)(fileContent, match.index);
88
+ for (const propName of propNames) {
89
+ // Skip whitelisted props
90
+ if (whitelistSet.has(propName))
91
+ continue;
92
+ // Count all identifier occurrences in the body
93
+ const totalOccurrences = countAllOccurrences(strippedBody, propName);
94
+ // Count pass-through occurrences (propName={propName} patterns)
95
+ const passThroughCount = countPassThroughOccurrences(strippedBody, propName);
96
+ // Each pass-through pattern like `propName={propName}` contains 2 occurrences
97
+ // of the identifier: the attribute name and the value.
98
+ const occurrencesAccountedByPassThrough = passThroughCount * 2;
99
+ // A prop is pass-through only if:
100
+ // 1. There is at least one pass-through usage
101
+ // 2. All occurrences of the identifier are accounted for by pass-through patterns
102
+ if (passThroughCount > 0 && totalOccurrences === occurrencesAccountedByPassThrough) {
103
+ detections.push({
104
+ snifferName: 'prop-drilling',
105
+ filePath,
106
+ line,
107
+ column: 1,
108
+ message: `Component "${componentName}" passes prop "${propName}" through without using it`,
109
+ severity,
110
+ suggestion: buildSuggestion(componentName, propName),
111
+ details: {
112
+ componentName,
113
+ propName,
114
+ passThroughCount,
115
+ },
116
+ });
117
+ }
118
+ }
119
+ }
120
+ return detections;
121
+ }
122
+ const sniffer = {
123
+ name: 'prop-drilling',
124
+ description: 'Detects props that are received by a component and passed through to children without local usage, indicating possible prop drilling.',
125
+ meta: {
126
+ name: 'prop-drilling',
127
+ description: 'Detects props that are received by a component and passed through to children without local usage, indicating possible prop drilling.',
128
+ category: 'props',
129
+ severity: 'warning',
130
+ defaultConfig: {
131
+ severity: 'warning',
132
+ whitelistedProps: DEFAULT_WHITELISTED_PROPS,
133
+ },
134
+ },
135
+ detect(fileContent, filePath, config) {
136
+ const severity = config.severity || 'warning';
137
+ const whitelistedProps = Array.isArray(config.whitelistedProps)
138
+ ? config.whitelistedProps
139
+ : DEFAULT_WHITELISTED_PROPS;
140
+ return detectPassThroughProps(fileContent, filePath, whitelistedProps, severity);
141
+ },
142
+ };
143
+ exports.propDrillingSniffer = sniffer;
144
+ exports.default = sniffer;
145
+ //# sourceMappingURL=prop-drilling-sniffer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop-drilling-sniffer.js","sourceRoot":"","sources":["../../../src/sniffers/prop-drilling-sniffer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { SnifferExport } from './sniffer-interface.js';
2
+ declare const propExplosionSniffer: SnifferExport;
3
+ export default propExplosionSniffer;
4
+ //# sourceMappingURL=prop-explosion-sniffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prop-explosion-sniffer.d.ts","sourceRoot":"","sources":["../../../src/sniffers/prop-explosion-sniffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,aAAa,EAAY,MAAM,wBAAwB,CAAC;AAoJjF,QAAA,MAAM,oBAAoB,EAAE,aA+C3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}