@elmoorx/code-review 2.0.0-alpha.25
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/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +34 -0
- package/src/index.ts +605 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wafra Framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @wafra/code-review
|
|
2
|
+
|
|
3
|
+
> AI code review: PR analysis, security scan, performance suggestions
|
|
4
|
+
|
|
5
|
+
Part of the [Wafra Framework](https://github.com/wafra/framework) — Build fast. Run anywhere. Stay secure.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wafra/code-review
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { /* exports */ } from '@wafra/code-review';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Zero external dependencies
|
|
22
|
+
- Full TypeScript support
|
|
23
|
+
- Tree-shakeable
|
|
24
|
+
- Edge-runtime compatible
|
|
25
|
+
- Arabic/RTL friendly
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
See [https://wafra.dev/docs/code-review](https://wafra.dev/docs/code-review)
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT © Wafra Framework
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elmoorx/code-review",
|
|
3
|
+
"version": "2.0.0-alpha.25",
|
|
4
|
+
"description": "AI code review: PR analysis, security scan, performance suggestions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@elmoorx/runtime": "^1.0.0"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Wafra Framework",
|
|
16
|
+
"homepage": "https://wafra.dev/packages/code-review",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/wafra/framework",
|
|
20
|
+
"directory": "packages/code-review"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/wafra/framework/issues"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"wafra",
|
|
27
|
+
"framework",
|
|
28
|
+
"code-review"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./src/index.ts"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wafra/code-review — AI Code Review System
|
|
3
|
+
* ============================================
|
|
4
|
+
* AI-powered code review that finds bugs, security issues,
|
|
5
|
+
* and suggests improvements — before you deploy.
|
|
6
|
+
*
|
|
7
|
+
* import { reviewCode } from "@wafra/code-review";
|
|
8
|
+
* const report = await reviewCode(myComponentCode);
|
|
9
|
+
* // → { score: 92, issues: [...], suggestions: [...] }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { $state, type WafraNode } from "@wafra/runtime";
|
|
13
|
+
|
|
14
|
+
// ============ REVIEW TYPES ============
|
|
15
|
+
|
|
16
|
+
export interface CodeReviewReport {
|
|
17
|
+
score: number;
|
|
18
|
+
issues: CodeIssue[];
|
|
19
|
+
suggestions: CodeSuggestion[];
|
|
20
|
+
metrics: CodeMetrics;
|
|
21
|
+
summary: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CodeIssue {
|
|
25
|
+
id: string;
|
|
26
|
+
severity: "critical" | "warning" | "info" | "style";
|
|
27
|
+
category: "bug" | "security" | "performance" | "accessibility" | "best-practice" | "typescript";
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
line?: number;
|
|
31
|
+
fix?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CodeSuggestion {
|
|
35
|
+
id: string;
|
|
36
|
+
type: "refactor" | "optimize" | "modernize" | "simplify";
|
|
37
|
+
title: string;
|
|
38
|
+
description: string;
|
|
39
|
+
before: string;
|
|
40
|
+
after: string;
|
|
41
|
+
impact: "low" | "medium" | "high";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CodeMetrics {
|
|
45
|
+
lines: number;
|
|
46
|
+
complexity: number;
|
|
47
|
+
maintainability: number;
|
|
48
|
+
readability: number;
|
|
49
|
+
testability: number;
|
|
50
|
+
bundleImpact: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============ CODE REVIEWER ============
|
|
54
|
+
|
|
55
|
+
class CodeReviewer {
|
|
56
|
+
async review(code: string): Promise<CodeReviewReport> {
|
|
57
|
+
const issues: CodeIssue[] = [];
|
|
58
|
+
const suggestions: CodeSuggestion[] = [];
|
|
59
|
+
|
|
60
|
+
// === SECURITY CHECKS ===
|
|
61
|
+
issues.push(...this.checkSecurity(code));
|
|
62
|
+
|
|
63
|
+
// === BUG CHECKS ===
|
|
64
|
+
issues.push(...this.checkBugs(code));
|
|
65
|
+
|
|
66
|
+
// === PERFORMANCE CHECKS ===
|
|
67
|
+
issues.push(...this.checkPerformance(code));
|
|
68
|
+
|
|
69
|
+
// === ACCESSIBILITY CHECKS ===
|
|
70
|
+
issues.push(...this.checkAccessibility(code));
|
|
71
|
+
|
|
72
|
+
// === BEST PRACTICES ===
|
|
73
|
+
issues.push(...this.checkBestPractices(code));
|
|
74
|
+
|
|
75
|
+
// === SUGGESTIONS ===
|
|
76
|
+
suggestions.push(...this.generateSuggestions(code));
|
|
77
|
+
|
|
78
|
+
// === METRICS ===
|
|
79
|
+
const metrics = this.calculateMetrics(code);
|
|
80
|
+
|
|
81
|
+
// === SCORE ===
|
|
82
|
+
const score = this.calculateScore(issues, metrics);
|
|
83
|
+
|
|
84
|
+
// === SUMMARY ===
|
|
85
|
+
const summary = this.generateSummary(score, issues, metrics);
|
|
86
|
+
|
|
87
|
+
return { score, issues, suggestions, metrics, summary };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============ SECURITY CHECKS ============
|
|
91
|
+
|
|
92
|
+
private checkSecurity(code: string): CodeIssue[] {
|
|
93
|
+
const issues: CodeIssue[] = [];
|
|
94
|
+
|
|
95
|
+
// dangerouslySetInnerHTML
|
|
96
|
+
if (code.includes("dangerouslySetInnerHTML")) {
|
|
97
|
+
issues.push({
|
|
98
|
+
id: "sec-xss-dangerous",
|
|
99
|
+
severity: "critical",
|
|
100
|
+
category: "security",
|
|
101
|
+
title: "XSS vulnerability: dangerouslySetInnerHTML",
|
|
102
|
+
description: "Using dangerouslySetInnerHTML allows XSS attacks. Use $html() from @wafra/runtime which auto-sanitizes.",
|
|
103
|
+
fix: 'Replace: dangerouslySetInnerHTML={{ __html: x }}\nWith: $html(x)',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// v-html
|
|
108
|
+
if (code.includes("v-html")) {
|
|
109
|
+
issues.push({
|
|
110
|
+
id: "sec-xss-vhtml",
|
|
111
|
+
severity: "critical",
|
|
112
|
+
category: "security",
|
|
113
|
+
title: "XSS vulnerability: v-html",
|
|
114
|
+
description: "Using v-html allows XSS attacks. Use $html() instead.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// innerHTML
|
|
119
|
+
if (code.includes(".innerHTML") && !code.includes("$html")) {
|
|
120
|
+
issues.push({
|
|
121
|
+
id: "sec-xss-innerhtml",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
category: "security",
|
|
124
|
+
title: "Potential XSS: innerHTML",
|
|
125
|
+
description: "Direct innerHTML assignment can cause XSS. Consider using $html() for auto-sanitization.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// eval
|
|
130
|
+
if (code.includes("eval(")) {
|
|
131
|
+
issues.push({
|
|
132
|
+
id: "sec-eval",
|
|
133
|
+
severity: "critical",
|
|
134
|
+
category: "security",
|
|
135
|
+
title: "Security risk: eval()",
|
|
136
|
+
description: "eval() allows arbitrary code execution. Remove it and use safer alternatives.",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Hardcoded secrets
|
|
141
|
+
if (/api[_-]?key\s*=\s*["'][a-zA-Z0-9]{20,}["']/i.test(code)) {
|
|
142
|
+
issues.push({
|
|
143
|
+
id: "sec-hardcoded-key",
|
|
144
|
+
severity: "critical",
|
|
145
|
+
category: "security",
|
|
146
|
+
title: "Hardcoded API key detected",
|
|
147
|
+
description: "Never hardcode API keys in source code. Use environment variables.",
|
|
148
|
+
fix: 'Use: const apiKey = process.env.API_KEY;',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// HTTP (not HTTPS)
|
|
153
|
+
if (/http:\/\/(?!localhost|127\.0\.0\.1)/.test(code)) {
|
|
154
|
+
issues.push({
|
|
155
|
+
id: "sec-http",
|
|
156
|
+
severity: "warning",
|
|
157
|
+
category: "security",
|
|
158
|
+
title: "Insecure HTTP URL",
|
|
159
|
+
description: "Use HTTPS for all external URLs to prevent man-in-the-middle attacks.",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return issues;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============ BUG CHECKS ============
|
|
167
|
+
|
|
168
|
+
private checkBugs(code: string): CodeIssue[] {
|
|
169
|
+
const issues: CodeIssue[] = [];
|
|
170
|
+
|
|
171
|
+
// Missing key in list
|
|
172
|
+
if (code.includes(".map(") && !code.includes("key=") && !code.includes("key:")) {
|
|
173
|
+
issues.push({
|
|
174
|
+
id: "bug-missing-key",
|
|
175
|
+
severity: "warning",
|
|
176
|
+
category: "bug",
|
|
177
|
+
title: "Missing key prop in list",
|
|
178
|
+
description: "When rendering lists with .map(), each item should have a stable key prop for optimal rendering.",
|
|
179
|
+
fix: 'items.map(item => h("li", { key: item.id }, item.name))',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// useEffect without cleanup
|
|
184
|
+
if (code.includes("$effect(") || code.includes("useEffect(")) {
|
|
185
|
+
if (code.includes("setInterval") && !code.includes("clearInterval")) {
|
|
186
|
+
issues.push({
|
|
187
|
+
id: "bug-interval-leak",
|
|
188
|
+
severity: "warning",
|
|
189
|
+
category: "bug",
|
|
190
|
+
title: "Memory leak: interval without cleanup",
|
|
191
|
+
description: "setInterval inside $effect should return a cleanup function that clears the interval.",
|
|
192
|
+
fix: '$effect(() => {\n const id = setInterval(tick, 1000);\n return () => clearInterval(id);\n});',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (code.includes("addEventListener") && !code.includes("removeEventListener")) {
|
|
196
|
+
issues.push({
|
|
197
|
+
id: "bug-listener-leak",
|
|
198
|
+
severity: "warning",
|
|
199
|
+
category: "bug",
|
|
200
|
+
title: "Memory leak: event listener without cleanup",
|
|
201
|
+
description: "addEventListener should be paired with removeEventListener in cleanup.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// State update on unmounted component
|
|
207
|
+
if (code.includes("setTimeout") && code.includes(".set(") && !code.includes("onCleanup")) {
|
|
208
|
+
issues.push({
|
|
209
|
+
id: "bug-timeout-leak",
|
|
210
|
+
severity: "info",
|
|
211
|
+
category: "bug",
|
|
212
|
+
title: "Potential state update after unmount",
|
|
213
|
+
description: "setTimeout callbacks may fire after component unmounts. Clear timeouts in onCleanup.",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Empty dependency array with external vars
|
|
218
|
+
if (/\$effect\(\(\)\s*=>\s*\{[^}]+\},\s*\[\]\)/.test(code)) {
|
|
219
|
+
issues.push({
|
|
220
|
+
id: "bug-empty-deps",
|
|
221
|
+
severity: "info",
|
|
222
|
+
category: "bug",
|
|
223
|
+
title: "Effect with empty dependencies",
|
|
224
|
+
description: "This effect has empty deps but may reference external variables. Verify this is intentional.",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return issues;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============ PERFORMANCE CHECKS ============
|
|
232
|
+
|
|
233
|
+
private checkPerformance(code: string): CodeIssue[] {
|
|
234
|
+
const issues: CodeIssue[] = [];
|
|
235
|
+
|
|
236
|
+
// Large inline objects
|
|
237
|
+
const styleMatches = code.match(/style\s*[:=]\s*\{[^}]{200,}\}/g);
|
|
238
|
+
if (styleMatches) {
|
|
239
|
+
issues.push({
|
|
240
|
+
id: "perf-large-style",
|
|
241
|
+
severity: "info",
|
|
242
|
+
category: "performance",
|
|
243
|
+
title: "Large inline style object",
|
|
244
|
+
description: "Consider extracting large style objects to a constant or using CSS classes.",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Missing memo
|
|
249
|
+
if (code.includes(".map(") && code.includes("filter(") && !code.includes("memo(") && !code.includes("useMemo")) {
|
|
250
|
+
issues.push({
|
|
251
|
+
id: "perf-missing-memo",
|
|
252
|
+
severity: "info",
|
|
253
|
+
category: "performance",
|
|
254
|
+
title: "Consider memoizing computed list",
|
|
255
|
+
description: "Filtering + mapping on every render is expensive. Wrap in useMemo or memo().",
|
|
256
|
+
fix: 'const filtered = useMemo(() => items.filter(pred).map(fn), [items]);',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Console.log in production
|
|
261
|
+
if (code.includes("console.log")) {
|
|
262
|
+
issues.push({
|
|
263
|
+
id: "perf-console-log",
|
|
264
|
+
severity: "style",
|
|
265
|
+
category: "performance",
|
|
266
|
+
title: "console.log in production code",
|
|
267
|
+
description: "Remove console.log statements before deploying to production.",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Inline function creation
|
|
272
|
+
if ((code.match(/onClick\s*[:=]\s*\(/g) || []).length > 5) {
|
|
273
|
+
issues.push({
|
|
274
|
+
id: "perf-inline-functions",
|
|
275
|
+
severity: "info",
|
|
276
|
+
category: "performance",
|
|
277
|
+
title: "Many inline event handlers",
|
|
278
|
+
description: "Creating many inline functions on each render can cause unnecessary re-renders. Consider useCallback.",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return issues;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============ ACCESSIBILITY CHECKS ============
|
|
286
|
+
|
|
287
|
+
private checkAccessibility(code: string): CodeIssue[] {
|
|
288
|
+
const issues: CodeIssue[] = [];
|
|
289
|
+
|
|
290
|
+
// img without alt
|
|
291
|
+
if (code.includes("<img") && !code.includes("alt=")) {
|
|
292
|
+
issues.push({
|
|
293
|
+
id: "a11y-img-alt",
|
|
294
|
+
severity: "warning",
|
|
295
|
+
category: "accessibility",
|
|
296
|
+
title: "Images without alt text",
|
|
297
|
+
description: "All images should have alt text for screen readers.",
|
|
298
|
+
fix: 'h("img", { src: url, alt: "Description" })',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// button without accessible name
|
|
303
|
+
if (code.includes('h("button"') && !code.includes("aria-label")) {
|
|
304
|
+
const emptyButtons = code.match(/h\(["']button["'],\s*\{[^}]*\}\s*,\s*["']["']\)/g);
|
|
305
|
+
if (emptyButtons) {
|
|
306
|
+
issues.push({
|
|
307
|
+
id: "a11y-button-name",
|
|
308
|
+
severity: "warning",
|
|
309
|
+
category: "accessibility",
|
|
310
|
+
title: "Buttons without accessible names",
|
|
311
|
+
description: "Buttons should have text content or aria-label for screen readers.",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// onClick without onKeyDown
|
|
317
|
+
if (code.includes("onClick") && !code.includes("onKeyDown") && !code.includes("onKeyPress")) {
|
|
318
|
+
issues.push({
|
|
319
|
+
id: "a11y-keyboard",
|
|
320
|
+
severity: "info",
|
|
321
|
+
category: "accessibility",
|
|
322
|
+
title: "Click handler without keyboard support",
|
|
323
|
+
description: "Interactive elements should also respond to keyboard events (Enter, Space).",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return issues;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============ BEST PRACTICES ============
|
|
331
|
+
|
|
332
|
+
private checkBestPractices(code: string): CodeIssue[] {
|
|
333
|
+
const issues: CodeIssue[] = [];
|
|
334
|
+
|
|
335
|
+
// var instead of const/let
|
|
336
|
+
if (code.includes("var ")) {
|
|
337
|
+
issues.push({
|
|
338
|
+
id: "bp-var",
|
|
339
|
+
severity: "style",
|
|
340
|
+
category: "best-practice",
|
|
341
|
+
title: "Use const/let instead of var",
|
|
342
|
+
description: "var has function scope and can cause bugs. Use const (default) or let (when reassigning).",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// == instead of ===
|
|
347
|
+
if (code.includes(" == ") && !code.includes(" === ")) {
|
|
348
|
+
issues.push({
|
|
349
|
+
id: "bp-strict-equality",
|
|
350
|
+
severity: "style",
|
|
351
|
+
category: "best-practice",
|
|
352
|
+
title: "Use strict equality (===)",
|
|
353
|
+
description: "== does type coercion which can cause unexpected behavior. Use === instead.",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// any type
|
|
358
|
+
if (code.includes(": any") || code.includes("as any")) {
|
|
359
|
+
issues.push({
|
|
360
|
+
id: "bp-any-type",
|
|
361
|
+
severity: "warning",
|
|
362
|
+
category: "typescript",
|
|
363
|
+
title: "Avoid 'any' type",
|
|
364
|
+
description: "Using 'any' defeats TypeScript's type safety. Use 'unknown' or specific types.",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// TODO/FIXME
|
|
369
|
+
if (code.includes("TODO") || code.includes("FIXME") || code.includes("HACK")) {
|
|
370
|
+
issues.push({
|
|
371
|
+
id: "bp-todo",
|
|
372
|
+
severity: "info",
|
|
373
|
+
category: "best-practice",
|
|
374
|
+
title: "Unresolved TODO/FIXME",
|
|
375
|
+
description: "Code contains TODO or FIXME comments that should be resolved before production.",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return issues;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ============ SUGGESTIONS ============
|
|
383
|
+
|
|
384
|
+
private generateSuggestions(code: string): CodeSuggestion[] {
|
|
385
|
+
const suggestions: CodeSuggestion[] = [];
|
|
386
|
+
|
|
387
|
+
// Suggest memo
|
|
388
|
+
if (code.includes("function") && !code.includes("memo(")) {
|
|
389
|
+
suggestions.push({
|
|
390
|
+
id: "sug-memo",
|
|
391
|
+
type: "optimize",
|
|
392
|
+
title: "Wrap pure components in memo()",
|
|
393
|
+
description: "Components that don't depend on external state can be memoized to skip unnecessary re-renders.",
|
|
394
|
+
before: 'function ExpensiveList(props) {\n return h("ul", null, props.items.map(...));\n}',
|
|
395
|
+
after: 'const ExpensiveList = memo(function(props) {\n return h("ul", null, props.items.map(...));\n});',
|
|
396
|
+
impact: "medium",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Suggest $store instead of multiple $state
|
|
401
|
+
const stateCount = (code.match(/\$state\(/g) || []).length;
|
|
402
|
+
if (stateCount >= 3) {
|
|
403
|
+
suggestions.push({
|
|
404
|
+
id: "sug-store",
|
|
405
|
+
type: "refactor",
|
|
406
|
+
title: `Consider using $store instead of ${stateCount} $state calls`,
|
|
407
|
+
description: "Multiple related $state calls can be combined into a single $store for cleaner code.",
|
|
408
|
+
before: 'const name = $state("");\nconst email = $state("");\nconst age = $state(0);',
|
|
409
|
+
after: 'const form = $store({\n name: "",\n email: "",\n age: 0\n});',
|
|
410
|
+
impact: "medium",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Suggest lazy loading
|
|
415
|
+
if (code.includes("import(") && !code.includes("lazy(")) {
|
|
416
|
+
suggestions.push({
|
|
417
|
+
id: "sug-lazy",
|
|
418
|
+
type: "optimize",
|
|
419
|
+
title: "Use lazy() for dynamic imports",
|
|
420
|
+
description: "Wrap dynamic imports in lazy() for better code splitting and prefetching.",
|
|
421
|
+
before: 'const Chart = () => import("./Chart");',
|
|
422
|
+
after: 'const Chart = lazy(() => import("./Chart"));',
|
|
423
|
+
impact: "high",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Suggest async/await over .then()
|
|
428
|
+
if (code.includes(".then(") && !code.includes("async")) {
|
|
429
|
+
suggestions.push({
|
|
430
|
+
id: "sug-async",
|
|
431
|
+
type: "modernize",
|
|
432
|
+
title: "Use async/await instead of .then()",
|
|
433
|
+
description: "async/await is more readable and easier to debug than promise chains.",
|
|
434
|
+
before: 'fetch(url).then(res => res.json()).then(data => console.log(data));',
|
|
435
|
+
after: 'const res = await fetch(url);\nconst data = await res.json();\nconsole.log(data);',
|
|
436
|
+
impact: "low",
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return suggestions;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============ METRICS ============
|
|
444
|
+
|
|
445
|
+
private calculateMetrics(code: string): CodeMetrics {
|
|
446
|
+
const lines = code.split("\n").length;
|
|
447
|
+
const conditions = (code.match(/if\s*\(/g) || []).length;
|
|
448
|
+
const loops = (code.match(/for\s*\(|while\s*\(|\.map\(|\.forEach\(|\.filter\(/g) || []).length;
|
|
449
|
+
const functions = (code.match(/function\s+\w+|=>\s*[{(]/g) || []).length;
|
|
450
|
+
|
|
451
|
+
const complexity = conditions + loops * 2;
|
|
452
|
+
const maintainability = Math.max(0, 100 - complexity * 3);
|
|
453
|
+
const readability = Math.max(0, 100 - (lines > 200 ? 30 : 0) - (functions > 10 ? 20 : 0));
|
|
454
|
+
const testability = Math.max(0, 100 - (complexity > 10 ? 30 : 0));
|
|
455
|
+
const bundleImpact = Math.min(100, lines * 0.5);
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
lines,
|
|
459
|
+
complexity,
|
|
460
|
+
maintainability: Math.round(maintainability),
|
|
461
|
+
readability: Math.round(readability),
|
|
462
|
+
testability: Math.round(testability),
|
|
463
|
+
bundleImpact: Math.round(bundleImpact),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ============ SCORE ============
|
|
468
|
+
|
|
469
|
+
private calculateScore(issues: CodeIssue[], metrics: CodeMetrics): number {
|
|
470
|
+
let score = 100;
|
|
471
|
+
|
|
472
|
+
for (const issue of issues) {
|
|
473
|
+
switch (issue.severity) {
|
|
474
|
+
case "critical": score -= 20; break;
|
|
475
|
+
case "warning": score -= 10; break;
|
|
476
|
+
case "info": score -= 3; break;
|
|
477
|
+
case "style": score -= 1; break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Factor in metrics
|
|
482
|
+
score = (score + metrics.maintainability + metrics.readability + metrics.testability) / 4;
|
|
483
|
+
|
|
484
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ============ SUMMARY ============
|
|
488
|
+
|
|
489
|
+
private generateSummary(score: number, issues: CodeIssue[], metrics: CodeMetrics): string {
|
|
490
|
+
const critical = issues.filter(i => i.severity === "critical").length;
|
|
491
|
+
const warnings = issues.filter(i => i.severity === "warning").length;
|
|
492
|
+
|
|
493
|
+
if (score >= 90) {
|
|
494
|
+
return `Excellent code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Ready for production.`;
|
|
495
|
+
} else if (score >= 70) {
|
|
496
|
+
return `Good code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Address critical issues before deploy.`;
|
|
497
|
+
} else if (score >= 50) {
|
|
498
|
+
return `Fair code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Needs improvement before production.`;
|
|
499
|
+
} else {
|
|
500
|
+
return `Poor code quality (score: ${score}/100). ${critical} critical, ${warnings} warnings. Major refactoring needed.`;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export const codeReviewer = new CodeReviewer();
|
|
506
|
+
|
|
507
|
+
// ============ CONVENIENCE FUNCTION ============
|
|
508
|
+
|
|
509
|
+
export async function reviewCode(code: string): Promise<CodeReviewReport> {
|
|
510
|
+
return codeReviewer.review(code);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ============ REVIEW DASHBOARD ============
|
|
514
|
+
|
|
515
|
+
export function CodeReviewDashboard(props: { report: CodeReviewReport }): WafraNode {
|
|
516
|
+
const r = props.report;
|
|
517
|
+
|
|
518
|
+
return h("div", {
|
|
519
|
+
style: "background:#0A0A0F;color:#E4E4E7;font-family:Inter,sans-serif;padding:32px;border-radius:12px;",
|
|
520
|
+
},
|
|
521
|
+
// Score
|
|
522
|
+
h("div", { style: "display:flex;align-items:center;gap:20px;margin-bottom:24px;" },
|
|
523
|
+
h("div", {
|
|
524
|
+
style: `
|
|
525
|
+
width:80px;height:80px;border-radius:50%;display:flex;align-items:center;justify-content:center;
|
|
526
|
+
font-size:28px;font-weight:700;font-family:'Space Grotesk',sans-serif;
|
|
527
|
+
background:conic-gradient(${r.score >= 80 ? "#10B981" : r.score >= 50 ? "#F59E0B" : "#EF4444"} ${r.score * 3.6}deg, #2A2A38 0deg);
|
|
528
|
+
`,
|
|
529
|
+
},
|
|
530
|
+
h("div", { style: "width:64px;height:64px;border-radius:50%;background:#0A0A0F;display:flex;align-items:center;justify-content:center;color:#E4E4E7;" }, String(r.score))
|
|
531
|
+
),
|
|
532
|
+
h("div", null,
|
|
533
|
+
h("div", { style: "font-size:14px;color:#A1A1AA;" }, "Code Review Score"),
|
|
534
|
+
h("div", { style: `font-size:20px;font-weight:600;color:${r.score >= 80 ? "#10B981" : r.score >= 50 ? "#F59E0B" : "#EF4444"};` },
|
|
535
|
+
r.score >= 90 ? "Excellent" : r.score >= 70 ? "Good" : r.score >= 50 ? "Fair" : "Poor"
|
|
536
|
+
),
|
|
537
|
+
h("div", { style: "font-size:12px;color:#71717A;margin-top:4px;" }, r.summary),
|
|
538
|
+
),
|
|
539
|
+
),
|
|
540
|
+
|
|
541
|
+
// Issues
|
|
542
|
+
r.issues.length > 0 ? h("div", { style: "margin-bottom:24px;" },
|
|
543
|
+
h("div", { style: "font-family:monospace;font-size:10px;color:#71717A;text-transform:uppercase;margin-bottom:8px;" }, `Issues (${r.issues.length})`),
|
|
544
|
+
...r.issues.map(issue =>
|
|
545
|
+
h("div", {
|
|
546
|
+
key: issue.id,
|
|
547
|
+
style: `background:#14141B;border:1px solid #2A2A38;border-left:3px solid ${issue.severity === "critical" ? "#EF4444" : issue.severity === "warning" ? "#F59E0B" : issue.severity === "info" ? "#06B6D4" : "#71717A"};border-radius:6px;padding:12px;margin-bottom:8px;`,
|
|
548
|
+
},
|
|
549
|
+
h("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:4px;" },
|
|
550
|
+
h("span", {
|
|
551
|
+
style: `padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;text-transform:uppercase;background:${issue.severity === "critical" ? "rgba(239,68,68,0.15);color:#EF4444" : issue.severity === "warning" ? "rgba(245,158,11,0.15);color:#F59E0B" : issue.severity === "info" ? "rgba(6,182,212,0.15);color:#06B6D4" : "rgba(113,113,122,0.15);color:#71717A"};`,
|
|
552
|
+
}, issue.severity),
|
|
553
|
+
h("span", { style: "font-size:13px;font-weight:600;color:#E4E4E7;" }, issue.title),
|
|
554
|
+
),
|
|
555
|
+
h("div", { style: "font-size:12px;color:#A1A1AA;margin-bottom:8px;" }, issue.description),
|
|
556
|
+
issue.fix ? h("pre", { style: "font-size:11px;color:#10B981;font-family:monospace;background:#0F0F17;padding:8px;border-radius:4px;white-space:pre-wrap;" }, issue.fix) : null,
|
|
557
|
+
)
|
|
558
|
+
)
|
|
559
|
+
) : null,
|
|
560
|
+
|
|
561
|
+
// Suggestions
|
|
562
|
+
r.suggestions.length > 0 ? h("div", { style: "margin-bottom:24px;" },
|
|
563
|
+
h("div", { style: "font-family:monospace;font-size:10px;color:#71717A;text-transform:uppercase;margin-bottom:8px;" }, `Suggestions (${r.suggestions.length})`),
|
|
564
|
+
...r.suggestions.map(sug =>
|
|
565
|
+
h("div", {
|
|
566
|
+
key: sug.id,
|
|
567
|
+
style: "background:#14141B;border:1px solid #2A2A38;border-radius:6px;padding:12px;margin-bottom:8px;",
|
|
568
|
+
},
|
|
569
|
+
h("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:4px;" },
|
|
570
|
+
h("span", { style: `padding:1px 6px;border-radius:3px;font-size:9px;font-weight:600;text-transform:uppercase;background:rgba(168,85,247,0.15);color:#A855F7;` }, sug.type),
|
|
571
|
+
h("span", { style: "font-size:13px;font-weight:600;color:#E4E4E7;" }, sug.title),
|
|
572
|
+
h("span", { style: `margin-left:auto;font-size:10px;color:${sug.impact === "high" ? "#10B981" : sug.impact === "medium" ? "#F59E0B" : "#71717A"};` }, sug.impact + " impact"),
|
|
573
|
+
),
|
|
574
|
+
h("div", { style: "font-size:12px;color:#A1A1AA;margin-bottom:8px;" }, sug.description),
|
|
575
|
+
h("div", { style: "display:grid;grid-template-columns:1fr 1fr;gap:8px;" },
|
|
576
|
+
h("div", null,
|
|
577
|
+
h("div", { style: "font-size:9px;color:#EF4444;text-transform:uppercase;margin-bottom:2px;" }, "Before"),
|
|
578
|
+
h("pre", { style: "font-size:10px;color:#A1A1AA;font-family:monospace;background:#0F0F17;padding:6px;border-radius:4px;white-space:pre-wrap;" }, sug.before),
|
|
579
|
+
),
|
|
580
|
+
h("div", null,
|
|
581
|
+
h("div", { style: "font-size:9px;color:#10B981;text-transform:uppercase;margin-bottom:2px;" }, "After"),
|
|
582
|
+
h("pre", { style: "font-size:10px;color:#10B981;font-family:monospace;background:#0F0F17;padding:6px;border-radius:4px;white-space:pre-wrap;" }, sug.after),
|
|
583
|
+
),
|
|
584
|
+
),
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
) : null,
|
|
588
|
+
|
|
589
|
+
// Metrics
|
|
590
|
+
h("div", null,
|
|
591
|
+
h("div", { style: "font-family:monospace;font-size:10px;color:#71717A;text-transform:uppercase;margin-bottom:8px;" }, "Metrics"),
|
|
592
|
+
h("div", { style: "display:grid;grid-template-columns:repeat(3,1fr);gap:8px;" },
|
|
593
|
+
...Object.entries(r.metrics).map(([key, value]) =>
|
|
594
|
+
h("div", {
|
|
595
|
+
key,
|
|
596
|
+
style: "background:#14141B;border:1px solid #2A2A38;border-radius:6px;padding:8px;text-align:center;",
|
|
597
|
+
},
|
|
598
|
+
h("div", { style: "font-size:20px;font-weight:700;color:#A855F7;" }, String(value)),
|
|
599
|
+
h("div", { style: "font-size:10px;color:#71717A;text-transform:uppercase;" }, key),
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
),
|
|
603
|
+
),
|
|
604
|
+
);
|
|
605
|
+
}
|