react-code-smell-detector 1.4.2 → 1.5.1
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/README.md +227 -22
- package/dist/__tests__/aiRefactoring.test.d.ts +2 -0
- package/dist/__tests__/aiRefactoring.test.d.ts.map +1 -0
- package/dist/__tests__/aiRefactoring.test.js +86 -0
- package/dist/__tests__/analyzer-real.test.d.ts +2 -0
- package/dist/__tests__/analyzer-real.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer-real.test.js +149 -0
- package/dist/__tests__/analyzer.test.d.ts +2 -0
- package/dist/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer.test.js +173 -0
- package/dist/__tests__/baseline.test.d.ts +2 -0
- package/dist/__tests__/baseline.test.d.ts.map +1 -0
- package/dist/__tests__/baseline.test.js +136 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts +2 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts.map +1 -0
- package/dist/__tests__/bundleAnalyzer.test.js +182 -0
- package/dist/__tests__/customRules.test.d.ts +2 -0
- package/dist/__tests__/customRules.test.d.ts.map +1 -0
- package/dist/__tests__/customRules.test.js +283 -0
- package/dist/__tests__/detectors/index.test.d.ts +2 -0
- package/dist/__tests__/detectors/index.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/index.test.js +1012 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts +2 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/newDetectors.test.js +333 -0
- package/dist/__tests__/docGenerator.test.d.ts +2 -0
- package/dist/__tests__/docGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/docGenerator.test.js +157 -0
- package/dist/__tests__/fixer.test.d.ts +2 -0
- package/dist/__tests__/fixer.test.d.ts.map +1 -0
- package/dist/__tests__/fixer.test.js +193 -0
- package/dist/__tests__/git.test.d.ts +2 -0
- package/dist/__tests__/git.test.d.ts.map +1 -0
- package/dist/__tests__/git.test.js +38 -0
- package/dist/__tests__/graphGenerator.test.d.ts +2 -0
- package/dist/__tests__/graphGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/graphGenerator.test.js +190 -0
- package/dist/__tests__/htmlReporter.test.d.ts +2 -0
- package/dist/__tests__/htmlReporter.test.d.ts.map +1 -0
- package/dist/__tests__/htmlReporter.test.js +258 -0
- package/dist/__tests__/interactiveFixer.test.d.ts +2 -0
- package/dist/__tests__/interactiveFixer.test.d.ts.map +1 -0
- package/dist/__tests__/interactiveFixer.test.js +231 -0
- package/dist/__tests__/parser.test.d.ts +2 -0
- package/dist/__tests__/parser.test.d.ts.map +1 -0
- package/dist/__tests__/parser.test.js +56 -0
- package/dist/__tests__/performanceBudget.test.d.ts +2 -0
- package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
- package/dist/__tests__/performanceBudget.test.js +242 -0
- package/dist/__tests__/prComments.test.d.ts +2 -0
- package/dist/__tests__/prComments.test.d.ts.map +1 -0
- package/dist/__tests__/prComments.test.js +118 -0
- package/dist/__tests__/reporter.test.d.ts +2 -0
- package/dist/__tests__/reporter.test.d.ts.map +1 -0
- package/dist/__tests__/reporter.test.js +136 -0
- package/dist/__tests__/watcher.test.d.ts +2 -0
- package/dist/__tests__/watcher.test.d.ts.map +1 -0
- package/dist/__tests__/watcher.test.js +161 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +209 -0
- package/dist/aiRefactoring.d.ts +29 -0
- package/dist/aiRefactoring.d.ts.map +1 -0
- package/dist/aiRefactoring.js +290 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +33 -1
- package/dist/cli.js +123 -1
- package/dist/detectors/contextApi.d.ts +11 -0
- package/dist/detectors/contextApi.d.ts.map +1 -0
- package/dist/detectors/contextApi.js +151 -0
- package/dist/detectors/errorBoundary.d.ts +11 -0
- package/dist/detectors/errorBoundary.d.ts.map +1 -0
- package/dist/detectors/errorBoundary.js +167 -0
- package/dist/detectors/formValidation.d.ts +11 -0
- package/dist/detectors/formValidation.d.ts.map +1 -0
- package/dist/detectors/formValidation.js +193 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +12 -0
- package/dist/detectors/serverComponents.d.ts +11 -0
- package/dist/detectors/serverComponents.d.ts.map +1 -0
- package/dist/detectors/serverComponents.js +222 -0
- package/dist/detectors/stateManagement.d.ts +11 -0
- package/dist/detectors/stateManagement.d.ts.map +1 -0
- package/dist/detectors/stateManagement.js +193 -0
- package/dist/detectors/testingGaps.d.ts +15 -0
- package/dist/detectors/testingGaps.d.ts.map +1 -0
- package/dist/detectors/testingGaps.js +182 -0
- package/dist/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/guide.d.ts +9 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +922 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/interactiveFixer.d.ts +20 -0
- package/dist/interactiveFixer.d.ts.map +1 -0
- package/dist/interactiveFixer.js +178 -0
- package/dist/performanceBudget.d.ts +54 -0
- package/dist/performanceBudget.d.ts.map +1 -0
- package/dist/performanceBudget.js +218 -0
- package/dist/prComments.d.ts +47 -0
- package/dist/prComments.d.ts.map +1 -0
- package/dist/prComments.js +233 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/package.json +10 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhooks.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { sendWebhookNotification, getWebhookConfig } from '../webhooks.js';
|
|
3
|
+
// Mock https module
|
|
4
|
+
vi.mock('https', () => ({
|
|
5
|
+
default: {
|
|
6
|
+
request: vi.fn((options, callback) => {
|
|
7
|
+
const mockRes = { statusCode: 200 };
|
|
8
|
+
setTimeout(() => callback(mockRes), 0);
|
|
9
|
+
return {
|
|
10
|
+
on: vi.fn(),
|
|
11
|
+
write: vi.fn(),
|
|
12
|
+
end: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
}),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
describe('Webhooks', () => {
|
|
18
|
+
let envBackup;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
envBackup = { ...process.env };
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
process.env = envBackup;
|
|
25
|
+
});
|
|
26
|
+
const createSmell = (type, severity = 'warning') => ({
|
|
27
|
+
type: type,
|
|
28
|
+
severity,
|
|
29
|
+
message: 'Test smell',
|
|
30
|
+
file: '/test.tsx',
|
|
31
|
+
line: 1,
|
|
32
|
+
column: 0,
|
|
33
|
+
suggestion: 'Fix it',
|
|
34
|
+
});
|
|
35
|
+
const createManySmells = (count) => {
|
|
36
|
+
return Array.from({ length: count }, (_, i) => createSmell(`type-${i % 5}`, i % 3 === 0 ? 'error' : 'warning'));
|
|
37
|
+
};
|
|
38
|
+
describe('sendWebhookNotification', () => {
|
|
39
|
+
it('should return false when webhook url is empty', async () => {
|
|
40
|
+
const config = {
|
|
41
|
+
url: '',
|
|
42
|
+
type: 'slack',
|
|
43
|
+
};
|
|
44
|
+
const smells = [createSmell('debug-statement')];
|
|
45
|
+
const result = await sendWebhookNotification(config, smells, 'test-project');
|
|
46
|
+
expect(result).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it('should return false when below threshold', async () => {
|
|
49
|
+
const config = {
|
|
50
|
+
url: 'https://hooks.example.com/test',
|
|
51
|
+
type: 'slack',
|
|
52
|
+
threshold: 10,
|
|
53
|
+
};
|
|
54
|
+
const smells = [createSmell('debug-statement')];
|
|
55
|
+
const result = await sendWebhookNotification(config, smells, 'test-project');
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('should format slack message correctly', async () => {
|
|
59
|
+
const config = {
|
|
60
|
+
url: 'https://hooks.slack.com/services/test',
|
|
61
|
+
type: 'slack',
|
|
62
|
+
};
|
|
63
|
+
const smells = createManySmells(5);
|
|
64
|
+
const result = await sendWebhookNotification(config, smells, 'test-project', {
|
|
65
|
+
branch: 'main',
|
|
66
|
+
commit: 'abc123def456',
|
|
67
|
+
author: 'Test User',
|
|
68
|
+
});
|
|
69
|
+
expect(typeof result).toBe('boolean');
|
|
70
|
+
});
|
|
71
|
+
it('should format discord message correctly', async () => {
|
|
72
|
+
const config = {
|
|
73
|
+
url: 'https://discord.com/api/webhooks/test',
|
|
74
|
+
type: 'discord',
|
|
75
|
+
};
|
|
76
|
+
const smells = createManySmells(15);
|
|
77
|
+
const result = await sendWebhookNotification(config, smells, 'test-project', {
|
|
78
|
+
branch: 'develop',
|
|
79
|
+
author: 'Dev User',
|
|
80
|
+
});
|
|
81
|
+
expect(typeof result).toBe('boolean');
|
|
82
|
+
});
|
|
83
|
+
it('should format generic message correctly', async () => {
|
|
84
|
+
const config = {
|
|
85
|
+
url: 'https://example.com/webhook',
|
|
86
|
+
type: 'generic',
|
|
87
|
+
};
|
|
88
|
+
const smells = createManySmells(25);
|
|
89
|
+
const result = await sendWebhookNotification(config, smells, 'test-project', {
|
|
90
|
+
branch: 'feature/test',
|
|
91
|
+
commit: 'xyz789',
|
|
92
|
+
});
|
|
93
|
+
expect(typeof result).toBe('boolean');
|
|
94
|
+
});
|
|
95
|
+
it('should handle high smell count (danger color)', async () => {
|
|
96
|
+
const config = {
|
|
97
|
+
url: 'https://hooks.slack.com/services/test',
|
|
98
|
+
type: 'slack',
|
|
99
|
+
};
|
|
100
|
+
const smells = createManySmells(25);
|
|
101
|
+
const result = await sendWebhookNotification(config, smells, 'test-project');
|
|
102
|
+
expect(typeof result).toBe('boolean');
|
|
103
|
+
});
|
|
104
|
+
it('should handle critical smell count (50+)', async () => {
|
|
105
|
+
const config = {
|
|
106
|
+
url: 'https://hooks.slack.com/services/test',
|
|
107
|
+
type: 'slack',
|
|
108
|
+
};
|
|
109
|
+
const smells = createManySmells(55);
|
|
110
|
+
const result = await sendWebhookNotification(config, smells, 'test-project');
|
|
111
|
+
expect(typeof result).toBe('boolean');
|
|
112
|
+
});
|
|
113
|
+
it('should handle zero smells', async () => {
|
|
114
|
+
const config = {
|
|
115
|
+
url: 'https://hooks.slack.com/services/test',
|
|
116
|
+
type: 'slack',
|
|
117
|
+
};
|
|
118
|
+
const result = await sendWebhookNotification(config, [], 'test-project');
|
|
119
|
+
expect(typeof result).toBe('boolean');
|
|
120
|
+
});
|
|
121
|
+
it('should include details when flag is set', async () => {
|
|
122
|
+
const config = {
|
|
123
|
+
url: 'https://hooks.slack.com/services/test',
|
|
124
|
+
type: 'slack',
|
|
125
|
+
includeDetails: true,
|
|
126
|
+
};
|
|
127
|
+
const smells = [createSmell('debug-statement')];
|
|
128
|
+
const result = await sendWebhookNotification(config, smells, 'test-project');
|
|
129
|
+
expect(typeof result).toBe('boolean');
|
|
130
|
+
});
|
|
131
|
+
it('should handle discord with includeDetails', async () => {
|
|
132
|
+
const config = {
|
|
133
|
+
url: 'https://discord.com/api/webhooks/test',
|
|
134
|
+
type: 'discord',
|
|
135
|
+
includeDetails: true,
|
|
136
|
+
};
|
|
137
|
+
const smells = createManySmells(12);
|
|
138
|
+
const result = await sendWebhookNotification(config, smells, 'test-project');
|
|
139
|
+
expect(typeof result).toBe('boolean');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('getWebhookConfig', () => {
|
|
143
|
+
it('should return null when no webhook configured', () => {
|
|
144
|
+
delete process.env.REACT_SMELL_SLACK_WEBHOOK;
|
|
145
|
+
delete process.env.SLACK_WEBHOOK_URL;
|
|
146
|
+
delete process.env.REACT_SMELL_DISCORD_WEBHOOK;
|
|
147
|
+
delete process.env.DISCORD_WEBHOOK_URL;
|
|
148
|
+
delete process.env.REACT_SMELL_WEBHOOK;
|
|
149
|
+
const config = getWebhookConfig();
|
|
150
|
+
expect(config).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
it('should return slack config when slack URL provided as argument', () => {
|
|
153
|
+
const config = getWebhookConfig('https://hooks.slack.com/test');
|
|
154
|
+
expect(config).not.toBeNull();
|
|
155
|
+
expect(config?.type).toBe('slack');
|
|
156
|
+
expect(config?.url).toBe('https://hooks.slack.com/test');
|
|
157
|
+
});
|
|
158
|
+
it('should return discord config when discord URL provided as argument', () => {
|
|
159
|
+
const config = getWebhookConfig(undefined, 'https://discord.com/api/webhooks/test');
|
|
160
|
+
expect(config).not.toBeNull();
|
|
161
|
+
expect(config?.type).toBe('discord');
|
|
162
|
+
});
|
|
163
|
+
it('should return generic config when generic URL provided as argument', () => {
|
|
164
|
+
const config = getWebhookConfig(undefined, undefined, 'https://example.com/webhook');
|
|
165
|
+
expect(config).not.toBeNull();
|
|
166
|
+
expect(config?.type).toBe('generic');
|
|
167
|
+
});
|
|
168
|
+
it('should prefer slack over discord', () => {
|
|
169
|
+
const config = getWebhookConfig('https://hooks.slack.com/test', 'https://discord.com/api/webhooks/test');
|
|
170
|
+
expect(config?.type).toBe('slack');
|
|
171
|
+
});
|
|
172
|
+
it('should read from REACT_SMELL_SLACK_WEBHOOK env var', () => {
|
|
173
|
+
process.env.REACT_SMELL_SLACK_WEBHOOK = 'https://hooks.slack.com/env';
|
|
174
|
+
const config = getWebhookConfig();
|
|
175
|
+
expect(config?.type).toBe('slack');
|
|
176
|
+
expect(config?.url).toBe('https://hooks.slack.com/env');
|
|
177
|
+
});
|
|
178
|
+
it('should read from SLACK_WEBHOOK_URL env var', () => {
|
|
179
|
+
delete process.env.REACT_SMELL_SLACK_WEBHOOK;
|
|
180
|
+
process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/env2';
|
|
181
|
+
const config = getWebhookConfig();
|
|
182
|
+
expect(config?.type).toBe('slack');
|
|
183
|
+
});
|
|
184
|
+
it('should read from REACT_SMELL_DISCORD_WEBHOOK env var', () => {
|
|
185
|
+
delete process.env.REACT_SMELL_SLACK_WEBHOOK;
|
|
186
|
+
delete process.env.SLACK_WEBHOOK_URL;
|
|
187
|
+
process.env.REACT_SMELL_DISCORD_WEBHOOK = 'https://discord.com/api/webhooks/env';
|
|
188
|
+
const config = getWebhookConfig();
|
|
189
|
+
expect(config?.type).toBe('discord');
|
|
190
|
+
});
|
|
191
|
+
it('should read from DISCORD_WEBHOOK_URL env var', () => {
|
|
192
|
+
delete process.env.REACT_SMELL_SLACK_WEBHOOK;
|
|
193
|
+
delete process.env.SLACK_WEBHOOK_URL;
|
|
194
|
+
delete process.env.REACT_SMELL_DISCORD_WEBHOOK;
|
|
195
|
+
process.env.DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/env2';
|
|
196
|
+
const config = getWebhookConfig();
|
|
197
|
+
expect(config?.type).toBe('discord');
|
|
198
|
+
});
|
|
199
|
+
it('should read from REACT_SMELL_WEBHOOK env var', () => {
|
|
200
|
+
delete process.env.REACT_SMELL_SLACK_WEBHOOK;
|
|
201
|
+
delete process.env.SLACK_WEBHOOK_URL;
|
|
202
|
+
delete process.env.REACT_SMELL_DISCORD_WEBHOOK;
|
|
203
|
+
delete process.env.DISCORD_WEBHOOK_URL;
|
|
204
|
+
process.env.REACT_SMELL_WEBHOOK = 'https://example.com/generic';
|
|
205
|
+
const config = getWebhookConfig();
|
|
206
|
+
expect(config?.type).toBe('generic');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CodeSmell, AnalysisResult } from './types/index.js';
|
|
2
|
+
export interface AIRefactoringSuggestion {
|
|
3
|
+
smell: CodeSmell;
|
|
4
|
+
originalCode: string;
|
|
5
|
+
suggestedCode: string;
|
|
6
|
+
explanation: string;
|
|
7
|
+
confidence: number;
|
|
8
|
+
estimatedEffort: 'low' | 'medium' | 'high';
|
|
9
|
+
}
|
|
10
|
+
export interface AIRefactoringConfig {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
model: string;
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
temperature?: number;
|
|
15
|
+
provider?: 'openai' | 'anthropic' | 'azure';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate AI-powered refactoring suggestions for detected code smells
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateAIRefactoringSuggestions(smells: CodeSmell[], sourceCode: string, config: AIRefactoringConfig): Promise<AIRefactoringSuggestion[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Generate a summary report with AI insights
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateAIAnalysisReport(result: AnalysisResult, config: AIRefactoringConfig): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Get refactoring suggestions for specific smell types
|
|
27
|
+
*/
|
|
28
|
+
export declare function getQuickRefactoringTemplates(smellType: string): string[];
|
|
29
|
+
//# sourceMappingURL=aiRefactoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiRefactoring.d.ts","sourceRoot":"","sources":["../src/aiRefactoring.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAE7E,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,SAAS,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;CAC7C;AAED;;GAEG;AACH,wBAAsB,gCAAgC,CACpD,MAAM,EAAE,SAAS,EAAE,EACnB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAmBpC;AAyND;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA6CjB;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAmCxE"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate AI-powered refactoring suggestions for detected code smells
|
|
3
|
+
*/
|
|
4
|
+
export async function generateAIRefactoringSuggestions(smells, sourceCode, config) {
|
|
5
|
+
if (!config.apiKey) {
|
|
6
|
+
console.warn('AI refactoring: No API key provided');
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const suggestions = [];
|
|
10
|
+
// Process smells in batches to avoid rate limits
|
|
11
|
+
const batchSize = 5;
|
|
12
|
+
for (let i = 0; i < smells.length; i += batchSize) {
|
|
13
|
+
const batch = smells.slice(i, i + batchSize);
|
|
14
|
+
const batchSuggestions = await Promise.all(batch.map(smell => generateSuggestionForSmell(smell, sourceCode, config)));
|
|
15
|
+
suggestions.push(...batchSuggestions.filter((s) => s !== null));
|
|
16
|
+
}
|
|
17
|
+
return suggestions;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a refactoring suggestion for a single smell
|
|
21
|
+
*/
|
|
22
|
+
async function generateSuggestionForSmell(smell, sourceCode, config) {
|
|
23
|
+
try {
|
|
24
|
+
const codeContext = extractCodeContext(sourceCode, smell.line, 10);
|
|
25
|
+
const prompt = buildRefactoringPrompt(smell, codeContext);
|
|
26
|
+
const response = await callLLMAPI(prompt, config);
|
|
27
|
+
if (!response)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
smell,
|
|
31
|
+
originalCode: codeContext,
|
|
32
|
+
suggestedCode: response.code || '',
|
|
33
|
+
explanation: response.explanation || smell.suggestion,
|
|
34
|
+
confidence: response.confidence || 0.7,
|
|
35
|
+
estimatedEffort: estimateEffort(smell.type),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(`AI refactoring failed for ${smell.type}:`, error);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract code context around a specific line
|
|
45
|
+
*/
|
|
46
|
+
function extractCodeContext(sourceCode, line, contextLines) {
|
|
47
|
+
const lines = sourceCode.split('\n');
|
|
48
|
+
const start = Math.max(0, line - contextLines - 1);
|
|
49
|
+
const end = Math.min(lines.length, line + contextLines);
|
|
50
|
+
return lines.slice(start, end).join('\n');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the prompt for the LLM
|
|
54
|
+
*/
|
|
55
|
+
function buildRefactoringPrompt(smell, codeContext) {
|
|
56
|
+
return `You are an expert React developer. Analyze this code smell and provide a refactored solution.
|
|
57
|
+
|
|
58
|
+
## Code Smell Detected
|
|
59
|
+
- Type: ${smell.type}
|
|
60
|
+
- Severity: ${smell.severity}
|
|
61
|
+
- Message: ${smell.message}
|
|
62
|
+
- Suggestion: ${smell.suggestion}
|
|
63
|
+
|
|
64
|
+
## Code Context (around line ${smell.line})
|
|
65
|
+
\`\`\`typescript
|
|
66
|
+
${codeContext}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
## Your Task
|
|
70
|
+
1. Provide the refactored code that fixes this issue
|
|
71
|
+
2. Explain why the refactoring improves the code
|
|
72
|
+
3. Rate your confidence in this suggestion (0-1)
|
|
73
|
+
|
|
74
|
+
Respond in JSON format:
|
|
75
|
+
{
|
|
76
|
+
"code": "// refactored code here",
|
|
77
|
+
"explanation": "Why this refactoring helps...",
|
|
78
|
+
"confidence": 0.85
|
|
79
|
+
}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Call the LLM API (supports OpenAI and Anthropic)
|
|
83
|
+
*/
|
|
84
|
+
async function callLLMAPI(prompt, config) {
|
|
85
|
+
const provider = config.provider || 'openai';
|
|
86
|
+
try {
|
|
87
|
+
if (provider === 'openai' || provider === 'azure') {
|
|
88
|
+
return await callOpenAI(prompt, config);
|
|
89
|
+
}
|
|
90
|
+
else if (provider === 'anthropic') {
|
|
91
|
+
return await callAnthropic(prompt, config);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('LLM API call failed:', error);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Call OpenAI API
|
|
101
|
+
*/
|
|
102
|
+
async function callOpenAI(prompt, config) {
|
|
103
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
model: config.model || 'gpt-4',
|
|
111
|
+
messages: [{ role: 'user', content: prompt }],
|
|
112
|
+
max_tokens: config.maxTokens || 1000,
|
|
113
|
+
temperature: config.temperature || 0.3,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(`OpenAI API error: ${response.status}`);
|
|
118
|
+
}
|
|
119
|
+
const data = await response.json();
|
|
120
|
+
const content = data.choices?.[0]?.message?.content;
|
|
121
|
+
if (!content)
|
|
122
|
+
return null;
|
|
123
|
+
try {
|
|
124
|
+
// Extract JSON from response (it might have markdown formatting)
|
|
125
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
126
|
+
if (jsonMatch) {
|
|
127
|
+
return JSON.parse(jsonMatch[0]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// If JSON parsing fails, extract what we can
|
|
132
|
+
return {
|
|
133
|
+
code: content,
|
|
134
|
+
explanation: 'AI-generated suggestion',
|
|
135
|
+
confidence: 0.5,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Call Anthropic API
|
|
142
|
+
*/
|
|
143
|
+
async function callAnthropic(prompt, config) {
|
|
144
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
'Content-Type': 'application/json',
|
|
148
|
+
'x-api-key': config.apiKey,
|
|
149
|
+
'anthropic-version': '2023-06-01',
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
model: config.model || 'claude-3-sonnet-20240229',
|
|
153
|
+
max_tokens: config.maxTokens || 1000,
|
|
154
|
+
messages: [{ role: 'user', content: prompt }],
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`Anthropic API error: ${response.status}`);
|
|
159
|
+
}
|
|
160
|
+
const data = await response.json();
|
|
161
|
+
const content = data.content?.[0]?.text;
|
|
162
|
+
if (!content)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
166
|
+
if (jsonMatch) {
|
|
167
|
+
return JSON.parse(jsonMatch[0]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return {
|
|
172
|
+
code: content,
|
|
173
|
+
explanation: 'AI-generated suggestion',
|
|
174
|
+
confidence: 0.5,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Estimate the effort required to fix a smell
|
|
181
|
+
*/
|
|
182
|
+
function estimateEffort(smellType) {
|
|
183
|
+
const lowEffort = [
|
|
184
|
+
'debug-statement',
|
|
185
|
+
'js-var-usage',
|
|
186
|
+
'js-loose-equality',
|
|
187
|
+
'a11y-missing-alt',
|
|
188
|
+
'magic-value',
|
|
189
|
+
'missing-key',
|
|
190
|
+
];
|
|
191
|
+
const highEffort = [
|
|
192
|
+
'prop-drilling',
|
|
193
|
+
'large-component',
|
|
194
|
+
'circular-dependency',
|
|
195
|
+
'high-cyclomatic-complexity',
|
|
196
|
+
'high-cognitive-complexity',
|
|
197
|
+
'context-overuse',
|
|
198
|
+
'state-sync-anti-pattern',
|
|
199
|
+
'complex-untestable',
|
|
200
|
+
];
|
|
201
|
+
if (lowEffort.includes(smellType))
|
|
202
|
+
return 'low';
|
|
203
|
+
if (highEffort.includes(smellType))
|
|
204
|
+
return 'high';
|
|
205
|
+
return 'medium';
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Generate a summary report with AI insights
|
|
209
|
+
*/
|
|
210
|
+
export async function generateAIAnalysisReport(result, config) {
|
|
211
|
+
if (!config.apiKey) {
|
|
212
|
+
return 'AI analysis not available: No API key provided';
|
|
213
|
+
}
|
|
214
|
+
const smellsSummary = Object.entries(result.summary.smellsByType)
|
|
215
|
+
.filter(([, count]) => count > 0)
|
|
216
|
+
.map(([type, count]) => `- ${type}: ${count}`)
|
|
217
|
+
.join('\n');
|
|
218
|
+
const prompt = `Analyze this React codebase analysis result and provide insights:
|
|
219
|
+
|
|
220
|
+
## Summary
|
|
221
|
+
- Total Files: ${result.summary.totalFiles}
|
|
222
|
+
- Total Components: ${result.summary.totalComponents}
|
|
223
|
+
- Total Smells: ${result.summary.totalSmells}
|
|
224
|
+
- Technical Debt Grade: ${result.debtScore.grade}
|
|
225
|
+
- Debt Score: ${result.debtScore.score}/100
|
|
226
|
+
|
|
227
|
+
## Smells by Type
|
|
228
|
+
${smellsSummary}
|
|
229
|
+
|
|
230
|
+
## Severity Distribution
|
|
231
|
+
- Errors: ${result.summary.smellsBySeverity.error}
|
|
232
|
+
- Warnings: ${result.summary.smellsBySeverity.warning}
|
|
233
|
+
- Info: ${result.summary.smellsBySeverity.info}
|
|
234
|
+
|
|
235
|
+
Provide:
|
|
236
|
+
1. Key insights about the codebase health
|
|
237
|
+
2. Top 3 priority areas to address
|
|
238
|
+
3. Estimated effort to improve the grade
|
|
239
|
+
4. Actionable recommendations
|
|
240
|
+
|
|
241
|
+
Keep the response concise and actionable.`;
|
|
242
|
+
try {
|
|
243
|
+
const response = await callLLMAPI(prompt, config);
|
|
244
|
+
if (response?.explanation) {
|
|
245
|
+
return response.explanation;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.error('AI analysis report failed:', error);
|
|
250
|
+
}
|
|
251
|
+
return 'AI analysis could not be generated';
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get refactoring suggestions for specific smell types
|
|
255
|
+
*/
|
|
256
|
+
export function getQuickRefactoringTemplates(smellType) {
|
|
257
|
+
const templates = {
|
|
258
|
+
'useEffect-overuse': [
|
|
259
|
+
'Consider combining related effects into a single useEffect',
|
|
260
|
+
'Extract complex effect logic into custom hooks',
|
|
261
|
+
'Use useReducer for complex state logic instead of multiple effects',
|
|
262
|
+
],
|
|
263
|
+
'prop-drilling': [
|
|
264
|
+
'Use React Context for deeply shared state',
|
|
265
|
+
'Consider component composition (children pattern)',
|
|
266
|
+
'Extract a custom hook for related props',
|
|
267
|
+
],
|
|
268
|
+
'large-component': [
|
|
269
|
+
'Extract logical sections into separate components',
|
|
270
|
+
'Move hooks to custom hook files',
|
|
271
|
+
'Use component composition patterns',
|
|
272
|
+
],
|
|
273
|
+
'context-overuse': [
|
|
274
|
+
'Split context into smaller, focused contexts',
|
|
275
|
+
'Use component composition instead of context',
|
|
276
|
+
'Consider using a state management library for complex needs',
|
|
277
|
+
],
|
|
278
|
+
'missing-error-boundary': [
|
|
279
|
+
'Wrap async components with ErrorBoundary',
|
|
280
|
+
'Add Suspense wrappers for lazy-loaded components',
|
|
281
|
+
'Use react-error-boundary library for consistent handling',
|
|
282
|
+
],
|
|
283
|
+
'state-sync-anti-pattern': [
|
|
284
|
+
'Derive values during render instead of syncing in effects',
|
|
285
|
+
'Use a key prop to reset component state',
|
|
286
|
+
'Consider useMemo for computed values',
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
return templates[smellType] || ['Review the code smell suggestion for improvement ideas'];
|
|
290
|
+
}
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAiCA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA8DtF;AA0SD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/analyzer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parseFile } from './parser/index.js';
|
|
4
|
-
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, detectUnusedCode, } from './detectors/index.js';
|
|
4
|
+
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, detectUnusedCode, detectServerComponentIssues, detectAsyncComponentIssues, } from './detectors/index.js';
|
|
5
5
|
import { parseCustomRules, detectCustomRuleViolations } from './customRules.js';
|
|
6
6
|
import { buildDependencyGraph } from './graphGenerator.js';
|
|
7
7
|
import { analyzeBundleImpact } from './bundleAnalyzer.js';
|
|
@@ -100,6 +100,9 @@ function analyzeFile(parseResult, filePath, config) {
|
|
|
100
100
|
smells.push(...detectMemoryLeaks(component, filePath, sourceCode, config));
|
|
101
101
|
smells.push(...detectImportIssues(component, filePath, sourceCode, config));
|
|
102
102
|
smells.push(...detectUnusedCode(component, filePath, sourceCode, config));
|
|
103
|
+
// Server Components (React 19)
|
|
104
|
+
smells.push(...detectServerComponentIssues(component, filePath, sourceCode, config, imports));
|
|
105
|
+
smells.push(...detectAsyncComponentIssues(component, filePath, sourceCode, config));
|
|
103
106
|
// Custom rules
|
|
104
107
|
const customRules = parseCustomRules(config);
|
|
105
108
|
if (customRules.length > 0) {
|
|
@@ -213,6 +216,35 @@ function calculateSummary(files) {
|
|
|
213
216
|
// Unused code
|
|
214
217
|
'unused-export': 0,
|
|
215
218
|
'dead-import': 0,
|
|
219
|
+
// Server Components (React 19)
|
|
220
|
+
'server-component-hooks': 0,
|
|
221
|
+
'server-component-events': 0,
|
|
222
|
+
'server-component-browser-api': 0,
|
|
223
|
+
'async-client-component': 0,
|
|
224
|
+
'mixed-directives': 0,
|
|
225
|
+
// Context API issues
|
|
226
|
+
'context-overuse': 0,
|
|
227
|
+
'context-in-loop': 0,
|
|
228
|
+
'missing-context-memo': 0,
|
|
229
|
+
'large-context-value': 0,
|
|
230
|
+
// Error Boundary issues
|
|
231
|
+
'missing-error-boundary': 0,
|
|
232
|
+
'error-boundary-missing-fallback': 0,
|
|
233
|
+
'suspense-missing-fallback': 0,
|
|
234
|
+
// Form validation issues
|
|
235
|
+
'uncontrolled-form': 0,
|
|
236
|
+
'missing-form-validation': 0,
|
|
237
|
+
'form-without-onsubmit': 0,
|
|
238
|
+
'input-without-label': 0,
|
|
239
|
+
// State management issues
|
|
240
|
+
'redux-in-render': 0,
|
|
241
|
+
'excessive-redux-selectors': 0,
|
|
242
|
+
'state-sync-anti-pattern': 0,
|
|
243
|
+
'derived-state-in-state': 0,
|
|
244
|
+
// Testing gaps
|
|
245
|
+
'complex-untestable': 0,
|
|
246
|
+
'side-effect-heavy': 0,
|
|
247
|
+
'tightly-coupled': 0,
|
|
216
248
|
// Custom rules
|
|
217
249
|
'custom-rule': 0,
|
|
218
250
|
};
|