@codebakers/cli 3.9.41 → 3.9.42
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/dist/commands/install-precommit.js +1106 -953
- package/package.json +1 -1
- package/src/commands/install-precommit.ts +1205 -1051
|
@@ -1,1051 +1,1205 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
|
|
5
|
-
const PRE_COMMIT_SCRIPT = `#!/bin/sh
|
|
6
|
-
# CodeBakers Pre-Commit Hook - Comprehensive Code Validation
|
|
7
|
-
# Actually scans code for pattern violations
|
|
8
|
-
|
|
9
|
-
# Run the validation script
|
|
10
|
-
node "$(dirname "$0")/validate-code.js"
|
|
11
|
-
exit $?
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
|
|
15
|
-
/**
|
|
16
|
-
* CodeBakers Pre-Commit Code Validator v2.0
|
|
17
|
-
* Comprehensive code validation - 40+ checks
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const { execSync } = require('child_process');
|
|
21
|
-
const fs = require('fs');
|
|
22
|
-
const path = require('path');
|
|
23
|
-
|
|
24
|
-
const RED = '\\x1b[31m';
|
|
25
|
-
const GREEN = '\\x1b[32m';
|
|
26
|
-
const YELLOW = '\\x1b[33m';
|
|
27
|
-
const CYAN = '\\x1b[36m';
|
|
28
|
-
const DIM = '\\x1b[2m';
|
|
29
|
-
const RESET = '\\x1b[0m';
|
|
30
|
-
|
|
31
|
-
function log(color, message) {
|
|
32
|
-
console.log(color + message + RESET);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Get staged files
|
|
36
|
-
function getStagedFiles() {
|
|
37
|
-
try {
|
|
38
|
-
const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf-8' });
|
|
39
|
-
return output.split('\\n').filter(f => f.trim());
|
|
40
|
-
} catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Get code files only
|
|
46
|
-
function getCodeFiles(files) {
|
|
47
|
-
return files.filter(f =>
|
|
48
|
-
f.endsWith('.ts') || f.endsWith('.tsx') ||
|
|
49
|
-
f.endsWith('.js') || f.endsWith('.jsx')
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ============================================
|
|
54
|
-
// ALL CHECKS - Organized by Category
|
|
55
|
-
// ============================================
|
|
56
|
-
|
|
57
|
-
const CHECKS = [
|
|
58
|
-
// ==========================================
|
|
59
|
-
// SECURITY CHECKS
|
|
60
|
-
// ==========================================
|
|
61
|
-
{
|
|
62
|
-
name: 'Debugger Statement',
|
|
63
|
-
category: 'security',
|
|
64
|
-
test: (content, file) => {
|
|
65
|
-
if (content.includes('debugger;') || content.includes('debugger ')) {
|
|
66
|
-
return 'debugger statement left in code - remove before commit';
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'Hardcoded Secrets',
|
|
73
|
-
category: 'security',
|
|
74
|
-
test: (content, file) => {
|
|
75
|
-
if (file.includes('.env') || file.includes('config')) return null;
|
|
76
|
-
const patterns = [
|
|
77
|
-
/api[_-]?key\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
|
|
78
|
-
/secret\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
|
|
79
|
-
/password\\s*[:=]\\s*['"][^'"]{8,}['"]/i,
|
|
80
|
-
/sk_live_[a-zA-Z0-9]+/,
|
|
81
|
-
/sk_test_[a-zA-Z0-9]+/,
|
|
82
|
-
/ghp_[a-zA-Z0-9]+/, // GitHub token
|
|
83
|
-
/xox[baprs]-[a-zA-Z0-9]+/, // Slack token
|
|
84
|
-
/AKIA[0-9A-Z]{16}/, // AWS access key
|
|
85
|
-
];
|
|
86
|
-
for (const pattern of patterns) {
|
|
87
|
-
if (pattern.test(content)) {
|
|
88
|
-
return 'Possible hardcoded secret - use environment variables';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
name: 'XSS Vulnerability',
|
|
96
|
-
category: 'security',
|
|
97
|
-
test: (content, file) => {
|
|
98
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
99
|
-
if (content.includes('dangerouslySetInnerHTML') &&
|
|
100
|
-
!content.includes('DOMPurify') &&
|
|
101
|
-
!content.includes('sanitize')) {
|
|
102
|
-
return 'dangerouslySetInnerHTML without sanitization - XSS risk';
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: 'Merge Conflict Markers',
|
|
109
|
-
category: 'security',
|
|
110
|
-
test: (content, file) => {
|
|
111
|
-
if (content.includes('<<<<<<<') || content.includes('>>>>>>>') || content.includes('=======\\n')) {
|
|
112
|
-
return 'Merge conflict markers found - resolve conflicts first';
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: 'Private Key in Code',
|
|
119
|
-
category: 'security',
|
|
120
|
-
test: (content, file) => {
|
|
121
|
-
if (content.includes('-----BEGIN RSA PRIVATE KEY-----') ||
|
|
122
|
-
content.includes('-----BEGIN PRIVATE KEY-----') ||
|
|
123
|
-
content.includes('-----BEGIN EC PRIVATE KEY-----')) {
|
|
124
|
-
return 'Private key detected in code - NEVER commit private keys';
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
name: 'Env File Commit',
|
|
131
|
-
category: 'security',
|
|
132
|
-
test: (content, file) => {
|
|
133
|
-
if (file === '.env' || file === '.env.local' || file === '.env.production') {
|
|
134
|
-
return '.env file should not be committed - add to .gitignore';
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
name: 'SQL Injection Risk',
|
|
141
|
-
category: 'security',
|
|
142
|
-
test: (content, file) => {
|
|
143
|
-
const sqlPatterns = [
|
|
144
|
-
/\\$\\{.*\\}.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/i,
|
|
145
|
-
/['"]\\s*\\+\\s*.*\\+\\s*['"].*(?:SELECT|INSERT|UPDATE|DELETE)/i,
|
|
146
|
-
/sql\\s*\\(\\s*\`[^\\)]*\\$\\{/,
|
|
147
|
-
];
|
|
148
|
-
for (const pattern of sqlPatterns) {
|
|
149
|
-
if (pattern.test(content)) {
|
|
150
|
-
return 'Possible SQL injection - use parameterized queries';
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
name: 'Eval Usage',
|
|
158
|
-
category: 'security',
|
|
159
|
-
test: (content, file) => {
|
|
160
|
-
if (/\\beval\\s*\\(/.test(content) || /new\\s+Function\\s*\\(/.test(content)) {
|
|
161
|
-
return 'eval() or new Function() detected - security risk';
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
name: 'Sensitive Data in Logs',
|
|
168
|
-
category: 'security',
|
|
169
|
-
test: (content, file) => {
|
|
170
|
-
const sensitivePatterns = [
|
|
171
|
-
/console\\.log.*password/i,
|
|
172
|
-
/console\\.log.*token/i,
|
|
173
|
-
/console\\.log.*secret/i,
|
|
174
|
-
/console\\.log.*apiKey/i,
|
|
175
|
-
/console\\.log.*creditCard/i,
|
|
176
|
-
/console\\.log.*ssn/i,
|
|
177
|
-
/console\\.log.*authorization/i,
|
|
178
|
-
];
|
|
179
|
-
for (const pattern of sensitivePatterns) {
|
|
180
|
-
if (pattern.test(content)) {
|
|
181
|
-
return 'Possible sensitive data being logged';
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
// ==========================================
|
|
189
|
-
// ERROR HANDLING CHECKS
|
|
190
|
-
// ==========================================
|
|
191
|
-
{
|
|
192
|
-
name: 'API Error Handling',
|
|
193
|
-
category: 'errors',
|
|
194
|
-
test: (content, file) => {
|
|
195
|
-
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
196
|
-
if (!content.includes('try {') && !content.includes('try{')) {
|
|
197
|
-
return 'API route missing try/catch error handling';
|
|
198
|
-
}
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
name: 'Empty Catch Block',
|
|
204
|
-
category: 'errors',
|
|
205
|
-
test: (content, file) => {
|
|
206
|
-
if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}/.test(content)) {
|
|
207
|
-
return 'Empty catch block - handle or rethrow errors';
|
|
208
|
-
}
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
name: 'Catch Without Logging',
|
|
214
|
-
category: 'errors',
|
|
215
|
-
test: (content, file) => {
|
|
216
|
-
const catchBlocks = content.match(/catch\\s*\\([^)]*\\)\\s*\\{[^}]{1,50}\\}/g) || [];
|
|
217
|
-
for (const block of catchBlocks) {
|
|
218
|
-
if (!block.includes('console') && !block.includes('log') &&
|
|
219
|
-
!block.includes('throw') && !block.includes('error') &&
|
|
220
|
-
!block.includes('report') && !block.includes('track')) {
|
|
221
|
-
return 'Catch block may be swallowing errors - log or rethrow';
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: 'Unsafe JSON Parse',
|
|
229
|
-
category: 'errors',
|
|
230
|
-
test: (content, file) => {
|
|
231
|
-
if (content.includes('JSON.parse(') &&
|
|
232
|
-
!content.includes('try') && !content.includes('catch')) {
|
|
233
|
-
return 'JSON.parse without try/catch - can throw on invalid JSON';
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
name: 'Unhandled Promise',
|
|
240
|
-
category: 'errors',
|
|
241
|
-
test: (content, file) => {
|
|
242
|
-
const lines = content.split('\\n');
|
|
243
|
-
for (let i = 0; i < lines.length; i++) {
|
|
244
|
-
const line = lines[i].trim();
|
|
245
|
-
if (line.match(/(?:fetch|axios|db\\.|prisma\\.).*\\(/) &&
|
|
246
|
-
!line.includes('await') &&
|
|
247
|
-
!line.includes('.then') &&
|
|
248
|
-
!line.includes('.catch') &&
|
|
249
|
-
!line.includes('return') &&
|
|
250
|
-
!line.includes('=')) {
|
|
251
|
-
return \`Unhandled promise at line \${i + 1}\`;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
name: 'Missing Async Error Handling',
|
|
259
|
-
category: 'errors',
|
|
260
|
-
test: (content, file) => {
|
|
261
|
-
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
262
|
-
if (content.includes('async') && content.includes('await') &&
|
|
263
|
-
!content.includes('try') && !content.includes('.catch')) {
|
|
264
|
-
return 'Async function with await but no error handling';
|
|
265
|
-
}
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
|
|
270
|
-
// ==========================================
|
|
271
|
-
// VALIDATION CHECKS
|
|
272
|
-
// ==========================================
|
|
273
|
-
{
|
|
274
|
-
name: 'Zod Validation',
|
|
275
|
-
category: 'validation',
|
|
276
|
-
test: (content, file) => {
|
|
277
|
-
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
278
|
-
if ((content.includes('POST') || content.includes('PUT') || content.includes('PATCH')) &&
|
|
279
|
-
content.includes('req.json()') &&
|
|
280
|
-
!content.includes('z.object') &&
|
|
281
|
-
!content.includes('schema.parse') &&
|
|
282
|
-
!content.includes('Schema.parse') &&
|
|
283
|
-
!content.includes('validate')) {
|
|
284
|
-
return 'API route accepts body but missing Zod validation';
|
|
285
|
-
}
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: 'Missing Auth Check',
|
|
291
|
-
category: 'validation',
|
|
292
|
-
test: (content, file) => {
|
|
293
|
-
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
294
|
-
if (file.includes('/public/') || file.includes('/auth/') || file.includes('/webhook')) return null;
|
|
295
|
-
if ((content.includes('userId') || content.includes('user.id') || content.includes('session')) &&
|
|
296
|
-
!content.includes('getServerSession') &&
|
|
297
|
-
!content.includes('auth(') &&
|
|
298
|
-
!content.includes('requireAuth') &&
|
|
299
|
-
!content.includes('verifyToken') &&
|
|
300
|
-
!content.includes('validateSession') &&
|
|
301
|
-
!content.includes('getSession')) {
|
|
302
|
-
return 'Route accesses user data but may be missing auth check';
|
|
303
|
-
}
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
// ==========================================
|
|
309
|
-
// CODE QUALITY CHECKS
|
|
310
|
-
// ==========================================
|
|
311
|
-
{
|
|
312
|
-
name: 'Console Statements',
|
|
313
|
-
category: 'quality',
|
|
314
|
-
test: (content, file) => {
|
|
315
|
-
if (file.includes('.test.') || file.includes('/tests/') || file.includes('/scripts/')) return null;
|
|
316
|
-
const lines = content.split('\\n');
|
|
317
|
-
for (let i = 0; i < lines.length; i++) {
|
|
318
|
-
const line = lines[i];
|
|
319
|
-
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
|
320
|
-
if (line.includes('console.log(')) {
|
|
321
|
-
return \`console.log at line \${i + 1} - use proper logging\`;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
name: 'TODO/FIXME Comments',
|
|
329
|
-
category: 'quality',
|
|
330
|
-
test: (content, file) => {
|
|
331
|
-
const match = content.match(/\\/\\/\\s*(TODO|FIXME|XXX|HACK):/i);
|
|
332
|
-
if (match) {
|
|
333
|
-
return \`Unresolved \${match[1]} comment - address before commit\`;
|
|
334
|
-
}
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
name: 'Hardcoded URLs',
|
|
340
|
-
category: 'quality',
|
|
341
|
-
test: (content, file) => {
|
|
342
|
-
if (file.includes('.test.') || file.includes('/tests/')) return null;
|
|
343
|
-
if (content.includes('localhost:') &&
|
|
344
|
-
!content.includes('process.env') &&
|
|
345
|
-
!content.includes("|| 'http://localhost") &&
|
|
346
|
-
!content.includes('|| "http://localhost')) {
|
|
347
|
-
return 'Hardcoded localhost URL - use environment variable with fallback';
|
|
348
|
-
}
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
name: 'Large File',
|
|
354
|
-
category: 'quality',
|
|
355
|
-
test: (content, file) => {
|
|
356
|
-
const lines = content.split('\\n').length;
|
|
357
|
-
if (lines > 500) {
|
|
358
|
-
return \`File has \${lines} lines - consider splitting into smaller modules\`;
|
|
359
|
-
}
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
name: 'Magic Numbers',
|
|
365
|
-
category: 'quality',
|
|
366
|
-
test: (content, file) => {
|
|
367
|
-
// Look for unexplained numbers in conditions
|
|
368
|
-
const magicPattern = /(?:if|while|for)\\s*\\([^)]*[^0-9.](\\d{3,})[^0-9.]/;
|
|
369
|
-
const match = content.match(magicPattern);
|
|
370
|
-
if (match && !content.includes('const') && !content.includes('let')) {
|
|
371
|
-
return 'Magic number detected - use named constants';
|
|
372
|
-
}
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
name: 'Commented Out Code',
|
|
378
|
-
category: 'quality',
|
|
379
|
-
test: (content, file) => {
|
|
380
|
-
const lines = content.split('\\n');
|
|
381
|
-
let commentedCodeCount = 0;
|
|
382
|
-
for (const line of lines) {
|
|
383
|
-
if (line.trim().match(/^\\/\\/\\s*(const|let|var|function|if|for|while|return|import|export)\\s/)) {
|
|
384
|
-
commentedCodeCount++;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (commentedCodeCount > 5) {
|
|
388
|
-
return \`\${commentedCodeCount} lines of commented code - remove dead code\`;
|
|
389
|
-
}
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
|
|
394
|
-
// ==========================================
|
|
395
|
-
// TYPESCRIPT CHECKS
|
|
396
|
-
// ==========================================
|
|
397
|
-
{
|
|
398
|
-
name: 'Any Type Usage',
|
|
399
|
-
category: 'typescript',
|
|
400
|
-
test: (content, file) => {
|
|
401
|
-
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
402
|
-
if (content.includes(': any)') || content.includes(': any,') ||
|
|
403
|
-
content.includes(': any;') || content.includes(': any =') ||
|
|
404
|
-
content.includes('<any>') || content.includes('as any')) {
|
|
405
|
-
return 'Using "any" type - provide proper TypeScript types';
|
|
406
|
-
}
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
name: 'Type Assertion Override',
|
|
412
|
-
category: 'typescript',
|
|
413
|
-
test: (content, file) => {
|
|
414
|
-
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
415
|
-
if (content.includes('as unknown as') || content.includes('!.') && content.match(/!\\.[a-zA-Z]/)) {
|
|
416
|
-
return 'Unsafe type assertion - validate types properly';
|
|
417
|
-
}
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
name: 'Missing Return Type',
|
|
423
|
-
category: 'typescript',
|
|
424
|
-
test: (content, file) => {
|
|
425
|
-
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
426
|
-
const exportedFunctions = content.match(/export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{/g) || [];
|
|
427
|
-
for (const func of exportedFunctions) {
|
|
428
|
-
if (!func.includes(':')) {
|
|
429
|
-
return 'Exported function missing return type annotation';
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
name: 'Non-null Assertion',
|
|
437
|
-
category: 'typescript',
|
|
438
|
-
test: (content, file) => {
|
|
439
|
-
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
440
|
-
const assertions = (content.match(/\\w+!/g) || []).filter(m => !m.includes('!='));
|
|
441
|
-
if (assertions.length > 3) {
|
|
442
|
-
return \`\${assertions.length} non-null assertions (!) - handle null cases properly\`;
|
|
443
|
-
}
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
// ==========================================
|
|
449
|
-
// REACT CHECKS
|
|
450
|
-
// ==========================================
|
|
451
|
-
{
|
|
452
|
-
name: 'Direct DOM Manipulation',
|
|
453
|
-
category: 'react',
|
|
454
|
-
test: (content, file) => {
|
|
455
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
456
|
-
if (content.includes('document.getElementById') ||
|
|
457
|
-
content.includes('document.querySelector') ||
|
|
458
|
-
content.includes('document.createElement') ||
|
|
459
|
-
content.includes('document.body')) {
|
|
460
|
-
return 'Direct DOM manipulation in React - use refs or state';
|
|
461
|
-
}
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
name: 'Missing useEffect Cleanup',
|
|
467
|
-
category: 'react',
|
|
468
|
-
test: (content, file) => {
|
|
469
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
470
|
-
// Check for useEffect with subscriptions but no cleanup
|
|
471
|
-
if (content.includes('useEffect') &&
|
|
472
|
-
(content.includes('addEventListener') ||
|
|
473
|
-
content.includes('subscribe') ||
|
|
474
|
-
content.includes('setInterval') ||
|
|
475
|
-
content.includes('setTimeout')) &&
|
|
476
|
-
!content.includes('removeEventListener') &&
|
|
477
|
-
!content.includes('unsubscribe') &&
|
|
478
|
-
!content.includes('clearInterval') &&
|
|
479
|
-
!content.includes('clearTimeout') &&
|
|
480
|
-
!content.includes('return () =>')) {
|
|
481
|
-
return 'useEffect with subscription but no cleanup - memory leak risk';
|
|
482
|
-
}
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
name: 'Conditional Hook',
|
|
488
|
-
category: 'react',
|
|
489
|
-
test: (content, file) => {
|
|
490
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
491
|
-
const lines = content.split('\\n');
|
|
492
|
-
let inCondition = false;
|
|
493
|
-
for (let i = 0; i < lines.length; i++) {
|
|
494
|
-
const line = lines[i].trim();
|
|
495
|
-
if (line.startsWith('if ') || line.startsWith('if(')) inCondition = true;
|
|
496
|
-
if (line.includes('}')) inCondition = false;
|
|
497
|
-
if (inCondition && (line.includes('useState') || line.includes('useEffect') ||
|
|
498
|
-
line.includes('useCallback') || line.includes('useMemo'))) {
|
|
499
|
-
return \`Hook called conditionally at line \${i + 1} - violates Rules of Hooks\`;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
name: 'Missing Key Prop',
|
|
507
|
-
category: 'react',
|
|
508
|
-
test: (content, file) => {
|
|
509
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
510
|
-
if ((content.includes('.map(') || content.includes('.map (')) &&
|
|
511
|
-
content.includes('return') &&
|
|
512
|
-
content.includes('<') &&
|
|
513
|
-
!content.includes('key=') &&
|
|
514
|
-
!content.includes('key:')) {
|
|
515
|
-
return 'Array .map() rendering JSX without key prop';
|
|
516
|
-
}
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
},
|
|
520
|
-
{
|
|
521
|
-
name: 'Index as Key',
|
|
522
|
-
category: 'react',
|
|
523
|
-
test: (content, file) => {
|
|
524
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
525
|
-
if (content.includes('key={i}') || content.includes('key={index}') ||
|
|
526
|
-
content.includes('key={idx}')) {
|
|
527
|
-
return 'Using array index as key - use unique identifier instead';
|
|
528
|
-
}
|
|
529
|
-
return null;
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
name: 'Inline Function in JSX',
|
|
534
|
-
category: 'react',
|
|
535
|
-
test: (content, file) => {
|
|
536
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
537
|
-
// Check for arrow functions in onClick, onChange, etc.
|
|
538
|
-
const inlinePatterns = [
|
|
539
|
-
/onClick=\\{\\s*\\(\\)\\s*=>/,
|
|
540
|
-
/onChange=\\{\\s*\\(e?\\)\\s*=>/,
|
|
541
|
-
/onSubmit=\\{\\s*\\(e?\\)\\s*=>/,
|
|
542
|
-
];
|
|
543
|
-
for (const pattern of inlinePatterns) {
|
|
544
|
-
if (pattern.test(content)) {
|
|
545
|
-
return 'Inline function in JSX - use useCallback for performance';
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return null;
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
{
|
|
552
|
-
name: 'Missing Error Boundary',
|
|
553
|
-
category: 'react',
|
|
554
|
-
test: (content, file) => {
|
|
555
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
556
|
-
// Check if it's a page component without error handling
|
|
557
|
-
if ((file.includes('/app/') || file.includes('/pages/')) &&
|
|
558
|
-
file.includes('page.') &&
|
|
559
|
-
!content.includes('ErrorBoundary') &&
|
|
560
|
-
!content.includes('error.') &&
|
|
561
|
-
content.includes('async')) {
|
|
562
|
-
return 'Page component may need error boundary for async operations';
|
|
563
|
-
}
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
|
|
568
|
-
// ==========================================
|
|
569
|
-
// ACCESSIBILITY CHECKS
|
|
570
|
-
// ==========================================
|
|
571
|
-
{
|
|
572
|
-
name: 'Image Without Alt',
|
|
573
|
-
category: 'a11y',
|
|
574
|
-
test: (content, file) => {
|
|
575
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
576
|
-
if ((content.includes('<img') || content.includes('<Image')) &&
|
|
577
|
-
!content.includes('alt=') && !content.includes('alt:')) {
|
|
578
|
-
return 'Image without alt attribute - add alt text for accessibility';
|
|
579
|
-
}
|
|
580
|
-
return null;
|
|
581
|
-
}
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
name: 'Button Without Type',
|
|
585
|
-
category: 'a11y',
|
|
586
|
-
test: (content, file) => {
|
|
587
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
588
|
-
// Check for buttons without type attribute
|
|
589
|
-
if (content.includes('<button') &&
|
|
590
|
-
!content.includes('type="button"') &&
|
|
591
|
-
!content.includes('type="submit"') &&
|
|
592
|
-
!content.includes("type='button'") &&
|
|
593
|
-
!content.includes("type='submit'") &&
|
|
594
|
-
!content.includes('type={"button"}') &&
|
|
595
|
-
!content.includes('type={"submit"}')) {
|
|
596
|
-
return 'Button without type attribute - specify type="button" or type="submit"';
|
|
597
|
-
}
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
name: 'Missing Form Label',
|
|
603
|
-
category: 'a11y',
|
|
604
|
-
test: (content, file) => {
|
|
605
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
606
|
-
if ((content.includes('<input') || content.includes('<select') || content.includes('<textarea')) &&
|
|
607
|
-
!content.includes('<label') &&
|
|
608
|
-
!content.includes('aria-label') &&
|
|
609
|
-
!content.includes('aria-labelledby') &&
|
|
610
|
-
!content.includes('Label')) {
|
|
611
|
-
return 'Form input without label - add label for accessibility';
|
|
612
|
-
}
|
|
613
|
-
return null;
|
|
614
|
-
}
|
|
615
|
-
},
|
|
616
|
-
{
|
|
617
|
-
name: 'Click Handler Without Keyboard',
|
|
618
|
-
category: 'a11y',
|
|
619
|
-
test: (content, file) => {
|
|
620
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
621
|
-
// Check for divs/spans with onClick but no keyboard handler
|
|
622
|
-
if ((content.includes('<div') || content.includes('<span')) &&
|
|
623
|
-
content.includes('onClick') &&
|
|
624
|
-
!content.includes('onKeyDown') &&
|
|
625
|
-
!content.includes('onKeyPress') &&
|
|
626
|
-
!content.includes('onKeyUp') &&
|
|
627
|
-
!content.includes('role=') &&
|
|
628
|
-
!content.includes('tabIndex')) {
|
|
629
|
-
return 'Clickable element without keyboard support - add onKeyDown and role';
|
|
630
|
-
}
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
name: 'Missing ARIA Role',
|
|
636
|
-
category: 'a11y',
|
|
637
|
-
test: (content, file) => {
|
|
638
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
639
|
-
// Custom interactive elements should have roles
|
|
640
|
-
if (content.includes('onClick') &&
|
|
641
|
-
(content.includes('<div') || content.includes('<span')) &&
|
|
642
|
-
!content.includes('role=') &&
|
|
643
|
-
!content.includes('<button') &&
|
|
644
|
-
!content.includes('<a ')) {
|
|
645
|
-
return 'Interactive element without ARIA role - add appropriate role';
|
|
646
|
-
}
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
},
|
|
650
|
-
|
|
651
|
-
// ==========================================
|
|
652
|
-
// PERFORMANCE CHECKS
|
|
653
|
-
// ==========================================
|
|
654
|
-
{
|
|
655
|
-
name: 'Large Import',
|
|
656
|
-
category: 'performance',
|
|
657
|
-
test: (content, file) => {
|
|
658
|
-
// Check for importing entire libraries
|
|
659
|
-
const largeImports = [
|
|
660
|
-
/import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['"]lodash['"]/,
|
|
661
|
-
/import\\s+\\{[^}]{100,}\\}\\s+from/, // Very large destructured import
|
|
662
|
-
/import\\s+moment\\s+from/, // moment.js is large
|
|
663
|
-
];
|
|
664
|
-
for (const pattern of largeImports) {
|
|
665
|
-
if (pattern.test(content)) {
|
|
666
|
-
return 'Large library import - use specific imports for smaller bundle';
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return null;
|
|
670
|
-
}
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
name: 'Sync File Operation',
|
|
674
|
-
category: 'performance',
|
|
675
|
-
test: (content, file) => {
|
|
676
|
-
if (file.includes('.test.') || file.includes('/scripts/')) return null;
|
|
677
|
-
if (content.includes('readFileSync') || content.includes('writeFileSync') ||
|
|
678
|
-
content.includes('existsSync') || content.includes('readdirSync')) {
|
|
679
|
-
if (file.includes('/api/') || file.includes('route.ts')) {
|
|
680
|
-
return 'Synchronous file operation in API route - use async version';
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
name: 'Missing Memoization',
|
|
688
|
-
category: 'performance',
|
|
689
|
-
test: (content, file) => {
|
|
690
|
-
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
691
|
-
// Large computations in render without useMemo
|
|
692
|
-
if ((content.includes('.filter(') || content.includes('.reduce(') || content.includes('.sort(')) &&
|
|
693
|
-
content.includes('return') &&
|
|
694
|
-
content.includes('<') &&
|
|
695
|
-
!content.includes('useMemo') &&
|
|
696
|
-
content.split('.filter(').length > 2) {
|
|
697
|
-
return 'Multiple array operations in render - consider useMemo';
|
|
698
|
-
}
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
|
|
703
|
-
// ==========================================
|
|
704
|
-
// API/DATABASE CHECKS
|
|
705
|
-
// ==========================================
|
|
706
|
-
{
|
|
707
|
-
name: 'Missing Rate Limit',
|
|
708
|
-
category: 'api',
|
|
709
|
-
test: (content, file) => {
|
|
710
|
-
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
711
|
-
if (file.includes('/public/')) return null;
|
|
712
|
-
if (!content.includes('rateLimit') &&
|
|
713
|
-
!content.includes('rateLimiter') &&
|
|
714
|
-
!content.includes('autoRateLimit') &&
|
|
715
|
-
!content.includes('throttle')) {
|
|
716
|
-
return 'API route without rate limiting - add protection against abuse';
|
|
717
|
-
}
|
|
718
|
-
return null;
|
|
719
|
-
}
|
|
720
|
-
},
|
|
721
|
-
{
|
|
722
|
-
name: 'N+1 Query Pattern',
|
|
723
|
-
category: 'api',
|
|
724
|
-
test: (content, file) => {
|
|
725
|
-
// Check for await in loop with database calls
|
|
726
|
-
if ((content.includes('for (') || content.includes('forEach') || content.includes('.map(')) &&
|
|
727
|
-
content.includes('await') &&
|
|
728
|
-
(content.includes('db.') || content.includes('prisma.') || content.includes('findOne') || content.includes('findById'))) {
|
|
729
|
-
return 'Possible N+1 query - fetch data in batch instead of loop';
|
|
730
|
-
}
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
},
|
|
734
|
-
{
|
|
735
|
-
name: 'Missing CORS Config',
|
|
736
|
-
category: 'api',
|
|
737
|
-
test: (content, file) => {
|
|
738
|
-
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
739
|
-
if (content.includes("'*'") &&
|
|
740
|
-
(content.includes('Access-Control-Allow-Origin') || content.includes('cors'))) {
|
|
741
|
-
return 'Overly permissive CORS (*) - restrict to specific origins';
|
|
742
|
-
}
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
},
|
|
746
|
-
|
|
747
|
-
// ==========================================
|
|
748
|
-
// IMPORT CHECKS
|
|
749
|
-
// ==========================================
|
|
750
|
-
{
|
|
751
|
-
name: 'Circular Import Risk',
|
|
752
|
-
category: 'imports',
|
|
753
|
-
test: (content, file) => {
|
|
754
|
-
// Check for importing from parent directory and exporting to child
|
|
755
|
-
const parentImports = (content.match(/from\\s+['"]\\.\\.\\/[^'"]+['"]/g) || []).length;
|
|
756
|
-
const hasExport = content.includes('export ');
|
|
757
|
-
if (parentImports > 3 && hasExport) {
|
|
758
|
-
return 'Multiple parent imports - potential circular dependency risk';
|
|
759
|
-
}
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
name: 'Unused Import',
|
|
765
|
-
category: 'imports',
|
|
766
|
-
test: (content, file) => {
|
|
767
|
-
// Simple check for imported names not used
|
|
768
|
-
const imports = content.match(/import\\s+\\{([^}]+)\\}/g) || [];
|
|
769
|
-
for (const imp of imports) {
|
|
770
|
-
const names = imp.replace(/import\\s+\\{/, '').replace(/\\}/, '').split(',');
|
|
771
|
-
for (const name of names) {
|
|
772
|
-
const cleanName = name.trim().split(' as ')[0].trim();
|
|
773
|
-
if (cleanName && cleanName.length > 1) {
|
|
774
|
-
// Count occurrences (should be > 1 to include the import itself)
|
|
775
|
-
const regex = new RegExp('\\\\b' + cleanName + '\\\\b', 'g');
|
|
776
|
-
const occurrences = (content.match(regex) || []).length;
|
|
777
|
-
if (occurrences === 1) {
|
|
778
|
-
return \`Possibly unused import: \${cleanName}\`;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
return null;
|
|
784
|
-
}
|
|
785
|
-
},
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
async
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const cwd = process.cwd();
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
//
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const PRE_COMMIT_SCRIPT = `#!/bin/sh
|
|
6
|
+
# CodeBakers Pre-Commit Hook - Comprehensive Code Validation
|
|
7
|
+
# Actually scans code for pattern violations
|
|
8
|
+
|
|
9
|
+
# Run the validation script
|
|
10
|
+
node "$(dirname "$0")/validate-code.js"
|
|
11
|
+
exit $?
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
const VALIDATE_CODE_SCRIPT = `#!/usr/bin/env node
|
|
15
|
+
/**
|
|
16
|
+
* CodeBakers Pre-Commit Code Validator v2.0
|
|
17
|
+
* Comprehensive code validation - 40+ checks
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
const RED = '\\x1b[31m';
|
|
25
|
+
const GREEN = '\\x1b[32m';
|
|
26
|
+
const YELLOW = '\\x1b[33m';
|
|
27
|
+
const CYAN = '\\x1b[36m';
|
|
28
|
+
const DIM = '\\x1b[2m';
|
|
29
|
+
const RESET = '\\x1b[0m';
|
|
30
|
+
|
|
31
|
+
function log(color, message) {
|
|
32
|
+
console.log(color + message + RESET);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get staged files
|
|
36
|
+
function getStagedFiles() {
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf-8' });
|
|
39
|
+
return output.split('\\n').filter(f => f.trim());
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get code files only
|
|
46
|
+
function getCodeFiles(files) {
|
|
47
|
+
return files.filter(f =>
|
|
48
|
+
f.endsWith('.ts') || f.endsWith('.tsx') ||
|
|
49
|
+
f.endsWith('.js') || f.endsWith('.jsx')
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// ALL CHECKS - Organized by Category
|
|
55
|
+
// ============================================
|
|
56
|
+
|
|
57
|
+
const CHECKS = [
|
|
58
|
+
// ==========================================
|
|
59
|
+
// SECURITY CHECKS
|
|
60
|
+
// ==========================================
|
|
61
|
+
{
|
|
62
|
+
name: 'Debugger Statement',
|
|
63
|
+
category: 'security',
|
|
64
|
+
test: (content, file) => {
|
|
65
|
+
if (content.includes('debugger;') || content.includes('debugger ')) {
|
|
66
|
+
return 'debugger statement left in code - remove before commit';
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Hardcoded Secrets',
|
|
73
|
+
category: 'security',
|
|
74
|
+
test: (content, file) => {
|
|
75
|
+
if (file.includes('.env') || file.includes('config')) return null;
|
|
76
|
+
const patterns = [
|
|
77
|
+
/api[_-]?key\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
|
|
78
|
+
/secret\\s*[:=]\\s*['"][a-zA-Z0-9]{20,}['"]/i,
|
|
79
|
+
/password\\s*[:=]\\s*['"][^'"]{8,}['"]/i,
|
|
80
|
+
/sk_live_[a-zA-Z0-9]+/,
|
|
81
|
+
/sk_test_[a-zA-Z0-9]+/,
|
|
82
|
+
/ghp_[a-zA-Z0-9]+/, // GitHub token
|
|
83
|
+
/xox[baprs]-[a-zA-Z0-9]+/, // Slack token
|
|
84
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key
|
|
85
|
+
];
|
|
86
|
+
for (const pattern of patterns) {
|
|
87
|
+
if (pattern.test(content)) {
|
|
88
|
+
return 'Possible hardcoded secret - use environment variables';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'XSS Vulnerability',
|
|
96
|
+
category: 'security',
|
|
97
|
+
test: (content, file) => {
|
|
98
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
99
|
+
if (content.includes('dangerouslySetInnerHTML') &&
|
|
100
|
+
!content.includes('DOMPurify') &&
|
|
101
|
+
!content.includes('sanitize')) {
|
|
102
|
+
return 'dangerouslySetInnerHTML without sanitization - XSS risk';
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'Merge Conflict Markers',
|
|
109
|
+
category: 'security',
|
|
110
|
+
test: (content, file) => {
|
|
111
|
+
if (content.includes('<<<<<<<') || content.includes('>>>>>>>') || content.includes('=======\\n')) {
|
|
112
|
+
return 'Merge conflict markers found - resolve conflicts first';
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'Private Key in Code',
|
|
119
|
+
category: 'security',
|
|
120
|
+
test: (content, file) => {
|
|
121
|
+
if (content.includes('-----BEGIN RSA PRIVATE KEY-----') ||
|
|
122
|
+
content.includes('-----BEGIN PRIVATE KEY-----') ||
|
|
123
|
+
content.includes('-----BEGIN EC PRIVATE KEY-----')) {
|
|
124
|
+
return 'Private key detected in code - NEVER commit private keys';
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Env File Commit',
|
|
131
|
+
category: 'security',
|
|
132
|
+
test: (content, file) => {
|
|
133
|
+
if (file === '.env' || file === '.env.local' || file === '.env.production') {
|
|
134
|
+
return '.env file should not be committed - add to .gitignore';
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'SQL Injection Risk',
|
|
141
|
+
category: 'security',
|
|
142
|
+
test: (content, file) => {
|
|
143
|
+
const sqlPatterns = [
|
|
144
|
+
/\\$\\{.*\\}.*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)/i,
|
|
145
|
+
/['"]\\s*\\+\\s*.*\\+\\s*['"].*(?:SELECT|INSERT|UPDATE|DELETE)/i,
|
|
146
|
+
/sql\\s*\\(\\s*\`[^\\)]*\\$\\{/,
|
|
147
|
+
];
|
|
148
|
+
for (const pattern of sqlPatterns) {
|
|
149
|
+
if (pattern.test(content)) {
|
|
150
|
+
return 'Possible SQL injection - use parameterized queries';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'Eval Usage',
|
|
158
|
+
category: 'security',
|
|
159
|
+
test: (content, file) => {
|
|
160
|
+
if (/\\beval\\s*\\(/.test(content) || /new\\s+Function\\s*\\(/.test(content)) {
|
|
161
|
+
return 'eval() or new Function() detected - security risk';
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Sensitive Data in Logs',
|
|
168
|
+
category: 'security',
|
|
169
|
+
test: (content, file) => {
|
|
170
|
+
const sensitivePatterns = [
|
|
171
|
+
/console\\.log.*password/i,
|
|
172
|
+
/console\\.log.*token/i,
|
|
173
|
+
/console\\.log.*secret/i,
|
|
174
|
+
/console\\.log.*apiKey/i,
|
|
175
|
+
/console\\.log.*creditCard/i,
|
|
176
|
+
/console\\.log.*ssn/i,
|
|
177
|
+
/console\\.log.*authorization/i,
|
|
178
|
+
];
|
|
179
|
+
for (const pattern of sensitivePatterns) {
|
|
180
|
+
if (pattern.test(content)) {
|
|
181
|
+
return 'Possible sensitive data being logged';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// ==========================================
|
|
189
|
+
// ERROR HANDLING CHECKS
|
|
190
|
+
// ==========================================
|
|
191
|
+
{
|
|
192
|
+
name: 'API Error Handling',
|
|
193
|
+
category: 'errors',
|
|
194
|
+
test: (content, file) => {
|
|
195
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
196
|
+
if (!content.includes('try {') && !content.includes('try{')) {
|
|
197
|
+
return 'API route missing try/catch error handling';
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'Empty Catch Block',
|
|
204
|
+
category: 'errors',
|
|
205
|
+
test: (content, file) => {
|
|
206
|
+
if (/catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}/.test(content)) {
|
|
207
|
+
return 'Empty catch block - handle or rethrow errors';
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'Catch Without Logging',
|
|
214
|
+
category: 'errors',
|
|
215
|
+
test: (content, file) => {
|
|
216
|
+
const catchBlocks = content.match(/catch\\s*\\([^)]*\\)\\s*\\{[^}]{1,50}\\}/g) || [];
|
|
217
|
+
for (const block of catchBlocks) {
|
|
218
|
+
if (!block.includes('console') && !block.includes('log') &&
|
|
219
|
+
!block.includes('throw') && !block.includes('error') &&
|
|
220
|
+
!block.includes('report') && !block.includes('track')) {
|
|
221
|
+
return 'Catch block may be swallowing errors - log or rethrow';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'Unsafe JSON Parse',
|
|
229
|
+
category: 'errors',
|
|
230
|
+
test: (content, file) => {
|
|
231
|
+
if (content.includes('JSON.parse(') &&
|
|
232
|
+
!content.includes('try') && !content.includes('catch')) {
|
|
233
|
+
return 'JSON.parse without try/catch - can throw on invalid JSON';
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'Unhandled Promise',
|
|
240
|
+
category: 'errors',
|
|
241
|
+
test: (content, file) => {
|
|
242
|
+
const lines = content.split('\\n');
|
|
243
|
+
for (let i = 0; i < lines.length; i++) {
|
|
244
|
+
const line = lines[i].trim();
|
|
245
|
+
if (line.match(/(?:fetch|axios|db\\.|prisma\\.).*\\(/) &&
|
|
246
|
+
!line.includes('await') &&
|
|
247
|
+
!line.includes('.then') &&
|
|
248
|
+
!line.includes('.catch') &&
|
|
249
|
+
!line.includes('return') &&
|
|
250
|
+
!line.includes('=')) {
|
|
251
|
+
return \`Unhandled promise at line \${i + 1}\`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'Missing Async Error Handling',
|
|
259
|
+
category: 'errors',
|
|
260
|
+
test: (content, file) => {
|
|
261
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
262
|
+
if (content.includes('async') && content.includes('await') &&
|
|
263
|
+
!content.includes('try') && !content.includes('.catch')) {
|
|
264
|
+
return 'Async function with await but no error handling';
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
// ==========================================
|
|
271
|
+
// VALIDATION CHECKS
|
|
272
|
+
// ==========================================
|
|
273
|
+
{
|
|
274
|
+
name: 'Zod Validation',
|
|
275
|
+
category: 'validation',
|
|
276
|
+
test: (content, file) => {
|
|
277
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
278
|
+
if ((content.includes('POST') || content.includes('PUT') || content.includes('PATCH')) &&
|
|
279
|
+
content.includes('req.json()') &&
|
|
280
|
+
!content.includes('z.object') &&
|
|
281
|
+
!content.includes('schema.parse') &&
|
|
282
|
+
!content.includes('Schema.parse') &&
|
|
283
|
+
!content.includes('validate')) {
|
|
284
|
+
return 'API route accepts body but missing Zod validation';
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'Missing Auth Check',
|
|
291
|
+
category: 'validation',
|
|
292
|
+
test: (content, file) => {
|
|
293
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
294
|
+
if (file.includes('/public/') || file.includes('/auth/') || file.includes('/webhook')) return null;
|
|
295
|
+
if ((content.includes('userId') || content.includes('user.id') || content.includes('session')) &&
|
|
296
|
+
!content.includes('getServerSession') &&
|
|
297
|
+
!content.includes('auth(') &&
|
|
298
|
+
!content.includes('requireAuth') &&
|
|
299
|
+
!content.includes('verifyToken') &&
|
|
300
|
+
!content.includes('validateSession') &&
|
|
301
|
+
!content.includes('getSession')) {
|
|
302
|
+
return 'Route accesses user data but may be missing auth check';
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// ==========================================
|
|
309
|
+
// CODE QUALITY CHECKS
|
|
310
|
+
// ==========================================
|
|
311
|
+
{
|
|
312
|
+
name: 'Console Statements',
|
|
313
|
+
category: 'quality',
|
|
314
|
+
test: (content, file) => {
|
|
315
|
+
if (file.includes('.test.') || file.includes('/tests/') || file.includes('/scripts/')) return null;
|
|
316
|
+
const lines = content.split('\\n');
|
|
317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
318
|
+
const line = lines[i];
|
|
319
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
|
|
320
|
+
if (line.includes('console.log(')) {
|
|
321
|
+
return \`console.log at line \${i + 1} - use proper logging\`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'TODO/FIXME Comments',
|
|
329
|
+
category: 'quality',
|
|
330
|
+
test: (content, file) => {
|
|
331
|
+
const match = content.match(/\\/\\/\\s*(TODO|FIXME|XXX|HACK):/i);
|
|
332
|
+
if (match) {
|
|
333
|
+
return \`Unresolved \${match[1]} comment - address before commit\`;
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'Hardcoded URLs',
|
|
340
|
+
category: 'quality',
|
|
341
|
+
test: (content, file) => {
|
|
342
|
+
if (file.includes('.test.') || file.includes('/tests/')) return null;
|
|
343
|
+
if (content.includes('localhost:') &&
|
|
344
|
+
!content.includes('process.env') &&
|
|
345
|
+
!content.includes("|| 'http://localhost") &&
|
|
346
|
+
!content.includes('|| "http://localhost')) {
|
|
347
|
+
return 'Hardcoded localhost URL - use environment variable with fallback';
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'Large File',
|
|
354
|
+
category: 'quality',
|
|
355
|
+
test: (content, file) => {
|
|
356
|
+
const lines = content.split('\\n').length;
|
|
357
|
+
if (lines > 500) {
|
|
358
|
+
return \`File has \${lines} lines - consider splitting into smaller modules\`;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'Magic Numbers',
|
|
365
|
+
category: 'quality',
|
|
366
|
+
test: (content, file) => {
|
|
367
|
+
// Look for unexplained numbers in conditions
|
|
368
|
+
const magicPattern = /(?:if|while|for)\\s*\\([^)]*[^0-9.](\\d{3,})[^0-9.]/;
|
|
369
|
+
const match = content.match(magicPattern);
|
|
370
|
+
if (match && !content.includes('const') && !content.includes('let')) {
|
|
371
|
+
return 'Magic number detected - use named constants';
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'Commented Out Code',
|
|
378
|
+
category: 'quality',
|
|
379
|
+
test: (content, file) => {
|
|
380
|
+
const lines = content.split('\\n');
|
|
381
|
+
let commentedCodeCount = 0;
|
|
382
|
+
for (const line of lines) {
|
|
383
|
+
if (line.trim().match(/^\\/\\/\\s*(const|let|var|function|if|for|while|return|import|export)\\s/)) {
|
|
384
|
+
commentedCodeCount++;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (commentedCodeCount > 5) {
|
|
388
|
+
return \`\${commentedCodeCount} lines of commented code - remove dead code\`;
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// ==========================================
|
|
395
|
+
// TYPESCRIPT CHECKS
|
|
396
|
+
// ==========================================
|
|
397
|
+
{
|
|
398
|
+
name: 'Any Type Usage',
|
|
399
|
+
category: 'typescript',
|
|
400
|
+
test: (content, file) => {
|
|
401
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
402
|
+
if (content.includes(': any)') || content.includes(': any,') ||
|
|
403
|
+
content.includes(': any;') || content.includes(': any =') ||
|
|
404
|
+
content.includes('<any>') || content.includes('as any')) {
|
|
405
|
+
return 'Using "any" type - provide proper TypeScript types';
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: 'Type Assertion Override',
|
|
412
|
+
category: 'typescript',
|
|
413
|
+
test: (content, file) => {
|
|
414
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
415
|
+
if (content.includes('as unknown as') || content.includes('!.') && content.match(/!\\.[a-zA-Z]/)) {
|
|
416
|
+
return 'Unsafe type assertion - validate types properly';
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'Missing Return Type',
|
|
423
|
+
category: 'typescript',
|
|
424
|
+
test: (content, file) => {
|
|
425
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
426
|
+
const exportedFunctions = content.match(/export\\s+(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{/g) || [];
|
|
427
|
+
for (const func of exportedFunctions) {
|
|
428
|
+
if (!func.includes(':')) {
|
|
429
|
+
return 'Exported function missing return type annotation';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'Non-null Assertion',
|
|
437
|
+
category: 'typescript',
|
|
438
|
+
test: (content, file) => {
|
|
439
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
440
|
+
const assertions = (content.match(/\\w+!/g) || []).filter(m => !m.includes('!='));
|
|
441
|
+
if (assertions.length > 3) {
|
|
442
|
+
return \`\${assertions.length} non-null assertions (!) - handle null cases properly\`;
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
// ==========================================
|
|
449
|
+
// REACT CHECKS
|
|
450
|
+
// ==========================================
|
|
451
|
+
{
|
|
452
|
+
name: 'Direct DOM Manipulation',
|
|
453
|
+
category: 'react',
|
|
454
|
+
test: (content, file) => {
|
|
455
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
456
|
+
if (content.includes('document.getElementById') ||
|
|
457
|
+
content.includes('document.querySelector') ||
|
|
458
|
+
content.includes('document.createElement') ||
|
|
459
|
+
content.includes('document.body')) {
|
|
460
|
+
return 'Direct DOM manipulation in React - use refs or state';
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: 'Missing useEffect Cleanup',
|
|
467
|
+
category: 'react',
|
|
468
|
+
test: (content, file) => {
|
|
469
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
470
|
+
// Check for useEffect with subscriptions but no cleanup
|
|
471
|
+
if (content.includes('useEffect') &&
|
|
472
|
+
(content.includes('addEventListener') ||
|
|
473
|
+
content.includes('subscribe') ||
|
|
474
|
+
content.includes('setInterval') ||
|
|
475
|
+
content.includes('setTimeout')) &&
|
|
476
|
+
!content.includes('removeEventListener') &&
|
|
477
|
+
!content.includes('unsubscribe') &&
|
|
478
|
+
!content.includes('clearInterval') &&
|
|
479
|
+
!content.includes('clearTimeout') &&
|
|
480
|
+
!content.includes('return () =>')) {
|
|
481
|
+
return 'useEffect with subscription but no cleanup - memory leak risk';
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: 'Conditional Hook',
|
|
488
|
+
category: 'react',
|
|
489
|
+
test: (content, file) => {
|
|
490
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
491
|
+
const lines = content.split('\\n');
|
|
492
|
+
let inCondition = false;
|
|
493
|
+
for (let i = 0; i < lines.length; i++) {
|
|
494
|
+
const line = lines[i].trim();
|
|
495
|
+
if (line.startsWith('if ') || line.startsWith('if(')) inCondition = true;
|
|
496
|
+
if (line.includes('}')) inCondition = false;
|
|
497
|
+
if (inCondition && (line.includes('useState') || line.includes('useEffect') ||
|
|
498
|
+
line.includes('useCallback') || line.includes('useMemo'))) {
|
|
499
|
+
return \`Hook called conditionally at line \${i + 1} - violates Rules of Hooks\`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'Missing Key Prop',
|
|
507
|
+
category: 'react',
|
|
508
|
+
test: (content, file) => {
|
|
509
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
510
|
+
if ((content.includes('.map(') || content.includes('.map (')) &&
|
|
511
|
+
content.includes('return') &&
|
|
512
|
+
content.includes('<') &&
|
|
513
|
+
!content.includes('key=') &&
|
|
514
|
+
!content.includes('key:')) {
|
|
515
|
+
return 'Array .map() rendering JSX without key prop';
|
|
516
|
+
}
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'Index as Key',
|
|
522
|
+
category: 'react',
|
|
523
|
+
test: (content, file) => {
|
|
524
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
525
|
+
if (content.includes('key={i}') || content.includes('key={index}') ||
|
|
526
|
+
content.includes('key={idx}')) {
|
|
527
|
+
return 'Using array index as key - use unique identifier instead';
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'Inline Function in JSX',
|
|
534
|
+
category: 'react',
|
|
535
|
+
test: (content, file) => {
|
|
536
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
537
|
+
// Check for arrow functions in onClick, onChange, etc.
|
|
538
|
+
const inlinePatterns = [
|
|
539
|
+
/onClick=\\{\\s*\\(\\)\\s*=>/,
|
|
540
|
+
/onChange=\\{\\s*\\(e?\\)\\s*=>/,
|
|
541
|
+
/onSubmit=\\{\\s*\\(e?\\)\\s*=>/,
|
|
542
|
+
];
|
|
543
|
+
for (const pattern of inlinePatterns) {
|
|
544
|
+
if (pattern.test(content)) {
|
|
545
|
+
return 'Inline function in JSX - use useCallback for performance';
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: 'Missing Error Boundary',
|
|
553
|
+
category: 'react',
|
|
554
|
+
test: (content, file) => {
|
|
555
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
556
|
+
// Check if it's a page component without error handling
|
|
557
|
+
if ((file.includes('/app/') || file.includes('/pages/')) &&
|
|
558
|
+
file.includes('page.') &&
|
|
559
|
+
!content.includes('ErrorBoundary') &&
|
|
560
|
+
!content.includes('error.') &&
|
|
561
|
+
content.includes('async')) {
|
|
562
|
+
return 'Page component may need error boundary for async operations';
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
// ==========================================
|
|
569
|
+
// ACCESSIBILITY CHECKS
|
|
570
|
+
// ==========================================
|
|
571
|
+
{
|
|
572
|
+
name: 'Image Without Alt',
|
|
573
|
+
category: 'a11y',
|
|
574
|
+
test: (content, file) => {
|
|
575
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
576
|
+
if ((content.includes('<img') || content.includes('<Image')) &&
|
|
577
|
+
!content.includes('alt=') && !content.includes('alt:')) {
|
|
578
|
+
return 'Image without alt attribute - add alt text for accessibility';
|
|
579
|
+
}
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: 'Button Without Type',
|
|
585
|
+
category: 'a11y',
|
|
586
|
+
test: (content, file) => {
|
|
587
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
588
|
+
// Check for buttons without type attribute
|
|
589
|
+
if (content.includes('<button') &&
|
|
590
|
+
!content.includes('type="button"') &&
|
|
591
|
+
!content.includes('type="submit"') &&
|
|
592
|
+
!content.includes("type='button'") &&
|
|
593
|
+
!content.includes("type='submit'") &&
|
|
594
|
+
!content.includes('type={"button"}') &&
|
|
595
|
+
!content.includes('type={"submit"}')) {
|
|
596
|
+
return 'Button without type attribute - specify type="button" or type="submit"';
|
|
597
|
+
}
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'Missing Form Label',
|
|
603
|
+
category: 'a11y',
|
|
604
|
+
test: (content, file) => {
|
|
605
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
606
|
+
if ((content.includes('<input') || content.includes('<select') || content.includes('<textarea')) &&
|
|
607
|
+
!content.includes('<label') &&
|
|
608
|
+
!content.includes('aria-label') &&
|
|
609
|
+
!content.includes('aria-labelledby') &&
|
|
610
|
+
!content.includes('Label')) {
|
|
611
|
+
return 'Form input without label - add label for accessibility';
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: 'Click Handler Without Keyboard',
|
|
618
|
+
category: 'a11y',
|
|
619
|
+
test: (content, file) => {
|
|
620
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
621
|
+
// Check for divs/spans with onClick but no keyboard handler
|
|
622
|
+
if ((content.includes('<div') || content.includes('<span')) &&
|
|
623
|
+
content.includes('onClick') &&
|
|
624
|
+
!content.includes('onKeyDown') &&
|
|
625
|
+
!content.includes('onKeyPress') &&
|
|
626
|
+
!content.includes('onKeyUp') &&
|
|
627
|
+
!content.includes('role=') &&
|
|
628
|
+
!content.includes('tabIndex')) {
|
|
629
|
+
return 'Clickable element without keyboard support - add onKeyDown and role';
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'Missing ARIA Role',
|
|
636
|
+
category: 'a11y',
|
|
637
|
+
test: (content, file) => {
|
|
638
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
639
|
+
// Custom interactive elements should have roles
|
|
640
|
+
if (content.includes('onClick') &&
|
|
641
|
+
(content.includes('<div') || content.includes('<span')) &&
|
|
642
|
+
!content.includes('role=') &&
|
|
643
|
+
!content.includes('<button') &&
|
|
644
|
+
!content.includes('<a ')) {
|
|
645
|
+
return 'Interactive element without ARIA role - add appropriate role';
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
// ==========================================
|
|
652
|
+
// PERFORMANCE CHECKS
|
|
653
|
+
// ==========================================
|
|
654
|
+
{
|
|
655
|
+
name: 'Large Import',
|
|
656
|
+
category: 'performance',
|
|
657
|
+
test: (content, file) => {
|
|
658
|
+
// Check for importing entire libraries
|
|
659
|
+
const largeImports = [
|
|
660
|
+
/import\\s+\\*\\s+as\\s+\\w+\\s+from\\s+['"]lodash['"]/,
|
|
661
|
+
/import\\s+\\{[^}]{100,}\\}\\s+from/, // Very large destructured import
|
|
662
|
+
/import\\s+moment\\s+from/, // moment.js is large
|
|
663
|
+
];
|
|
664
|
+
for (const pattern of largeImports) {
|
|
665
|
+
if (pattern.test(content)) {
|
|
666
|
+
return 'Large library import - use specific imports for smaller bundle';
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: 'Sync File Operation',
|
|
674
|
+
category: 'performance',
|
|
675
|
+
test: (content, file) => {
|
|
676
|
+
if (file.includes('.test.') || file.includes('/scripts/')) return null;
|
|
677
|
+
if (content.includes('readFileSync') || content.includes('writeFileSync') ||
|
|
678
|
+
content.includes('existsSync') || content.includes('readdirSync')) {
|
|
679
|
+
if (file.includes('/api/') || file.includes('route.ts')) {
|
|
680
|
+
return 'Synchronous file operation in API route - use async version';
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: 'Missing Memoization',
|
|
688
|
+
category: 'performance',
|
|
689
|
+
test: (content, file) => {
|
|
690
|
+
if (!file.endsWith('.tsx') && !file.endsWith('.jsx')) return null;
|
|
691
|
+
// Large computations in render without useMemo
|
|
692
|
+
if ((content.includes('.filter(') || content.includes('.reduce(') || content.includes('.sort(')) &&
|
|
693
|
+
content.includes('return') &&
|
|
694
|
+
content.includes('<') &&
|
|
695
|
+
!content.includes('useMemo') &&
|
|
696
|
+
content.split('.filter(').length > 2) {
|
|
697
|
+
return 'Multiple array operations in render - consider useMemo';
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
// ==========================================
|
|
704
|
+
// API/DATABASE CHECKS
|
|
705
|
+
// ==========================================
|
|
706
|
+
{
|
|
707
|
+
name: 'Missing Rate Limit',
|
|
708
|
+
category: 'api',
|
|
709
|
+
test: (content, file) => {
|
|
710
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
711
|
+
if (file.includes('/public/')) return null;
|
|
712
|
+
if (!content.includes('rateLimit') &&
|
|
713
|
+
!content.includes('rateLimiter') &&
|
|
714
|
+
!content.includes('autoRateLimit') &&
|
|
715
|
+
!content.includes('throttle')) {
|
|
716
|
+
return 'API route without rate limiting - add protection against abuse';
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: 'N+1 Query Pattern',
|
|
723
|
+
category: 'api',
|
|
724
|
+
test: (content, file) => {
|
|
725
|
+
// Check for await in loop with database calls
|
|
726
|
+
if ((content.includes('for (') || content.includes('forEach') || content.includes('.map(')) &&
|
|
727
|
+
content.includes('await') &&
|
|
728
|
+
(content.includes('db.') || content.includes('prisma.') || content.includes('findOne') || content.includes('findById'))) {
|
|
729
|
+
return 'Possible N+1 query - fetch data in batch instead of loop';
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: 'Missing CORS Config',
|
|
736
|
+
category: 'api',
|
|
737
|
+
test: (content, file) => {
|
|
738
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
739
|
+
if (content.includes("'*'") &&
|
|
740
|
+
(content.includes('Access-Control-Allow-Origin') || content.includes('cors'))) {
|
|
741
|
+
return 'Overly permissive CORS (*) - restrict to specific origins';
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
// ==========================================
|
|
748
|
+
// IMPORT CHECKS
|
|
749
|
+
// ==========================================
|
|
750
|
+
{
|
|
751
|
+
name: 'Circular Import Risk',
|
|
752
|
+
category: 'imports',
|
|
753
|
+
test: (content, file) => {
|
|
754
|
+
// Check for importing from parent directory and exporting to child
|
|
755
|
+
const parentImports = (content.match(/from\\s+['"]\\.\\.\\/[^'"]+['"]/g) || []).length;
|
|
756
|
+
const hasExport = content.includes('export ');
|
|
757
|
+
if (parentImports > 3 && hasExport) {
|
|
758
|
+
return 'Multiple parent imports - potential circular dependency risk';
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: 'Unused Import',
|
|
765
|
+
category: 'imports',
|
|
766
|
+
test: (content, file) => {
|
|
767
|
+
// Simple check for imported names not used
|
|
768
|
+
const imports = content.match(/import\\s+\\{([^}]+)\\}/g) || [];
|
|
769
|
+
for (const imp of imports) {
|
|
770
|
+
const names = imp.replace(/import\\s+\\{/, '').replace(/\\}/, '').split(',');
|
|
771
|
+
for (const name of names) {
|
|
772
|
+
const cleanName = name.trim().split(' as ')[0].trim();
|
|
773
|
+
if (cleanName && cleanName.length > 1) {
|
|
774
|
+
// Count occurrences (should be > 1 to include the import itself)
|
|
775
|
+
const regex = new RegExp('\\\\b' + cleanName + '\\\\b', 'g');
|
|
776
|
+
const occurrences = (content.match(regex) || []).length;
|
|
777
|
+
if (occurrences === 1) {
|
|
778
|
+
return \`Possibly unused import: \${cleanName}\`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
// ==========================================
|
|
788
|
+
// 🍪 CODEBAKERS PATTERN CHECKS
|
|
789
|
+
// ==========================================
|
|
790
|
+
{
|
|
791
|
+
name: 'CB: Missing handleApiError',
|
|
792
|
+
category: 'codebakers',
|
|
793
|
+
test: (content, file) => {
|
|
794
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
795
|
+
if (file.includes('/webhook')) return null; // Webhooks may have special handling
|
|
796
|
+
|
|
797
|
+
// Check if it's an API route with exports
|
|
798
|
+
if ((content.includes('export async function') || content.includes('export function')) &&
|
|
799
|
+
(content.includes('POST') || content.includes('GET') || content.includes('PUT') || content.includes('DELETE'))) {
|
|
800
|
+
|
|
801
|
+
if (!content.includes('handleApiError')) {
|
|
802
|
+
return 'API route missing handleApiError - use CodeBakers error handling pattern';
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: 'CB: Missing successResponse',
|
|
810
|
+
category: 'codebakers',
|
|
811
|
+
test: (content, file) => {
|
|
812
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
813
|
+
|
|
814
|
+
// Check if returning NextResponse.json directly instead of successResponse
|
|
815
|
+
if (content.includes('NextResponse.json') &&
|
|
816
|
+
!content.includes('successResponse') &&
|
|
817
|
+
!content.includes('/webhook')) {
|
|
818
|
+
return 'Use successResponse() helper instead of NextResponse.json directly';
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: 'CB: Missing Zod Schema',
|
|
825
|
+
category: 'codebakers',
|
|
826
|
+
test: (content, file) => {
|
|
827
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
828
|
+
|
|
829
|
+
// POST/PUT/PATCH should have Zod validation
|
|
830
|
+
if ((content.includes('export async function POST') ||
|
|
831
|
+
content.includes('export async function PUT') ||
|
|
832
|
+
content.includes('export async function PATCH')) &&
|
|
833
|
+
content.includes('req.json()') &&
|
|
834
|
+
!content.includes('z.object') &&
|
|
835
|
+
!content.includes('Schema.parse') &&
|
|
836
|
+
!content.includes('schema.parse') &&
|
|
837
|
+
!content.includes('.safeParse')) {
|
|
838
|
+
return 'API mutation missing Zod schema validation - define schema with z.object()';
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: 'CB: Missing Rate Limiting',
|
|
845
|
+
category: 'codebakers',
|
|
846
|
+
test: (content, file) => {
|
|
847
|
+
if (!file.includes('/api/') && !file.includes('route.ts')) return null;
|
|
848
|
+
if (file.includes('/public/') || file.includes('/webhook')) return null;
|
|
849
|
+
|
|
850
|
+
if ((content.includes('export async function') || content.includes('export function')) &&
|
|
851
|
+
!content.includes('autoRateLimit') &&
|
|
852
|
+
!content.includes('rateLimit')) {
|
|
853
|
+
return 'API route missing autoRateLimit() - add rate limiting protection';
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: 'CB: Service Not Singleton',
|
|
860
|
+
category: 'codebakers',
|
|
861
|
+
test: (content, file) => {
|
|
862
|
+
if (!file.includes('/services/') && !file.includes('-service.ts')) return null;
|
|
863
|
+
|
|
864
|
+
// Services should use static methods (singleton pattern)
|
|
865
|
+
if (content.includes('class ') &&
|
|
866
|
+
!content.includes('static ') &&
|
|
867
|
+
content.includes('export')) {
|
|
868
|
+
return 'Service class should use static methods (singleton pattern)';
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
name: 'CB: Raw SQL Query',
|
|
875
|
+
category: 'codebakers',
|
|
876
|
+
test: (content, file) => {
|
|
877
|
+
// Check for raw SQL instead of Drizzle
|
|
878
|
+
if ((content.includes('SELECT ') || content.includes('INSERT ') || content.includes('UPDATE ')) &&
|
|
879
|
+
content.includes("'") &&
|
|
880
|
+
!content.includes('drizzle') &&
|
|
881
|
+
!content.includes('sql.raw') &&
|
|
882
|
+
!content.includes('db.') &&
|
|
883
|
+
file.endsWith('.ts')) {
|
|
884
|
+
return 'Use Drizzle ORM instead of raw SQL queries';
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
name: 'CB: Missing Error Boundary',
|
|
891
|
+
category: 'codebakers',
|
|
892
|
+
test: (content, file) => {
|
|
893
|
+
// Page components with data fetching should have error handling
|
|
894
|
+
if (file.includes('/app/') && file.includes('page.tsx')) {
|
|
895
|
+
if (content.includes('async') &&
|
|
896
|
+
(content.includes('fetch') || content.includes('db.') || content.includes('Service.')) &&
|
|
897
|
+
!content.includes('error.tsx') &&
|
|
898
|
+
!content.includes('ErrorBoundary') &&
|
|
899
|
+
!content.includes('try {')) {
|
|
900
|
+
return 'Page with data fetching needs error.tsx or try/catch';
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: 'CB: Form Without Validation',
|
|
908
|
+
category: 'codebakers',
|
|
909
|
+
test: (content, file) => {
|
|
910
|
+
if (!file.endsWith('.tsx')) return null;
|
|
911
|
+
|
|
912
|
+
// Forms should use react-hook-form with zod
|
|
913
|
+
if (content.includes('<form') &&
|
|
914
|
+
content.includes('onSubmit') &&
|
|
915
|
+
!content.includes('useForm') &&
|
|
916
|
+
!content.includes('react-hook-form') &&
|
|
917
|
+
!content.includes('zodResolver')) {
|
|
918
|
+
return 'Use react-hook-form with zodResolver for form validation';
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
name: 'CB: Direct fetch() Usage',
|
|
925
|
+
category: 'codebakers',
|
|
926
|
+
test: (content, file) => {
|
|
927
|
+
if (file.includes('.test.') || file.includes('/tests/')) return null;
|
|
928
|
+
if (!file.endsWith('.ts') && !file.endsWith('.tsx')) return null;
|
|
929
|
+
|
|
930
|
+
// Client components should use React Query or SWR, not raw fetch
|
|
931
|
+
if (content.includes("'use client'") &&
|
|
932
|
+
content.includes('fetch(') &&
|
|
933
|
+
!content.includes('useQuery') &&
|
|
934
|
+
!content.includes('useMutation') &&
|
|
935
|
+
!content.includes('useSWR')) {
|
|
936
|
+
return 'Use React Query (useQuery/useMutation) instead of raw fetch in client components';
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
name: 'CB: Missing Loading State',
|
|
943
|
+
category: 'codebakers',
|
|
944
|
+
test: (content, file) => {
|
|
945
|
+
if (!file.endsWith('.tsx')) return null;
|
|
946
|
+
|
|
947
|
+
// Components with async data should have loading states
|
|
948
|
+
if (content.includes("'use client'") &&
|
|
949
|
+
(content.includes('useQuery') || content.includes('useSWR') || content.includes('useState')) &&
|
|
950
|
+
content.includes('async') &&
|
|
951
|
+
!content.includes('isLoading') &&
|
|
952
|
+
!content.includes('loading') &&
|
|
953
|
+
!content.includes('isPending') &&
|
|
954
|
+
!content.includes('Skeleton') &&
|
|
955
|
+
!content.includes('Spinner')) {
|
|
956
|
+
return 'Add loading state (isLoading, Skeleton, or Spinner) for async operations';
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
async function validateCode() {
|
|
964
|
+
const cwd = process.cwd();
|
|
965
|
+
const violations = [];
|
|
966
|
+
const warnings = [];
|
|
967
|
+
|
|
968
|
+
const allStagedFiles = getStagedFiles();
|
|
969
|
+
const codeFiles = getCodeFiles(allStagedFiles);
|
|
970
|
+
|
|
971
|
+
log(CYAN, '\\n🍪 CodeBakers Pre-Commit Checks');
|
|
972
|
+
log(CYAN, '================================\\n');
|
|
973
|
+
|
|
974
|
+
// Check for .env files being committed
|
|
975
|
+
for (const file of allStagedFiles) {
|
|
976
|
+
if (file.startsWith('.env')) {
|
|
977
|
+
violations.push({
|
|
978
|
+
check: 'Env File Commit',
|
|
979
|
+
category: 'security',
|
|
980
|
+
message: '.env file should not be committed',
|
|
981
|
+
file: file
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (codeFiles.length === 0 && violations.length === 0) {
|
|
987
|
+
log(DIM, 'No code files staged.\\n');
|
|
988
|
+
log(GREEN, '================================');
|
|
989
|
+
log(GREEN, '✅ All pre-commit checks passed!');
|
|
990
|
+
log(GREEN, '================================\\n');
|
|
991
|
+
return { valid: true };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
log(DIM, '📋 Checking \${codeFiles.length} code file(s)...\\n');
|
|
995
|
+
|
|
996
|
+
for (const file of codeFiles) {
|
|
997
|
+
const filePath = path.join(cwd, file);
|
|
998
|
+
if (!fs.existsSync(filePath)) continue;
|
|
999
|
+
|
|
1000
|
+
let content;
|
|
1001
|
+
try {
|
|
1002
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
1003
|
+
} catch {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
for (const check of CHECKS) {
|
|
1008
|
+
try {
|
|
1009
|
+
const result = check.test(content, file);
|
|
1010
|
+
if (result) {
|
|
1011
|
+
violations.push({
|
|
1012
|
+
check: check.name,
|
|
1013
|
+
category: check.category,
|
|
1014
|
+
message: result,
|
|
1015
|
+
file: file
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
// Skip check on error
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Report results
|
|
1025
|
+
if (violations.length > 0) {
|
|
1026
|
+
// Group by category
|
|
1027
|
+
const byCategory = {};
|
|
1028
|
+
for (const v of violations) {
|
|
1029
|
+
if (!byCategory[v.category]) byCategory[v.category] = [];
|
|
1030
|
+
byCategory[v.category].push(v);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const categoryNames = {
|
|
1034
|
+
security: '🔒 Security',
|
|
1035
|
+
errors: '⚠️ Error Handling',
|
|
1036
|
+
validation: '✅ Validation',
|
|
1037
|
+
quality: '📝 Code Quality',
|
|
1038
|
+
typescript: '📘 TypeScript',
|
|
1039
|
+
react: '⚛️ React',
|
|
1040
|
+
a11y: '♿ Accessibility',
|
|
1041
|
+
performance: '⚡ Performance',
|
|
1042
|
+
api: '🌐 API',
|
|
1043
|
+
imports: '📦 Imports',
|
|
1044
|
+
codebakers: '🍪 CodeBakers Patterns',
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
log(RED, \`\\n❌ Found \${violations.length} issue(s):\\n\`);
|
|
1048
|
+
|
|
1049
|
+
for (const [category, items] of Object.entries(byCategory)) {
|
|
1050
|
+
log(YELLOW, \`\\n\${categoryNames[category] || category}:\`);
|
|
1051
|
+
const byFile = {};
|
|
1052
|
+
for (const v of items) {
|
|
1053
|
+
if (!byFile[v.file]) byFile[v.file] = [];
|
|
1054
|
+
byFile[v.file].push(v);
|
|
1055
|
+
}
|
|
1056
|
+
for (const [file, fileViolations] of Object.entries(byFile)) {
|
|
1057
|
+
log(DIM, \` \${file}:\`);
|
|
1058
|
+
for (const v of fileViolations) {
|
|
1059
|
+
log(RED, \` ✗ \${v.message}\`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
console.log('');
|
|
1065
|
+
log(CYAN, 'Fix these issues and try again.');
|
|
1066
|
+
log(YELLOW, 'To bypass (not recommended): git commit --no-verify\\n');
|
|
1067
|
+
|
|
1068
|
+
return { valid: false, violations };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
log(GREEN, '✅ All \${CHECKS.length} checks passed!\\n');
|
|
1072
|
+
|
|
1073
|
+
// Run tests if available
|
|
1074
|
+
log(DIM, '🧪 Running tests...\\n');
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
1078
|
+
if (fs.existsSync(pkgPath)) {
|
|
1079
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1080
|
+
if (pkg.scripts && pkg.scripts.test) {
|
|
1081
|
+
execSync('npm test', { stdio: 'inherit', cwd });
|
|
1082
|
+
log(GREEN, '✅ Tests passed!\\n');
|
|
1083
|
+
} else {
|
|
1084
|
+
log(DIM, 'No test script found, skipping...\\n');
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
log(RED, '❌ Tests failed!\\n');
|
|
1089
|
+
return { valid: false, reason: 'tests-failed' };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
log(GREEN, '================================');
|
|
1093
|
+
log(GREEN, '✅ All pre-commit checks passed!');
|
|
1094
|
+
log(GREEN, '================================\\n');
|
|
1095
|
+
|
|
1096
|
+
return { valid: true };
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function main() {
|
|
1100
|
+
const result = await validateCode();
|
|
1101
|
+
process.exit(result.valid ? 0 : 1);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
main().catch(error => {
|
|
1105
|
+
log(RED, ' Error: ' + error.message);
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
});
|
|
1108
|
+
`;
|
|
1109
|
+
|
|
1110
|
+
export async function installPrecommit(): Promise<void> {
|
|
1111
|
+
console.log(chalk.blue('\n CodeBakers Pre-Commit Hook Installation\n'));
|
|
1112
|
+
|
|
1113
|
+
const cwd = process.cwd();
|
|
1114
|
+
|
|
1115
|
+
// Check if this is a git repository
|
|
1116
|
+
const gitDir = join(cwd, '.git');
|
|
1117
|
+
if (!existsSync(gitDir)) {
|
|
1118
|
+
console.log(chalk.red(' ✗ Not a git repository'));
|
|
1119
|
+
console.log(chalk.gray(' Initialize git first: git init\n'));
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Create hooks directory if it doesn't exist
|
|
1124
|
+
const hooksDir = join(gitDir, 'hooks');
|
|
1125
|
+
if (!existsSync(hooksDir)) {
|
|
1126
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Write the pre-commit hook
|
|
1130
|
+
const preCommitPath = join(hooksDir, 'pre-commit');
|
|
1131
|
+
writeFileSync(preCommitPath, PRE_COMMIT_SCRIPT);
|
|
1132
|
+
|
|
1133
|
+
// Make it executable
|
|
1134
|
+
try {
|
|
1135
|
+
chmodSync(preCommitPath, '755');
|
|
1136
|
+
} catch {
|
|
1137
|
+
// Windows doesn't support chmod
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
console.log(chalk.green(' ✓ Created pre-commit hook'));
|
|
1141
|
+
|
|
1142
|
+
// Write the validation script
|
|
1143
|
+
const validatePath = join(hooksDir, 'validate-code.js');
|
|
1144
|
+
writeFileSync(validatePath, VALIDATE_CODE_SCRIPT);
|
|
1145
|
+
|
|
1146
|
+
console.log(chalk.green(' ✓ Created code validation script (40+ checks)'));
|
|
1147
|
+
|
|
1148
|
+
// Check if husky is being used
|
|
1149
|
+
const huskyDir = join(cwd, '.husky');
|
|
1150
|
+
if (existsSync(huskyDir)) {
|
|
1151
|
+
const huskyPreCommit = join(huskyDir, 'pre-commit');
|
|
1152
|
+
let huskyContent = '';
|
|
1153
|
+
|
|
1154
|
+
if (existsSync(huskyPreCommit)) {
|
|
1155
|
+
huskyContent = readFileSync(huskyPreCommit, 'utf-8');
|
|
1156
|
+
if (!huskyContent.includes('validate-code')) {
|
|
1157
|
+
huskyContent += '\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
|
|
1158
|
+
writeFileSync(huskyPreCommit, huskyContent);
|
|
1159
|
+
console.log(chalk.green(' ✓ Added to existing husky pre-commit'));
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers code validation\nnode .git/hooks/validate-code.js\n';
|
|
1163
|
+
writeFileSync(huskyPreCommit, huskyContent);
|
|
1164
|
+
try {
|
|
1165
|
+
chmodSync(huskyPreCommit, '755');
|
|
1166
|
+
} catch {}
|
|
1167
|
+
console.log(chalk.green(' ✓ Created husky pre-commit hook'));
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
console.log(chalk.green('\n ✅ Pre-commit hook installed with 53 checks!\n'));
|
|
1172
|
+
|
|
1173
|
+
console.log(chalk.magenta(' 🍪 CodeBakers Patterns (10 checks):'));
|
|
1174
|
+
console.log(chalk.gray(' handleApiError, successResponse, Zod schemas, rate limiting,'));
|
|
1175
|
+
console.log(chalk.gray(' service singletons, Drizzle ORM, error boundaries,'));
|
|
1176
|
+
console.log(chalk.gray(' react-hook-form, React Query, loading states\n'));
|
|
1177
|
+
|
|
1178
|
+
console.log(chalk.cyan(' 🔒 Security (9 checks):'));
|
|
1179
|
+
console.log(chalk.gray(' Debugger statements, hardcoded secrets, XSS, SQL injection,'));
|
|
1180
|
+
console.log(chalk.gray(' merge conflicts, private keys, .env files, eval(), sensitive logs\n'));
|
|
1181
|
+
|
|
1182
|
+
console.log(chalk.cyan(' ⚠️ Error Handling (6 checks):'));
|
|
1183
|
+
console.log(chalk.gray(' API try/catch, empty catch, unhandled promises, JSON.parse safety\n'));
|
|
1184
|
+
|
|
1185
|
+
console.log(chalk.cyan(' 📘 TypeScript (4 checks):'));
|
|
1186
|
+
console.log(chalk.gray(' No "any" types, unsafe assertions, return types, non-null assertions\n'));
|
|
1187
|
+
|
|
1188
|
+
console.log(chalk.cyan(' ⚛️ React (7 checks):'));
|
|
1189
|
+
console.log(chalk.gray(' DOM manipulation, useEffect cleanup, conditional hooks, keys,'));
|
|
1190
|
+
console.log(chalk.gray(' inline functions, error boundaries\n'));
|
|
1191
|
+
|
|
1192
|
+
console.log(chalk.cyan(' ♿ Accessibility (5 checks):'));
|
|
1193
|
+
console.log(chalk.gray(' Image alt text, button types, form labels, keyboard support, ARIA roles\n'));
|
|
1194
|
+
|
|
1195
|
+
console.log(chalk.cyan(' ⚡ Performance (3 checks):'));
|
|
1196
|
+
console.log(chalk.gray(' Large imports, sync file operations, missing memoization\n'));
|
|
1197
|
+
|
|
1198
|
+
console.log(chalk.cyan(' 🌐 API (3 checks):'));
|
|
1199
|
+
console.log(chalk.gray(' Rate limiting, N+1 queries, CORS config\n'));
|
|
1200
|
+
|
|
1201
|
+
console.log(chalk.cyan(' 📝 Code Quality (6 checks):'));
|
|
1202
|
+
console.log(chalk.gray(' Console.log, TODO/FIXME, hardcoded URLs, file size, magic numbers\n'));
|
|
1203
|
+
|
|
1204
|
+
console.log(chalk.yellow(' To bypass (not recommended): git commit --no-verify\n'));
|
|
1205
|
+
}
|