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.
- package/.snifferrc.json +29 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/bin/react-sniff.js +3 -0
- package/dist/src/cli/arg-parser.d.ts +10 -0
- package/dist/src/cli/arg-parser.d.ts.map +1 -0
- package/dist/src/cli/arg-parser.js +81 -0
- package/dist/src/cli/arg-parser.js.map +1 -0
- package/dist/src/cli/config-loader.d.ts +11 -0
- package/dist/src/cli/config-loader.d.ts.map +1 -0
- package/dist/src/cli/config-loader.js +140 -0
- package/dist/src/cli/config-loader.js.map +1 -0
- package/dist/src/cli/help.d.ts +3 -0
- package/dist/src/cli/help.d.ts.map +1 -0
- package/dist/src/cli/help.js +59 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.d.ts.map +1 -0
- package/dist/src/cli/index.js +107 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/core/file-discoverer.d.ts +8 -0
- package/dist/src/core/file-discoverer.d.ts.map +1 -0
- package/dist/src/core/file-discoverer.js +151 -0
- package/dist/src/core/file-discoverer.js.map +1 -0
- package/dist/src/core/orchestrator.d.ts +13 -0
- package/dist/src/core/orchestrator.d.ts.map +1 -0
- package/dist/src/core/orchestrator.js +176 -0
- package/dist/src/core/orchestrator.js.map +1 -0
- package/dist/src/core/sniffer-ignore.d.ts +25 -0
- package/dist/src/core/sniffer-ignore.d.ts.map +1 -0
- package/dist/src/core/sniffer-ignore.js +91 -0
- package/dist/src/core/sniffer-ignore.js.map +1 -0
- package/dist/src/core/sniffer-registry.d.ts +8 -0
- package/dist/src/core/sniffer-registry.d.ts.map +1 -0
- package/dist/src/core/sniffer-registry.js +64 -0
- package/dist/src/core/sniffer-registry.js.map +1 -0
- package/dist/src/core/worker-pool.d.ts +27 -0
- package/dist/src/core/worker-pool.d.ts.map +1 -0
- package/dist/src/core/worker-pool.js +176 -0
- package/dist/src/core/worker-pool.js.map +1 -0
- package/dist/src/core/worker-runner.d.ts +2 -0
- package/dist/src/core/worker-runner.d.ts.map +1 -0
- package/dist/src/core/worker-runner.js +52 -0
- package/dist/src/core/worker-runner.js.map +1 -0
- package/dist/src/output/formatter.d.ts +3 -0
- package/dist/src/output/formatter.d.ts.map +1 -0
- package/dist/src/output/formatter.js +13 -0
- package/dist/src/output/formatter.js.map +1 -0
- package/dist/src/output/json-renderer.d.ts +3 -0
- package/dist/src/output/json-renderer.d.ts.map +1 -0
- package/dist/src/output/json-renderer.js +49 -0
- package/dist/src/output/json-renderer.js.map +1 -0
- package/dist/src/output/markdown-renderer.d.ts +3 -0
- package/dist/src/output/markdown-renderer.d.ts.map +1 -0
- package/dist/src/output/markdown-renderer.js +70 -0
- package/dist/src/output/markdown-renderer.js.map +1 -0
- package/dist/src/plugins/plugin-loader.d.ts +7 -0
- package/dist/src/plugins/plugin-loader.d.ts.map +1 -0
- package/dist/src/plugins/plugin-loader.js +47 -0
- package/dist/src/plugins/plugin-loader.js.map +1 -0
- package/dist/src/plugins/plugin-sandbox.d.ts +3 -0
- package/dist/src/plugins/plugin-sandbox.d.ts.map +1 -0
- package/dist/src/plugins/plugin-sandbox.js +105 -0
- package/dist/src/plugins/plugin-sandbox.js.map +1 -0
- package/dist/src/plugins/plugin-validator.d.ts +14 -0
- package/dist/src/plugins/plugin-validator.d.ts.map +1 -0
- package/dist/src/plugins/plugin-validator.js +92 -0
- package/dist/src/plugins/plugin-validator.js.map +1 -0
- package/dist/src/sniffers/god-hook-sniffer.d.ts +12 -0
- package/dist/src/sniffers/god-hook-sniffer.d.ts.map +1 -0
- package/dist/src/sniffers/god-hook-sniffer.js +109 -0
- package/dist/src/sniffers/god-hook-sniffer.js.map +1 -0
- package/dist/src/sniffers/prop-drilling-sniffer.d.ts +5 -0
- package/dist/src/sniffers/prop-drilling-sniffer.d.ts.map +1 -0
- package/dist/src/sniffers/prop-drilling-sniffer.js +145 -0
- package/dist/src/sniffers/prop-drilling-sniffer.js.map +1 -0
- package/dist/src/sniffers/prop-explosion-sniffer.d.ts +4 -0
- package/dist/src/sniffers/prop-explosion-sniffer.d.ts.map +1 -0
- package/dist/src/sniffers/prop-explosion-sniffer.js +134 -0
- package/dist/src/sniffers/prop-explosion-sniffer.js.map +1 -0
- package/dist/src/sniffers/sniffer-interface.d.ts +88 -0
- package/dist/src/sniffers/sniffer-interface.d.ts.map +1 -0
- package/dist/src/sniffers/sniffer-interface.js +18 -0
- package/dist/src/sniffers/sniffer-interface.js.map +1 -0
- package/dist/src/tui/interactive-viewer.d.ts +7 -0
- package/dist/src/tui/interactive-viewer.d.ts.map +1 -0
- package/dist/src/tui/interactive-viewer.js +453 -0
- package/dist/src/tui/interactive-viewer.js.map +1 -0
- package/dist/src/utils/logger.d.ts +11 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +90 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/regex-helpers.d.ts +53 -0
- package/dist/src/utils/regex-helpers.d.ts.map +1 -0
- package/dist/src/utils/regex-helpers.js +275 -0
- package/dist/src/utils/regex-helpers.js.map +1 -0
- 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":"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"}
|
|
@@ -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":";AAAA;;;;;;GAMG;;;AAGH,gEAamC;AASnC,MAAM,cAAc,GAAkB;IACpC,WAAW,EAAE,CAAC;IACd,YAAY,EAAE,CAAC;IACf,aAAa,EAAE,EAAE;IACjB,QAAQ,EAAE,SAAS;CACpB,CAAC;AAEF,SAAS,aAAa,CAAC,MAA+B;IACpD,OAAO;QACL,WAAW,EACT,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC,WAAW;QAC1F,YAAY,EACV,OAAO,MAAM,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,CAAC,YAAY;QAC7F,aAAa,EACX,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,cAAc,CAAC,aAAa;QAChG,QAAQ,EACN,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAM,CAAC,QAAqB,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ;KAChG,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CACb,WAAmB,EACnB,QAAgB,EAChB,MAA+B;IAE/B,MAAM,UAAU,GAAgB,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAElC,oCAAoC;IACpC,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,mCAAgB,CAAC,MAAM,EAAE,mCAAgB,CAAC,KAAK,CAAC,CAAC;IAElF,IAAI,KAA6B,CAAC;IAElC,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC;QAE/B,0CAA0C;QAC1C,MAAM,UAAU,GAAG,IAAA,mCAAgB,EAAC,WAAW,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC/E,IAAI,UAAU,KAAK,CAAC,CAAC;YAAE,SAAS;QAEhC,6BAA6B;QAC7B,MAAM,IAAI,GAAG,IAAA,qCAAkB,EAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,sDAAsD;QACtD,MAAM,YAAY,GAAG,IAAA,0CAAuB,EAAC,IAAI,CAAC,CAAC;QAEnD,8BAA8B;QAC9B,MAAM,aAAa,GAAG,IAAA,+BAAY,EAAC,YAAY,EAAE,4BAAS,CAAC,CAAC;QAC5D,MAAM,cAAc,GAAG,IAAA,+BAAY,EAAC,YAAY,EAAE,6BAAU,CAAC,CAAC;QAC9D,MAAM,gBAAgB,GAAG,IAAA,+BAAY,EAAC,YAAY,EAAE,+BAAY,CAAC,CAAC;QAClE,MAAM,YAAY,GAAG,IAAA,+BAAY,EAAC,YAAY,EAAE,2BAAQ,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAA,+BAAY,EAAC,YAAY,EAAE,0BAAO,CAAC,CAAC;QACxD,MAAM,oBAAoB,GAAG,IAAA,+BAAY,EAAC,YAAY,EAAE,oCAAiB,CAAC,CAAC;QAE3E,MAAM,UAAU,GACd,aAAa;YACb,cAAc;YACd,gBAAgB;YAChB,YAAY;YACZ,WAAW;YACX,oBAAoB,CAAC;QAEvB,mBAAmB;QACnB,MAAM,eAAe,GAAG,aAAa,GAAG,GAAG,CAAC,WAAW,CAAC;QACxD,MAAM,gBAAgB,GAAG,cAAc,GAAG,GAAG,CAAC,YAAY,CAAC;QAC3D,MAAM,iBAAiB,GAAG,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC;QAEzD,IAAI,eAAe,IAAI,gBAAgB,IAAI,iBAAiB,EAAE,CAAC;YAC7D,MAAM,IAAI,GAAG,IAAA,gCAAa,EAAC,WAAW,EAAE,UAAU,CAAC,CAAC;YAEpD,UAAU,CAAC,IAAI,CAAC;gBACd,WAAW,EAAE,UAAU;gBACvB,QAAQ;gBACR,IAAI;gBACJ,MAAM,EAAE,CAAC;gBACT,OAAO,EAAE,SAAS,QAAQ,SAAS,aAAa,cAAc,cAAc,eAAe,UAAU,mBAAmB;gBACxH,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,UAAU,EACR,0BAA0B,QAAQ,SAAS;oBAC3C,4DAA4D;oBAC5D,mDAAmD;oBACnD,kDAAkD;oBAClD,mDAAmD;gBACrD,OAAO,EAAE;oBACP,QAAQ;oBACR,aAAa;oBACb,cAAc;oBACd,gBAAgB;oBAChB,YAAY;oBACZ,WAAW;oBACX,UAAU;oBACV,UAAU,EAAE;wBACV,WAAW,EAAE,GAAG,CAAC,WAAW;wBAC5B,YAAY,EAAE,GAAG,CAAC,YAAY;wBAC9B,aAAa,EAAE,GAAG,CAAC,aAAa;qBACjC;iBACF;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,OAAO,GAAkB;IAC7B,IAAI,EAAE,UAAU;IAChB,WAAW,EACT,yJAAyJ;IAC3J,IAAI,EAAE;QACJ,IAAI,EAAE,UAAU;QAChB,WAAW,EACT,yJAAyJ;QAC3J,QAAQ,EAAE,OAAO;QACjB,QAAQ,EAAE,SAAS;QACnB,aAAa,EAAE,EAAE,GAAG,cAAc,EAAE;KACrC;IACD,MAAM;CACP,CAAC;AAGkB,iCAAc;AADlC,kBAAe,OAAO,CAAC"}
|
|
@@ -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 @@
|
|
|
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"}
|