buddy-workbench 0.1.80 → 0.1.82
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/bin/devbuddy.js +7 -2
- package/package.json +1 -1
- package/server/routes/overview.js +75 -0
- package/server/routes/pr-review.js +64 -124
- package/ui/dist/assets/{index-CdZaqQ2i.js → index-CH4N7LXY.js} +149 -149
- package/ui/dist/assets/index-X_Dghfg1.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-BkYIiYTQ.css +0 -1
package/bin/devbuddy.js
CHANGED
|
@@ -64,10 +64,15 @@ function npmExecOptions(options = {}) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function npmInstallArgs(packageSpec, global = false) {
|
|
68
|
+
const before = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString();
|
|
69
|
+
return ['install', ...(global ? ['--global'] : []), '--before', before, packageSpec];
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
function scheduleWindowsSelfUpdate(packageSpec) {
|
|
68
73
|
const updateScript = `import { execFileSync } from 'node:child_process';
|
|
69
74
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
70
|
-
execFileSync('npm.cmd',
|
|
75
|
+
execFileSync('npm.cmd', ${JSON.stringify(npmInstallArgs(packageSpec, true))}, { shell: true, windowsHide: true, stdio: 'inherit' });`;
|
|
71
76
|
const updater = spawn(process.execPath, ['--input-type=module', '-e', updateScript], {
|
|
72
77
|
cwd: homedir(),
|
|
73
78
|
detached: true,
|
|
@@ -117,7 +122,7 @@ function selfUpdate(args) {
|
|
|
117
122
|
return;
|
|
118
123
|
}
|
|
119
124
|
try {
|
|
120
|
-
execFileSync(npmCommand(),
|
|
125
|
+
execFileSync(npmCommand(), npmInstallArgs(`${details.name}@${version}`, true), npmExecOptions({
|
|
121
126
|
cwd: root,
|
|
122
127
|
stdio: 'inherit'
|
|
123
128
|
}));
|
package/package.json
CHANGED
|
@@ -68,6 +68,55 @@ function mapJiraIssue(issue) {
|
|
|
68
68
|
|
|
69
69
|
function issueUrl(host, key) { return `https://${host}/browse/${encodeURIComponent(key)}`; }
|
|
70
70
|
|
|
71
|
+
function myWorkUrl(host, path) { return `https://${host}/rest/mywork/latest${path}`; }
|
|
72
|
+
|
|
73
|
+
function notificationText(value) {
|
|
74
|
+
if (typeof value !== 'string') return '';
|
|
75
|
+
return value.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function notificationUrl(host, item) {
|
|
79
|
+
const candidate = item?.url || item?.link || item?._links?.webui || item?.content?.url || item?.content?._links?.webui;
|
|
80
|
+
if (!candidate) return '';
|
|
81
|
+
return /^https?:\/\//i.test(candidate) ? candidate : `https://${host}${candidate.startsWith('/') ? '' : '/'}${candidate}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function notificationDate(value) {
|
|
85
|
+
if (!value) return null;
|
|
86
|
+
const date = new Date(value);
|
|
87
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function loadNotifications(host, token, source) {
|
|
91
|
+
if (!host || !token) return [];
|
|
92
|
+
const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
93
|
+
const url = myWorkUrl(host, '/notification?limit=50');
|
|
94
|
+
const response = await httpClient.get(url, { headers: { Accept: 'application/json', Authorization: authHeader } });
|
|
95
|
+
if (!isSuccess(response)) throw new Error(`${source} notifications failed (${response.status}): ${apiMessage(response, 'Request failed')}`);
|
|
96
|
+
|
|
97
|
+
const payload = response.data;
|
|
98
|
+
const values = Array.isArray(payload)
|
|
99
|
+
? payload
|
|
100
|
+
: payload?.notifications || payload?.values || payload?.items || payload?.results || [];
|
|
101
|
+
return values.map((item, index) => {
|
|
102
|
+
const title = notificationText(item?.title || item?.subject || item?.message || item?.content?.title || item?.content?.summary) || `${source} notification`;
|
|
103
|
+
const summary = notificationText(item?.body || item?.description || item?.content?.body || item?.content?.description || item?.message || '');
|
|
104
|
+
const date = item?.created || item?.createdDate || item?.updated || item?.updatedDate || item?.timestamp || item?.content?.updated;
|
|
105
|
+
const normalizedDate = notificationDate(date);
|
|
106
|
+
return {
|
|
107
|
+
id: `${source.toLowerCase()}-${item?.id || item?.notificationId || index}`,
|
|
108
|
+
title,
|
|
109
|
+
summary,
|
|
110
|
+
source,
|
|
111
|
+
sourceType: source.toLowerCase(),
|
|
112
|
+
read: item?.read === true || item?.isRead === true,
|
|
113
|
+
activityAt: normalizedDate,
|
|
114
|
+
updated: normalizedDate,
|
|
115
|
+
url: notificationUrl(host, item)
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
71
120
|
async function loadJiraData(host, token) {
|
|
72
121
|
const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
|
|
73
122
|
const headers = { Accept: 'application/json', Authorization: authHeader };
|
|
@@ -267,4 +316,30 @@ router.get('/', async (_req, res) => {
|
|
|
267
316
|
});
|
|
268
317
|
});
|
|
269
318
|
|
|
319
|
+
router.get('/notifications', async (_req, res) => {
|
|
320
|
+
const settings = readSettings();
|
|
321
|
+
const jira = jiraHost(settings.domain);
|
|
322
|
+
const confluence = confluenceHost(settings.domain);
|
|
323
|
+
const confluenceToken = settings.confluenceAccessToken || settings.jiraAccessToken;
|
|
324
|
+
|
|
325
|
+
if (!jira && !confluence) {
|
|
326
|
+
return res.json({ configured: false, notifications: { jira: [], confluence: [] }, errors: ['Configure a Jira or Confluence domain in Settings.'] });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const [jiraResult, confluenceResult] = await Promise.all([
|
|
330
|
+
requestSection('Jira notifications', () => loadNotifications(jira, settings.jiraAccessToken, 'Jira')),
|
|
331
|
+
requestSection('Confluence notifications', () => loadNotifications(confluence, confluenceToken, 'Confluence'))
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
return res.json({
|
|
335
|
+
configured: true,
|
|
336
|
+
fetchedAt: new Date().toISOString(),
|
|
337
|
+
notifications: {
|
|
338
|
+
jira: jiraResult.items || [],
|
|
339
|
+
confluence: confluenceResult.items || []
|
|
340
|
+
},
|
|
341
|
+
errors: [jiraResult.error, confluenceResult.error].filter(Boolean)
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
270
345
|
export default router;
|
|
@@ -13,7 +13,7 @@ const aiReviewJobs = new Map();
|
|
|
13
13
|
const PR_REVIEW_RULES = [
|
|
14
14
|
{ id: 'lodash-get', name: 'Lodash Get Required', description: 'Prefer lodash/get for nested object property access.', pattern: String.raw`\b([a-zA-Z_$][\w$]*)\s*(\?\.|\.)\s*([a-zA-Z_$][\w$]*)\b(?!\s*\()`, severity: 'warning' },
|
|
15
15
|
{ id: 'no-any', name: 'No Any Type', description: 'Disallow the TypeScript any type.', pattern: String.raw`:\s*any\b|:\s*any\[\]|as\s+any\b|<[^>]*?\bany\b[^>]*?>|\bany\[\]`, severity: 'critical' },
|
|
16
|
-
{ id: 'no-hardcoded-strings', name: 'No Hardcoded Strings', description: 'Move hardcoded text into constants or i18n.', pattern: String.raw`(const|let|var)\s+[a-zA-Z_$][\w$]*\s*=\s*['"][^'"]+['"]`, severity: 'warning' },
|
|
16
|
+
{ id: 'no-hardcoded-strings', name: 'No Hardcoded Strings', description: 'Move hardcoded text into constants or i18n.', pattern: String.raw`(const|let|var)\s+[a-zA-Z_$][\w$]*\s*=\s*['"\x60][^'"\x60]+['"\x60]|\b(?!className\b|type\b|key\b|name\b|rel\b|target\b|method\b)[a-zA-Z_$][\w$-]*\s*=\s*['"\x60][^'"\x60]+['"\x60]`, severity: 'warning' },
|
|
17
17
|
{ id: 'no-inline-styles', name: 'No Inline Styles', description: 'Use CSS classes or styled components instead of inline styles.', pattern: String.raw`\bstyle\s*=\s*\{\s*\{[\s\S]*?\}\s*\}|\bstyle\s*=\s*['"][\s\S]*?['"]`, severity: 'warning' },
|
|
18
18
|
{ id: 'clickable-element-tag', name: 'Clickable Element Tag', description: 'Use button or anchor elements for clickable UI.', pattern: String.raw`<(div|span|p|li|tr|td|img|i|svg|section|article|header|footer|h[1-6]|label)\b[^>]*?\b(onClick|@click)\b[^>]*?>`, severity: 'warning' },
|
|
19
19
|
{ id: 'no-relative-paths', name: 'No Relative Paths', description: 'Use absolute module imports or path aliases.', pattern: String.raw`import\s*(?:\{[^}]*\}|[^{'"\n]+)\s*from\s*['"]\.\.?\/*['"]|require\s*\(\s*['"]\.\.?\/*['"]\)`, severity: 'warning' },
|
|
@@ -77,6 +77,25 @@ async function fetchFullFileContent({ host, projectKey, repositorySlug, filePath
|
|
|
77
77
|
return String(response.data ?? '');
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function formatDiffForAi(hunks) {
|
|
81
|
+
return (hunks || []).map((hunk) => {
|
|
82
|
+
const sourceLine = hunk.sourceLine ?? hunk.source ?? 0;
|
|
83
|
+
const sourceSpan = hunk.sourceSpan ?? 0;
|
|
84
|
+
const destinationLine = hunk.destinationLine ?? hunk.destination ?? 0;
|
|
85
|
+
const destinationSpan = hunk.destinationSpan ?? 0;
|
|
86
|
+
const header = `@@ -${sourceLine},${sourceSpan} +${destinationLine},${destinationSpan} @@`;
|
|
87
|
+
const lines = (hunk.segments || []).flatMap((segment) => (segment.lines || []).map((line) => {
|
|
88
|
+
const text = line.line ?? line.text ?? '';
|
|
89
|
+
const lineNumber = segment.type === 'REMOVED'
|
|
90
|
+
? (line.source ?? line.sourceLine ?? '')
|
|
91
|
+
: (line.destination ?? line.destinationLine ?? '');
|
|
92
|
+
const prefix = segment.type === 'ADDED' ? '+' : segment.type === 'REMOVED' ? '-' : ' ';
|
|
93
|
+
return `${prefix}${lineNumber === '' ? '' : `${lineNumber} `}${text}`;
|
|
94
|
+
}));
|
|
95
|
+
return [header, ...lines].join('\n');
|
|
96
|
+
}).filter(Boolean).join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
function aiCompletionUrl(host) {
|
|
81
100
|
const normalizedHost = String(host || '').replace(/\/+$/, '');
|
|
82
101
|
return /\/chat\/completions$/i.test(normalizedHost) ? normalizedHost : `${normalizedHost}/chat/completions`;
|
|
@@ -127,7 +146,7 @@ async function runAiReview({ aiApiHost, pr, files }) {
|
|
|
127
146
|
`Description: ${pr.description || ''}`,
|
|
128
147
|
`Branches: ${pr.sourceBranch || ''} -> ${pr.targetBranch || ''}`,
|
|
129
148
|
'',
|
|
130
|
-
'Changed files (
|
|
149
|
+
'Changed files (diff only; do not review unchanged code):',
|
|
131
150
|
...files.map((file) => [
|
|
132
151
|
`--- FILE: ${file.filePath} (${file.type || 'MODIFY'}) ---`,
|
|
133
152
|
file.content,
|
|
@@ -545,129 +564,50 @@ const analyzeDiff = (filePath, hunks) => {
|
|
|
545
564
|
}
|
|
546
565
|
}
|
|
547
566
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const lineText = getLineInfo(matchIdx).text.trim();
|
|
567
|
-
if (/^\s*(import|export|type|interface)\b/.test(lineText)) continue;
|
|
568
|
-
if (/\b(lodash\.)?get\s*\(/.test(lineText)) continue;
|
|
569
|
-
|
|
570
|
-
addIssue('warning', 'Lodash Get Required', 'Object property better use `lodash/get`: `get(obj, ...)`', matchIdx);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Rule 2: TS cannot use 'any' type
|
|
574
|
-
const anyRegex = /:\s*any\b|:\s*any\[\]|as\s+any\b|<[^>]*?\bany\b[^>]*?>|\bany\[\]/g;
|
|
575
|
-
for (const match of segmentText.matchAll(anyRegex)) {
|
|
576
|
-
addIssue('critical', 'No Any Type', 'TypeScript strictly prohibits using the `any` type.', match.index);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Rule 3: No hardcoded text / strings
|
|
580
|
-
const hardcodedVarRegex = /(const|let|var)\s+[a-zA-Z_$][\w$]*\s*=\s*['"`][^'"`]+['"`]/g;
|
|
581
|
-
for (const match of segmentText.matchAll(hardcodedVarRegex)) {
|
|
582
|
-
const lineText = getLineInfo(match.index).text.trim();
|
|
583
|
-
if (!/^\s*(import|export)\b/.test(lineText) && !/\brequire\s*\(/.test(lineText)) {
|
|
584
|
-
addIssue('warning', 'No Hardcoded Strings', 'Do not use hardcoded string literals (e.g., `const a = "aa"` or `attr="111"`). Use constants or i18n.', match.index);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
const hardcodedAttrRegex = /\b(?!className\b|type\b|key\b|name\b|rel\b|target\b|method\b)[a-zA-Z_$][\w$-]*\s*=\s*['"`][^'"`]+['"`]/g;
|
|
588
|
-
for (const match of segmentText.matchAll(hardcodedAttrRegex)) {
|
|
589
|
-
const lineText = getLineInfo(match.index).text.trim();
|
|
590
|
-
if (!/^\s*(import|export)\b/.test(lineText) && !/\brequire\s*\(/.test(lineText)) {
|
|
591
|
-
addIssue('warning', 'No Hardcoded Strings', 'Do not use hardcoded string literals (e.g., `const a = "aa"` or `attr="111"`). Use constants or i18n.', match.index);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Rule 4: No inline styles (multiline)
|
|
596
|
-
const styleRegex = /\bstyle\s*=\s*\{\s*\{[\s\S]*?\}\s*\}|\bstyle\s*=\s*['"][\s\S]*?['"]/g;
|
|
597
|
-
for (const match of segmentText.matchAll(styleRegex)) {
|
|
598
|
-
addIssue('warning', 'No Inline Styles', 'Inline styles are strictly prohibited. Use CSS classes or styled components.', match.index);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Rule 6: Clickable elements must be button or a (multiline JSX opening tag)
|
|
602
|
-
const nonClickableTagRegex = /<(div|span|p|li|tr|td|img|i|svg|section|article|header|footer|h[1-6]|label)\b[^>]*?\b(onClick|@click)\b[^>]*?>/gis;
|
|
603
|
-
for (const match of segmentText.matchAll(nonClickableTagRegex)) {
|
|
604
|
-
addIssue('warning', 'Clickable Element Tag', 'Clickable elements must use `<button>` or `<a>` tags (or Button/Link components). Do not bind onClick to non-clickable tags like div/span.', match.index, match[0]);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Rule 7: No relative paths (multiline import)
|
|
608
|
-
const relativeImportRegex = /(import\s*(?:\{[^}]*\}|[^{'"\n]+)\s*from\s*['"]\.\.?\/*['"]|require\s*\(\s*['"]\.\.?\/*['"]\))/gs;
|
|
609
|
-
for (const match of segmentText.matchAll(relativeImportRegex)) {
|
|
610
|
-
addIssue('warning', 'No Relative Paths', 'Relative module imports (`./` or `../`) are prohibited. Use absolute paths or path aliases.', match.index, match[0]);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Rule 8: No debugger
|
|
614
|
-
const debuggerRegex = /\bdebugger\b/g;
|
|
615
|
-
for (const match of segmentText.matchAll(debuggerRegex)) {
|
|
616
|
-
addIssue('critical', 'No Debugger', 'Do not leave `debugger;` statements in code.', match.index);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Rule 9: No console.log
|
|
620
|
-
const consoleRegex = /\bconsole\.log\b/g;
|
|
621
|
-
for (const match of segmentText.matchAll(consoleRegex)) {
|
|
622
|
-
addIssue('critical', 'No Console Log', 'Do not leave `console.log` statements in code.', match.index);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Rule 10: No array index key
|
|
626
|
-
const keyIndexRegex = /\bkey\s*=\s*\{\s*(index|idx|i)\s*\}/g;
|
|
627
|
-
for (const match of segmentText.matchAll(keyIndexRegex)) {
|
|
628
|
-
addIssue('warning', 'No Array Index Key', 'Avoid using array index (e.g., `key={index}`) as a React key in list rendering.', match.index);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Rule 11: No nested components
|
|
632
|
-
const nestedCompRegex = /^\s*(const|function)\s+[A-Z][a-zA-Z0-9_$]*\s*=\s*\(|\bfunction\s+[A-Z][a-zA-Z0-9_$]*\s*\(/gm;
|
|
633
|
-
for (const match of segmentText.matchAll(nestedCompRegex)) {
|
|
634
|
-
const lineText = getLineInfo(match.index).text.trim();
|
|
635
|
-
if (!/^\s*(export|import)\b/.test(lineText)) {
|
|
636
|
-
addIssue('warning', 'No Nested Components', 'Do not define React components inside another component body. Move component definitions outside.', match.index);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Rule 12: No async client component
|
|
641
|
-
const asyncCompRegex = /\b(const|function)\s+[A-Z][a-zA-Z0-9_$]*\s*=\s*async\b|\basync\s+function\s+[A-Z][a-zA-Z0-9_$]*/g;
|
|
642
|
-
for (const match of segmentText.matchAll(asyncCompRegex)) {
|
|
643
|
-
addIssue('critical', 'No Async Component', 'React client components must not be async functions.', match.index);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// Rule 13: No non-null assertions (!.)
|
|
647
|
-
const nonNullRegex = /[a-zA-Z0-9_$]!\s*(\.|\[)/g;
|
|
648
|
-
for (const match of segmentText.matchAll(nonNullRegex)) {
|
|
649
|
-
addIssue('warning', 'No Non-null Assertion', 'Avoid using TypeScript non-null assertions (`!.`). Use optional chaining (`?.`) or defensive null checks instead.', match.index);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Rule 14: No direct state mutation
|
|
653
|
-
const stateMutationRegex = /\b(state|list|items|data)\.(push|pop|shift|unshift|splice|sort|reverse)\s*\(|\bstate\.[a-zA-Z0-9_$]+\s*=\s*/g;
|
|
654
|
-
for (const match of segmentText.matchAll(stateMutationRegex)) {
|
|
655
|
-
addIssue('warning', 'No Direct State Mutation', 'Do not mutate React state directly. Use immutable data patterns.', match.index);
|
|
656
|
-
}
|
|
567
|
+
const messages = {
|
|
568
|
+
'Lodash Get Required': 'Object property better use `lodash/get`: `get(obj, ...)`',
|
|
569
|
+
'No Any Type': 'TypeScript strictly prohibits using the `any` type.',
|
|
570
|
+
'No Hardcoded Strings': 'Do not use hardcoded string literals. Use constants or i18n.',
|
|
571
|
+
'No Inline Styles': 'Inline styles are strictly prohibited. Use CSS classes or styled components.',
|
|
572
|
+
'Clickable Element Tag': 'Clickable elements must use `<button>` or `<a>` tags (or Button/Link components). Do not bind onClick to non-clickable tags like div/span.',
|
|
573
|
+
'No Relative Paths': 'Relative module imports (`./` or `../`) are prohibited. Use absolute paths or path aliases.',
|
|
574
|
+
'No Debugger': 'Do not leave `debugger;` statements in code.',
|
|
575
|
+
'No Console Log': 'Do not leave `console.log` statements in code.',
|
|
576
|
+
'No Array Index Key': 'Avoid using array index (e.g., `key={index}`) as a React key in list rendering.',
|
|
577
|
+
'No Nested Components': 'Do not define React components inside another component body. Move component definitions outside.',
|
|
578
|
+
'No Async Component': 'React client components must not be async functions.',
|
|
579
|
+
'No Non-null Assertion': 'Avoid using TypeScript non-null assertions (`!.`). Use optional chaining (`?.`) or defensive null checks instead.',
|
|
580
|
+
'No Direct State Mutation': 'Do not mutate React state directly. Use immutable data patterns.',
|
|
581
|
+
'Unhandled Async Event': 'Async event handlers must wrap asynchronous calls in a try...catch block to handle errors.',
|
|
582
|
+
'No Magic Numbers': 'Avoid magic numbers in business logic. Define named constants or enums instead.'
|
|
583
|
+
};
|
|
657
584
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
585
|
+
for (const rule of Object.values(ruleSettings)) {
|
|
586
|
+
if (rule.enabled === false || !rule.pattern) continue;
|
|
587
|
+
let regex;
|
|
588
|
+
try {
|
|
589
|
+
// The built-in rule definition is the source of truth for matching.
|
|
590
|
+
regex = new RegExp(rule.pattern, 'gims');
|
|
591
|
+
} catch {
|
|
592
|
+
continue;
|
|
663
593
|
}
|
|
664
|
-
|
|
594
|
+
for (const match of segmentText.matchAll(regex)) {
|
|
595
|
+
const matchIdx = match.index;
|
|
596
|
+
const lineText = getLineInfo(matchIdx).text.trim();
|
|
597
|
+
|
|
598
|
+
if (rule.id === 'lodash-get') {
|
|
599
|
+
const objName = match[1];
|
|
600
|
+
const propName = match[3];
|
|
601
|
+
const globalNamespaces = ['Math', 'Object', 'Array', 'JSON', 'Promise', 'Reflect', 'Symbol', 'Date', 'Number', 'String', 'Boolean', 'React', 'process', 'e', 'evt', 'event', 'console', 'window', 'document', 'history', 'location', 'styles', 'style', 'req', 'res'];
|
|
602
|
+
if (objName === 'row' && propName === 'original') continue;
|
|
603
|
+
if (objName === 'original' && matchIdx >= 4 && segmentText.slice(matchIdx - 4, matchIdx).includes('row.')) continue;
|
|
604
|
+
if (globalNamespaces.includes(objName) || /^\s*(import|export|type|interface)\b/.test(lineText) || /\b(lodash\.)?get\s*\(/.test(lineText)) continue;
|
|
605
|
+
}
|
|
606
|
+
if ((rule.id === 'no-hardcoded-strings' || rule.id === 'no-nested-components') && /^\s*(import|export)\b/.test(lineText)) continue;
|
|
607
|
+
if (rule.id === 'unhandled-async-event' && !match[0].includes('try')) continue;
|
|
608
|
+
if (rule.id === 'no-magic-numbers' && /\b(200|201|204|400|401|403|404|500)\b/.test(match[0])) continue;
|
|
665
609
|
|
|
666
|
-
|
|
667
|
-
const magicNumberRegex = /(===|==|!==|!=)\s*([2-9]|\d{2,})\b|\bsetTimeout\s*\([^,]+,\s*([2-9]|\d{2,})\)/g;
|
|
668
|
-
for (const match of segmentText.matchAll(magicNumberRegex)) {
|
|
669
|
-
if (!/\b(200|201|204|400|401|403|404|500)\b/.test(match[0])) {
|
|
670
|
-
addIssue('info', 'No Magic Numbers', 'Avoid magic numbers in business logic. Define named constants or enums instead.', match.index);
|
|
610
|
+
addIssue(rule.severity, rule.name, messages[rule.name] || rule.description, matchIdx, rule.id === 'clickable-element-tag' || rule.id === 'no-relative-paths' || rule.id === 'unhandled-async-event' ? match[0] : undefined);
|
|
671
611
|
}
|
|
672
612
|
}
|
|
673
613
|
}
|
|
@@ -767,7 +707,7 @@ router.post('/check', async (req, res) => {
|
|
|
767
707
|
}
|
|
768
708
|
const fileIssues = analyzeDiff(filePath, hunks);
|
|
769
709
|
const content = buildDemoContent(filePath, hunks);
|
|
770
|
-
aiFiles.push({ filePath, type: 'MODIFY', content });
|
|
710
|
+
aiFiles.push({ filePath, type: 'MODIFY', content: formatDiffForAi(hunks) });
|
|
771
711
|
reviewFiles.push({ filePath, type: 'MODIFY', content, diffLines: getDiffLineMetadata(hunks), issues: fileIssues });
|
|
772
712
|
if (fileIssues.length > 0) {
|
|
773
713
|
totalIssues += fileIssues.length;
|
|
@@ -913,7 +853,7 @@ The pull request introduces feature updates across **${aiFiles.length} files**.
|
|
|
913
853
|
const fileIssues = analyzeDiff(filePath, hunks || []);
|
|
914
854
|
const contentCommit = change.type === 'DELETE' ? sinceCommit : untilCommit;
|
|
915
855
|
const content = await fetchFullFileContent({ host, projectKey, repositorySlug, filePath, commit: contentCommit, headers });
|
|
916
|
-
|
|
856
|
+
aiFiles.push({ filePath, type: change.type, content: formatDiffForAi(hunks || []) });
|
|
917
857
|
|
|
918
858
|
reviewFiles.push({ filePath, type: change.type, content: content || '', diffLines: getDiffLineMetadata(hunks), issues: fileIssues });
|
|
919
859
|
if (fileIssues.length > 0) {
|