git-repo-analyzer-test 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.github/copilot-instructions.md +108 -0
  2. package/.idea/aianalyzer.iml +9 -0
  3. package/.idea/misc.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/API_REFERENCE.md +244 -0
  7. package/ENHANCEMENTS.md +282 -0
  8. package/README.md +179 -0
  9. package/USAGE.md +189 -0
  10. package/analysis.txt +0 -0
  11. package/bin/cli.js +135 -0
  12. package/docs/SONARCLOUD_ANALYSIS_COVERED.md +144 -0
  13. package/docs/SonarCloud_Presentation_Points.md +81 -0
  14. package/docs/UI_IMPROVEMENTS.md +117 -0
  15. package/package-lock_cmd.json +542 -0
  16. package/package.json +44 -0
  17. package/package_command.json +16 -0
  18. package/public/analysis-options.json +31 -0
  19. package/public/images/README.txt +2 -0
  20. package/public/images/rws-logo.png +0 -0
  21. package/public/index.html +2433 -0
  22. package/repositories.example.txt +17 -0
  23. package/sample-repos.txt +20 -0
  24. package/src/analyzers/accessibility.js +47 -0
  25. package/src/analyzers/cicd-enhanced.js +113 -0
  26. package/src/analyzers/codeReview-enhanced.js +599 -0
  27. package/src/analyzers/codeReview-enhanced.js:Zone.Identifier +3 -0
  28. package/src/analyzers/codeReview.js +171 -0
  29. package/src/analyzers/codeReview.js:Zone.Identifier +3 -0
  30. package/src/analyzers/documentation-enhanced.js +137 -0
  31. package/src/analyzers/performance-enhanced.js +747 -0
  32. package/src/analyzers/performance-enhanced.js:Zone.Identifier +3 -0
  33. package/src/analyzers/performance.js +211 -0
  34. package/src/analyzers/performance.js:Zone.Identifier +3 -0
  35. package/src/analyzers/performance_cmd.js +216 -0
  36. package/src/analyzers/quality-enhanced.js +386 -0
  37. package/src/analyzers/quality-enhanced.js:Zone.Identifier +3 -0
  38. package/src/analyzers/quality.js +92 -0
  39. package/src/analyzers/quality.js:Zone.Identifier +3 -0
  40. package/src/analyzers/security-enhanced.js +512 -0
  41. package/src/analyzers/security-enhanced.js:Zone.Identifier +3 -0
  42. package/src/analyzers/snyk-ai.js:Zone.Identifier +3 -0
  43. package/src/analyzers/sonarcloud.js +928 -0
  44. package/src/analyzers/vulnerability.js +185 -0
  45. package/src/analyzers/vulnerability.js:Zone.Identifier +3 -0
  46. package/src/cli.js:Zone.Identifier +3 -0
  47. package/src/config.js +43 -0
  48. package/src/core/analyzerEngine.js +68 -0
  49. package/src/core/reportGenerator.js +21 -0
  50. package/src/gemini.js +321 -0
  51. package/src/github/client.js +124 -0
  52. package/src/github/client.js:Zone.Identifier +3 -0
  53. package/src/index.js +93 -0
  54. package/src/index_cmd.js +130 -0
  55. package/src/openai.js +297 -0
  56. package/src/report/generator.js +459 -0
  57. package/src/report/generator_cmd.js +459 -0
  58. package/src/report/pdf-generator.js +387 -0
  59. package/src/report/pdf-generator.js:Zone.Identifier +3 -0
  60. package/src/server.js +431 -0
  61. package/src/server.js:Zone.Identifier +3 -0
  62. package/src/server_cmd.js +434 -0
  63. package/src/sonarcloud/client.js +365 -0
  64. package/src/sonarcloud/scanner.js +171 -0
  65. package/src.zip +0 -0
@@ -0,0 +1,928 @@
1
+ /**
2
+ * SonarCloud Code Quality Analyzer (free plan compatible).
3
+ *
4
+ * SonarCloud free plan does NOT provide a single "Overall Code" API. We use only:
5
+ * - api/qualitygates/project_status → overall PASS/FAIL + conditions
6
+ * - api/measures/component → individual metrics (free-tier keys)
7
+ * - api/issues/search → issues list (optional)
8
+ * We combine these into our own "Overall Summary" for the UI.
9
+ *
10
+ * When project is not on SonarCloud (404), can optionally clone and run SonarScanner
11
+ * (set SONAR_RUN_SCANNER_IF_MISSING=true).
12
+ */
13
+
14
+ import { config } from '../config.js';
15
+ import { SonarCloudClient, FREE_TIER_METRIC_KEYS } from '../sonarcloud/client.js';
16
+ import { cloneAndScan } from '../sonarcloud/scanner.js';
17
+
18
+ /** Free-tier safe metrics (documented as accessible on free plan). Use first for reliable fetch. */
19
+ const CORE_METRIC_KEYS = [...FREE_TIER_METRIC_KEYS];
20
+ /** Extended metrics to request when available (may not all be returned on free plan). */
21
+ const DEFAULT_METRIC_KEYS = [
22
+ ...CORE_METRIC_KEYS,
23
+ 'security_hotspots',
24
+ 'security_hotspots_reviewed',
25
+ 'sqale_rating',
26
+ 'reliability_rating',
27
+ 'security_rating',
28
+ 'quality_gate_status',
29
+ 'complexity',
30
+ 'cognitive_complexity',
31
+ 'duplicated_blocks',
32
+ 'lines',
33
+ ];
34
+
35
+ export class SonarCloudAnalyzer {
36
+ constructor() {
37
+ this.client = new SonarCloudClient();
38
+ }
39
+
40
+ /**
41
+ * Analyze repository using SonarCloud API.
42
+ * Returns metrics, quality gate status, and issues for UI display.
43
+ * If token is missing or project not found, returns a graceful "not available" result.
44
+ */
45
+ async analyzeWithSonarCloud(owner, repo) {
46
+ try {
47
+ if (!this.client.token) {
48
+ return this.getUnavailableResult(owner, repo, 'SONAR_TOKEN not set in .env');
49
+ }
50
+
51
+ // 1) Derive project key from SONAR_ORGANIZATION + repo URL (no SONAR_PROJECT_KEY needed). Try fetch first.
52
+ const derivedKey = this.getScanProjectKey(owner, repo);
53
+ const initialResult = await this.fetchFullAnalysisData(derivedKey);
54
+ if (initialResult) {
55
+ this.logFetchedSonarCloudData(initialResult);
56
+ return initialResult;
57
+ }
58
+
59
+ // Run latest analysis every time user hits Analyze: use cached clone (clone if not, pull latest), run scanner, then wait for metrics.
60
+ const alwaysRunScan = (process.env.SONAR_ALWAYS_RUN_SCAN ?? 'true').toString().trim().toLowerCase();
61
+ const runScanFirst = alwaysRunScan !== 'false' && alwaysRunScan !== '0' && alwaysRunScan !== 'no';
62
+ if (runScanFirst) {
63
+ try {
64
+ console.warn(`📦 SonarCloud: running latest analysis (cached clone, pull, scan) for ${owner}/${repo}...`);
65
+ await this.runLatestScanOnly(owner, repo);
66
+ console.warn(`⏳ SonarCloud: scan uploaded. Waiting for metrics (poll up to ~3 min)...`);
67
+ const afterScan = await this.waitForMeasuresAfterScan(owner, repo);
68
+ if (afterScan?.component?.measures?.length) {
69
+ const keyToUse = afterScan.projectKey;
70
+ const [qualityGateData, issuesData] = await Promise.all([
71
+ this.client.getQualityGateStatus(keyToUse).catch(() => null),
72
+ this.client.getIssues(keyToUse, { ps: 100 }).catch(() => ({ issues: [], total: 0 })),
73
+ ]);
74
+ const metrics = this.parseMeasures(afterScan.component.measures);
75
+ const qualityGate = this.parseQualityGate(qualityGateData);
76
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0);
77
+ const score = this.calculateScore(metrics, qualityGate, issues);
78
+ const rating = this.getRating(score);
79
+ const result = this.buildSonarResult(keyToUse, metrics, qualityGate, issues, score, rating);
80
+ this.logFetchedSonarCloudData(result);
81
+ return result;
82
+ }
83
+ } catch (err) {
84
+ console.warn('SonarCloud run-latest-scan:', err.message);
85
+ }
86
+ }
87
+
88
+ // If we ran scan but waitForMeasuresAfterScan returned null, check again after a short delay (first-time scan may have just become ready)
89
+ if (runScanFirst) {
90
+ const scanKeyRetry = this.getScanProjectKey(owner, repo);
91
+ const branches = ['master', 'main', null];
92
+ const retryDelaysMs = [10000, 20000];
93
+ for (let i = 0; i < retryDelaysMs.length; i++) {
94
+ if (i > 0) await new Promise((r) => setTimeout(r, retryDelaysMs[i - 1]));
95
+ for (const branch of branches) {
96
+ for (const useBasic of [false, true]) {
97
+ try {
98
+ const data = await this.client.getMeasuresWithAuth(scanKeyRetry, DEFAULT_METRIC_KEYS, useBasic, branch);
99
+ if (data?.component?.measures?.length) {
100
+ const keyToUse = scanKeyRetry;
101
+ const [qualityGateData, issuesData] = await Promise.all([
102
+ this.client.getQualityGateStatus(keyToUse).catch(() => null),
103
+ this.client.getIssues(keyToUse, { ps: 100 }).catch(() => ({ issues: [], total: 0 })),
104
+ ]);
105
+ const metrics = this.parseMeasures(data.component.measures);
106
+ const qualityGate = this.parseQualityGate(qualityGateData);
107
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0);
108
+ const score = this.calculateScore(metrics, qualityGate, issues);
109
+ const rating = this.getRating(score);
110
+ const result = this.buildSonarResult(keyToUse, metrics, qualityGate, issues, score, rating);
111
+ this.logFetchedSonarCloudData(result);
112
+ return result;
113
+ }
114
+ } catch (_) {}
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // Project key is always derived from SONAR_ORGANIZATION + repo (getScanProjectKey / resolveProjectKey).
121
+ const scanKey = this.getScanProjectKey(owner, repo);
122
+ const projectKey = runScanFirst ? scanKey : await this.client.resolveProjectKey(owner, repo);
123
+ const candidatesTried = this.client.getProjectKeyCandidates(owner, repo);
124
+ const explicitKey = (config.sonarProjectKey || '').trim() && !/^[a-f0-9]{32,64}$/i.test((config.sonarProjectKey || '').trim())
125
+ ? (config.sonarProjectKey || '').replace(/^["']|["']$/g, '').trim()
126
+ : null;
127
+
128
+ const captureError = (e) => ({ component: null, error: e?.message || String(e), status: e?.responseStatus ?? e?.response?.status });
129
+
130
+ // 2) Try integrated full analysis (measures + quality gate + issues + score) for resolved project key
131
+ const fullResult = await this.fetchFullAnalysisData(projectKey);
132
+ if (fullResult) {
133
+ this.logFetchedSonarCloudData(fullResult);
134
+ return fullResult;
135
+ }
136
+ // Try each candidate key if main projectKey did not return data
137
+ for (const key of candidatesTried) {
138
+ if (key === projectKey) continue;
139
+ const candidateResult = await this.fetchFullAnalysisData(key);
140
+ if (candidateResult) {
141
+ this.logFetchedSonarCloudData(candidateResult);
142
+ return candidateResult;
143
+ }
144
+ }
145
+
146
+ let measuresData = await this.client.getMeasures(projectKey, DEFAULT_METRIC_KEYS).catch(captureError);
147
+ let component = measuresData?.component;
148
+
149
+ // If we got 404, try with branch (main/master) and without auth (public projects)
150
+ if (measuresData?.status === 404) {
151
+ for (const branch of ['master', 'main', null]) {
152
+ for (const useBasic of [false, true]) {
153
+ try {
154
+ const data = await this.client.getMeasuresWithAuth(projectKey, DEFAULT_METRIC_KEYS, useBasic, branch);
155
+ if (data?.component?.measures?.length) {
156
+ measuresData = { component: data.component };
157
+ component = data.component;
158
+ break;
159
+ }
160
+ } catch (_) {}
161
+ }
162
+ if (component?.measures?.length) break;
163
+ }
164
+ if (!component?.measures?.length) {
165
+ for (const branch of ['master', 'main', null]) {
166
+ try {
167
+ const data = await this.client.getMeasuresWithoutAuth(projectKey, DEFAULT_METRIC_KEYS, branch);
168
+ if (data?.component?.measures?.length) {
169
+ measuresData = { component: data.component };
170
+ component = data.component;
171
+ break;
172
+ }
173
+ } catch (_) {}
174
+ }
175
+ }
176
+ }
177
+ // If still 404 and we just ran a scan, SonarCloud may still be indexing — retry with longer delays (2–4 min total)
178
+ if (measuresData?.status === 404 && runScanFirst && projectKey === scanKey) {
179
+ const retryDelays = [15000, 30000, 45000, 60000]; // 15s, 30s, 45s, 60s — SonarCloud often needs 2–4 min after upload
180
+ for (let r = 0; r < retryDelays.length; r++) {
181
+ console.warn(`⏳ SonarCloud: project not ready (404), retrying in ${retryDelays[r] / 1000}s (${r + 1}/${retryDelays.length})...`);
182
+ await new Promise((res) => setTimeout(res, retryDelays[r]));
183
+ for (const branch of ['master', 'main', null]) {
184
+ try {
185
+ const data = await this.client.getMeasures(scanKey, DEFAULT_METRIC_KEYS, branch);
186
+ if (data?.component?.measures?.length) {
187
+ measuresData = { component: data.component };
188
+ component = data.component;
189
+ break;
190
+ }
191
+ } catch (_) {}
192
+ for (const useBasic of [false, true]) {
193
+ try {
194
+ const data = await this.client.getMeasuresWithAuth(scanKey, DEFAULT_METRIC_KEYS, useBasic, branch);
195
+ if (data?.component?.measures?.length) {
196
+ measuresData = { component: data.component };
197
+ component = data.component;
198
+ break;
199
+ }
200
+ } catch (_) {}
201
+ }
202
+ if (component?.measures?.length) break;
203
+ }
204
+ if (component?.measures?.length) break;
205
+ measuresData = await this.client.getMeasures(scanKey, DEFAULT_METRIC_KEYS).catch(captureError);
206
+ component = measuresData?.component;
207
+ if (component?.measures?.length) break;
208
+ }
209
+ }
210
+
211
+ if (component && !component?.measures?.length) {
212
+ for (let r = 0; r < 6; r++) {
213
+ await new Promise((res) => setTimeout(res, 30000));
214
+ measuresData = await this.client.getMeasures(projectKey, DEFAULT_METRIC_KEYS).catch(captureError);
215
+ component = measuresData?.component;
216
+ if (component?.measures?.length) break;
217
+ }
218
+ }
219
+ const keyForFetch = component?.key || projectKey;
220
+ let qualityGateData = await this.client.getQualityGateStatus(keyForFetch).catch(() => null);
221
+ let issuesData = await this.client.getIssues(keyForFetch, { ps: 100 }).catch(() => ({ issues: [], total: 0 }));
222
+
223
+ if (measuresData?.error && !measuresData?.component) {
224
+ const errMsg = measuresData.error || '';
225
+ const status = measuresData.status;
226
+ const is401 = status === 401 || errMsg.includes('401');
227
+ const is404 = status === 404 || errMsg.includes('404');
228
+ if (is401) {
229
+ return this.getUnavailableResult(
230
+ owner,
231
+ repo,
232
+ 'SonarCloud returned 401 Unauthorized. Generate a new token at https://sonarcloud.io/account/security and set SONAR_TOKEN in .env (no quotes). Tokens can expire or be revoked.'
233
+ );
234
+ }
235
+ // Scenario B: Analysis visible on SonarCloud but API returns 404 — try project search, then measures with auth + branch.
236
+ if (is404 && explicitKey) {
237
+ const orgForSearch = (config.sonarOrgKey || owner).replace(/^["'\s]+|["'\s]+$/g, '').trim().toLowerCase();
238
+ const projects = await this.client.searchProjects(orgForSearch);
239
+ const match = projects.find((p) => p.key === explicitKey || p.key.toLowerCase() === explicitKey.toLowerCase() || (p.key.includes(repo) || (p.name && p.name.toLowerCase().includes(repo.toLowerCase()))));
240
+ const keyToTry = match ? match.key : explicitKey;
241
+ const branchesToTry = ['master', 'main', null];
242
+ for (const branch of branchesToTry) {
243
+ for (const useBasic of [false, true]) {
244
+ try {
245
+ const retryMeasures = await this.client.getMeasuresWithAuth(keyToTry, DEFAULT_METRIC_KEYS, useBasic, branch);
246
+ if (retryMeasures?.component?.measures?.length) {
247
+ const comp = retryMeasures.component;
248
+ const [qg, iss] = await Promise.all([
249
+ this.client.getQualityGateStatus(keyToTry, branch).catch(() => null),
250
+ this.client.getIssues(keyToTry, { ps: 100, branch }).catch(() => ({ issues: [], total: 0 })),
251
+ ]);
252
+ const metrics = this.parseMeasures(comp.measures);
253
+ const qualityGate = this.parseQualityGate(qg);
254
+ const issues = this.parseIssues(iss?.issues || [], iss?.total ?? 0);
255
+ const score = this.calculateScore(metrics, qualityGate, issues);
256
+ const rating = this.getRating(score);
257
+ const result = this.buildSonarResult(keyToTry, metrics, qualityGate, issues, score, rating);
258
+ this.logFetchedSonarCloudData(result);
259
+ return result;
260
+ }
261
+ } catch (_) {}
262
+ }
263
+ }
264
+ // One more attempt with integrated fetch (master/main first) before returning empty
265
+ const lastTry = await this.fetchFullAnalysisData(explicitKey);
266
+ if (lastTry) {
267
+ this.logFetchedSonarCloudData(lastTry);
268
+ return lastTry;
269
+ }
270
+ const link = `https://sonarcloud.io/project/overview?id=${encodeURIComponent(explicitKey)}`;
271
+ const partialResult = {
272
+ source: 'SONARCLOUD',
273
+ available: true,
274
+ projectKey: explicitKey,
275
+ score: null,
276
+ rating: null,
277
+ qualityGate: { status: null, conditions: [] },
278
+ metrics: {},
279
+ issues: { total: 0, items: [] },
280
+ recommendations: [],
281
+ viewOnSonarCloud: link,
282
+ };
283
+ this.logFetchedSonarCloudData(partialResult);
284
+ return partialResult;
285
+ }
286
+ const runScannerEnv = (process.env.SONAR_RUN_SCANNER_IF_MISSING || '').toString().trim().toLowerCase();
287
+ const runScanner = runScannerEnv === 'true' || runScannerEnv === '1' || runScannerEnv === 'yes';
288
+ let scannerAttempted = false;
289
+ let scannerError = null;
290
+ if (is404 && runScanner) {
291
+ scannerAttempted = true;
292
+ const { result: scanResult, error: scanErr } = await this.runScannerAndFetch(owner, repo, candidatesTried[0]);
293
+ if (scanErr) scannerError = scanErr;
294
+ if (scanResult) return scanResult;
295
+ }
296
+ let hint;
297
+ if (is404 && scannerAttempted && scannerError) {
298
+ if (scannerError.includes('No organization with key')) {
299
+ hint = `SonarCloud: no organization with that key. Set SONAR_ORGANIZATION in .env to your exact org key from https://sonarcloud.io/organizations (sign in → open your org → copy the key from the URL). Use lowercase.`;
300
+ } else {
301
+ hint = `Scanner ran but failed. ${scannerError}`;
302
+ }
303
+ } else if (is404 && scannerAttempted) {
304
+ hint = `Scanner ran but failed (timed out). Check server console.`;
305
+ } else if (is404) {
306
+ hint = `Project not found (tried: ${candidatesTried.join(', ')}). Add this repo at sonarcloud.io, or set SONAR_RUN_SCANNER_IF_MISSING=true in .env to clone and scan automatically.`;
307
+ } else {
308
+ hint = measuresData.error;
309
+ }
310
+ for (const keyToTry of [scanKey, projectKey].filter(Boolean)) {
311
+ try {
312
+ const fallback = await this.fetchReportByProjectKey(keyToTry);
313
+ if (fallback?.available && fallback?.metrics && (fallback.metrics.ncloc != null || fallback.metrics.lines != null)) return fallback;
314
+ } catch (_) {}
315
+ }
316
+ const unavail = this.getUnavailableResult(owner, repo, hint);
317
+ if (scanKey) unavail.viewOnSonarCloud = `https://sonarcloud.io/project/overview?id=${encodeURIComponent(scanKey)}`;
318
+ return unavail;
319
+ }
320
+
321
+ if (!component?.measures?.length) {
322
+ for (const keyToTry of [scanKey, keyForFetch || projectKey].filter(Boolean)) {
323
+ try {
324
+ const fallback = await this.fetchReportByProjectKey(keyToTry);
325
+ if (fallback?.available && fallback?.metrics && (fallback.metrics.ncloc != null || fallback.metrics.lines != null)) return fallback;
326
+ } catch (_) {}
327
+ }
328
+ const unavail = this.getUnavailableResult(owner, repo, 'No measures returned for this project. If the project was just scanned, wait 1–2 min and run the analysis again.');
329
+ if (scanKey) unavail.viewOnSonarCloud = `https://sonarcloud.io/project/overview?id=${encodeURIComponent(scanKey)}`;
330
+ return unavail;
331
+ }
332
+
333
+ const metrics = this.parseMeasures(component.measures);
334
+ const qualityGate = this.parseQualityGate(qualityGateData);
335
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0);
336
+ const score = this.calculateScore(metrics, qualityGate, issues);
337
+ const rating = this.getRating(score);
338
+
339
+ const result = this.buildSonarResult(keyForFetch, metrics, qualityGate, issues, score, rating);
340
+ this.logFetchedSonarCloudData(result);
341
+ return result;
342
+ } catch (error) {
343
+ console.warn('SonarCloud analysis error:', error.message);
344
+ return this.getUnavailableResult(owner, repo, error.message);
345
+ }
346
+ }
347
+
348
+ parseMeasures(measures) {
349
+ const map = {};
350
+ for (const m of measures || []) {
351
+ map[m.metric] = m.value ?? m.period?.value ?? null;
352
+ }
353
+ return {
354
+ ncloc: parseInt(map.ncloc, 10) || 0,
355
+ lines: parseInt(map.lines, 10) || 0,
356
+ bugs: parseInt(map.bugs, 10) || 0,
357
+ vulnerabilities: parseInt(map.vulnerabilities, 10) || 0,
358
+ codeSmells: parseInt(map.code_smells, 10) || 0,
359
+ securityHotspots: parseInt(map.security_hotspots, 10) || 0,
360
+ securityHotspotsReviewed: parseFloat(map.security_hotspots_reviewed) != null ? parseFloat(map.security_hotspots_reviewed) : null,
361
+ duplicatedLinesDensity: parseFloat(map.duplicated_lines_density) || 0,
362
+ coverage: parseFloat(map.coverage) != null ? parseFloat(map.coverage) : null,
363
+ complexity: parseInt(map.complexity, 10) || 0,
364
+ cognitiveComplexity: parseInt(map.cognitive_complexity, 10) || 0,
365
+ duplicatedBlocks: parseInt(map.duplicated_blocks, 10) || 0,
366
+ // A=1, B=2, C=3, D=4, E=5
367
+ sqaleRating: map.sqale_rating || null,
368
+ reliabilityRating: map.reliability_rating || null,
369
+ securityRating: map.security_rating || null,
370
+ qualityGateStatus: map.quality_gate_status || null,
371
+ };
372
+ }
373
+
374
+ parseQualityGate(data) {
375
+ if (!data?.projectStatus) return { status: null, conditions: [] };
376
+ const status = data.projectStatus.status; // OK, ERROR, NONE
377
+ const conditions = (data.projectStatus.conditions || []).map((c) => ({
378
+ metricKey: c.metricKey,
379
+ status: c.status,
380
+ operator: c.operator,
381
+ value: c.value,
382
+ errorThreshold: c.errorThreshold,
383
+ }));
384
+ return { status, conditions };
385
+ }
386
+
387
+ parseIssues(issues, total, maxItems = 100) {
388
+ return {
389
+ total,
390
+ items: (issues || []).slice(0, maxItems).map((i) => ({
391
+ key: i.key,
392
+ type: i.type,
393
+ severity: i.severity,
394
+ message: i.message,
395
+ component: i.component,
396
+ line: i.line,
397
+ rule: i.rule,
398
+ effort: i.effort,
399
+ })),
400
+ };
401
+ }
402
+
403
+ getRating(score) {
404
+ if (score >= 9) return 'A+';
405
+ if (score >= 8) return 'A';
406
+ if (score >= 7) return 'B+';
407
+ if (score >= 6) return 'B';
408
+ if (score >= 5) return 'C+';
409
+ if (score >= 4) return 'C';
410
+ return 'F';
411
+ }
412
+
413
+ /**
414
+ * Fetch full SonarCloud analysis: measures, quality gate, issues; compute score/rating and recommendations.
415
+ * Tries with and without branch (main, master). Use this for "overall" analysis and to avoid duplicated fetch logic.
416
+ * @param {string} projectKey - SonarCloud project key
417
+ * @param {{ issuesPageSize?: number }} [options] - issuesPageSize default 100
418
+ * @returns {Promise<object|null>} Full result object or null if no measures
419
+ */
420
+ async fetchFullAnalysisData(projectKey, options = {}) {
421
+ const issuesPageSize = options.issuesPageSize ?? 100;
422
+ const branches = ['master', 'main', null];
423
+
424
+ for (const branch of branches) {
425
+ // 1) Free-plan path: only qualitygates/project_status + measures/component (core metrics)
426
+ const freeTier = await this.client.fetchQualityGateAndMeasures(projectKey, branch);
427
+ if (freeTier.measures?.length) {
428
+ const metrics = this.parseMeasures(freeTier.measures);
429
+ const qualityGate = this.parseQualityGate(freeTier.qualityGate);
430
+ const issuesData = await this.client.getIssues(projectKey, { ps: issuesPageSize, branch }).catch(() => ({ issues: [], total: 0 }));
431
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0, issuesPageSize);
432
+ const score = this.calculateScore(metrics, qualityGate, issues);
433
+ const rating = this.getRating(score);
434
+ const result = this.buildSonarResult(projectKey, metrics, qualityGate, issues, score, rating);
435
+ return result;
436
+ }
437
+
438
+ // 2) Fallback: full measures API (may include extra metrics when available)
439
+ let component = null;
440
+ try {
441
+ const data = await this.client.getMeasures(projectKey, CORE_METRIC_KEYS, branch);
442
+ if (data?.component?.measures?.length) component = data.component;
443
+ } catch (_) {}
444
+ if (!component) {
445
+ try {
446
+ const data = await this.client.getMeasures(projectKey, DEFAULT_METRIC_KEYS, branch);
447
+ if (data?.component?.measures?.length) component = data.component;
448
+ } catch (_) {}
449
+ }
450
+ if (!component) {
451
+ for (const useBasic of [false, true]) {
452
+ try {
453
+ const data = await this.client.getMeasuresWithAuth(projectKey, CORE_METRIC_KEYS, useBasic, branch);
454
+ if (data?.component?.measures?.length) { component = data.component; break; }
455
+ } catch (_) {}
456
+ }
457
+ }
458
+ if (!component) {
459
+ for (const useBasic of [false, true]) {
460
+ try {
461
+ const data = await this.client.getMeasuresWithAuth(projectKey, DEFAULT_METRIC_KEYS, useBasic, branch);
462
+ if (data?.component?.measures?.length) { component = data.component; break; }
463
+ } catch (_) {}
464
+ }
465
+ }
466
+ if (!component) {
467
+ try {
468
+ const data = await this.client.getMeasuresWithoutAuth(projectKey, CORE_METRIC_KEYS, branch);
469
+ if (data?.component?.measures?.length) component = data.component;
470
+ } catch (_) {}
471
+ }
472
+ if (!component?.measures?.length) continue;
473
+
474
+ const keyUsed = component.key || projectKey;
475
+ const [qualityGateData, issuesData] = await Promise.all([
476
+ this.client.getQualityGateStatus(keyUsed, branch).catch(() => null),
477
+ this.client.getIssues(keyUsed, { ps: issuesPageSize, branch }).catch(() => ({ issues: [], total: 0 })),
478
+ ]);
479
+ const metrics = this.parseMeasures(component.measures);
480
+ const qualityGate = this.parseQualityGate(qualityGateData);
481
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0, issuesPageSize);
482
+ const score = this.calculateScore(metrics, qualityGate, issues);
483
+ const rating = this.getRating(score);
484
+ const result = this.buildSonarResult(keyUsed, metrics, qualityGate, issues, score, rating);
485
+ return result;
486
+ }
487
+ return null;
488
+ }
489
+
490
+ /**
491
+ * Build the SonarCloud result object for the final analysis result.
492
+ * Structures the response in an "analysis" way: summary, metrics, qualityGate, issues, recommendations.
493
+ * Free plan: we combine quality gate status + individual metrics; no single "Overall Code" API.
494
+ */
495
+ buildSonarResult(projectKey, metrics, qualityGate, issues, score, rating) {
496
+ const status = qualityGate?.status === 'OK' ? 'Passed' : qualityGate?.status === 'ERROR' ? 'Failed' : 'Unknown';
497
+ const recommendations = this.generateRecommendations(metrics, qualityGate, issues);
498
+ const viewOnSonarCloud = `https://sonarcloud.io/project/overview?id=${encodeURIComponent(projectKey)}`;
499
+ const overallSummary = {
500
+ status,
501
+ metrics: {
502
+ bugs: metrics.bugs ?? 0,
503
+ vulnerabilities: metrics.vulnerabilities ?? 0,
504
+ codeSmells: metrics.codeSmells ?? 0,
505
+ coverage: metrics.coverage != null ? metrics.coverage : null,
506
+ duplicatedLinesDensity: metrics.duplicatedLinesDensity ?? 0,
507
+ ncloc: metrics.ncloc ?? metrics.lines ?? 0,
508
+ },
509
+ };
510
+ return {
511
+ source: 'SONARCLOUD',
512
+ available: true,
513
+ projectKey,
514
+ score,
515
+ rating,
516
+ qualityGate,
517
+ metrics,
518
+ issues,
519
+ recommendations,
520
+ viewOnSonarCloud,
521
+ overallSummary,
522
+ /** Analysis-style structure for final result (summary → details → recommendations) */
523
+ analysis: {
524
+ summary: {
525
+ score,
526
+ rating,
527
+ qualityGateStatus: qualityGate?.status ?? null,
528
+ overallStatus: status,
529
+ projectKey,
530
+ viewOnSonarCloud,
531
+ },
532
+ metrics,
533
+ qualityGate,
534
+ issues,
535
+ recommendations,
536
+ overallSummary,
537
+ },
538
+ };
539
+ }
540
+
541
+ /**
542
+ * Log a summary of data fetched from SonarCloud to the console.
543
+ * Call when we have successfully retrieved metrics, quality gate, or issues.
544
+ */
545
+ logFetchedSonarCloudData(result) {
546
+ if (!result || result.source !== 'SONARCLOUD') return;
547
+ const prefix = '\n📊 SonarCloud data fetched';
548
+ console.log(prefix + ' — project:', result.projectKey || '—');
549
+ if (result.qualityGate) {
550
+ console.log(' Quality gate:', result.qualityGate.status || '—', result.qualityGate.conditions?.length ? `(${result.qualityGate.conditions.length} conditions)` : '');
551
+ }
552
+ if (result.metrics && Object.keys(result.metrics).length) {
553
+ const m = result.metrics;
554
+ const parts = [];
555
+ if (m.ncloc != null) parts.push(`lines of code: ${m.ncloc}`);
556
+ if (m.bugs != null) parts.push(`bugs: ${m.bugs}`);
557
+ if (m.vulnerabilities != null) parts.push(`vulnerabilities: ${m.vulnerabilities}`);
558
+ if (m.codeSmells != null) parts.push(`code smells: ${m.codeSmells}`);
559
+ if (m.coverage != null) parts.push(`coverage: ${m.coverage}%`);
560
+ if (m.duplicatedLinesDensity != null) parts.push(`duplication: ${m.duplicatedLinesDensity}%`);
561
+ if (parts.length) console.log(' Metrics:', parts.join(' | '));
562
+ }
563
+ if (result.issues && (result.issues.total > 0 || (result.issues.items && result.issues.items.length))) {
564
+ console.log(' Issues:', result.issues.total ?? result.issues.items?.length ?? 0, 'total (showing up to 50)');
565
+ }
566
+ if (result.score != null) console.log(' Score:', result.score, '| Rating:', result.rating || '—');
567
+ if (result.viewOnSonarCloud) console.log(' View:', result.viewOnSonarCloud);
568
+ console.log('');
569
+ }
570
+
571
+ calculateScore(metrics, qualityGate, issues) {
572
+ let score = 10;
573
+ // Deduct for bugs
574
+ score -= Math.min(2, (metrics.bugs || 0) * 0.5);
575
+ // Deduct for vulnerabilities
576
+ score -= Math.min(3, (metrics.vulnerabilities || 0) * 1);
577
+ // Deduct for code smells (capped)
578
+ score -= Math.min(1.5, (metrics.codeSmells || 0) / 500);
579
+ // Quality gate failed
580
+ if (qualityGate.status === 'ERROR') score -= 2;
581
+ // Reliability/security ratings (1=A, 5=E)
582
+ const rel = parseInt(metrics.reliabilityRating, 10) || 1;
583
+ const sec = parseInt(metrics.securityRating, 10) || 1;
584
+ score -= (rel - 1) * 0.3 + (sec - 1) * 0.5;
585
+ // Coverage bonus (if present)
586
+ if (metrics.coverage != null && metrics.coverage >= 80) score = Math.min(10, score + 0.5);
587
+ return Math.max(0, Math.min(10, Math.round(score * 10) / 10));
588
+ }
589
+
590
+ generateRecommendations(metrics, qualityGate, issues) {
591
+ const recs = [];
592
+ if (qualityGate.status === 'ERROR') {
593
+ recs.push({ priority: 'HIGH', category: 'Quality Gate', action: 'Fix failing quality gate conditions on SonarCloud to improve code quality.' });
594
+ }
595
+ if ((metrics.bugs || 0) > 0) {
596
+ recs.push({ priority: 'HIGH', category: 'Bugs', action: `Address ${metrics.bugs} bug(s) reported by SonarCloud.` });
597
+ }
598
+ if ((metrics.vulnerabilities || 0) > 0) {
599
+ recs.push({ priority: 'HIGH', category: 'Security', action: `Remediate ${metrics.vulnerabilities} vulnerability(ies) reported by SonarCloud.` });
600
+ }
601
+ if ((metrics.codeSmells || 0) > 50) {
602
+ recs.push({ priority: 'MEDIUM', category: 'Maintainability', action: `Reduce code smells (${metrics.codeSmells}) to improve maintainability.` });
603
+ }
604
+ if (metrics.coverage != null && metrics.coverage < 80) {
605
+ recs.push({ priority: 'MEDIUM', category: 'Coverage', action: `Increase test coverage from ${metrics.coverage}% toward 80% or higher.` });
606
+ }
607
+ if ((metrics.duplicatedLinesDensity || 0) > 5) {
608
+ recs.push({ priority: 'LOW', category: 'Duplication', action: `Reduce duplicated lines (${metrics.duplicatedLinesDensity}%) to improve clarity.` });
609
+ }
610
+ return recs;
611
+ }
612
+
613
+ /** Project key used by the scanner (must match when fetching after a fresh scan). */
614
+ getScanProjectKey(owner, repo) {
615
+ const orgRaw = (config.sonarOrgKey && !/^[a-f0-9]{32,64}$/i.test(config.sonarOrgKey))
616
+ ? config.sonarOrgKey
617
+ : owner;
618
+ const organization = orgRaw.trim().toLowerCase();
619
+ return `${organization}_${repo}`.replace(/\//g, '-');
620
+ }
621
+
622
+ /**
623
+ * Run latest SonarCloud analysis only: create project if needed, get-or-clone repo, pull latest, run scanner.
624
+ * Used when SONAR_ALWAYS_RUN_SCAN=true so every Analyze runs latest analysis then fetch.
625
+ */
626
+ async runLatestScanOnly(owner, repo, options = {}) {
627
+ const repoUrl = `https://github.com/${owner}/${repo}`;
628
+ const orgRaw = (config.sonarOrgKey && !/^[a-f0-9]{32,64}$/i.test(config.sonarOrgKey))
629
+ ? config.sonarOrgKey
630
+ : owner;
631
+ const organization = orgRaw.trim().toLowerCase();
632
+ const projectKey = `${organization}_${repo}`.replace(/\//g, '-');
633
+ const projectName = `${owner}/${repo}`;
634
+ await this.client.createProject(projectKey, projectName, organization);
635
+ await cloneAndScan(repoUrl, projectKey, projectName, organization, this.client.token);
636
+ await new Promise((r) => setTimeout(r, 5000));
637
+ if (options.makePublic !== false) {
638
+ await this.client.updateProjectVisibility(projectKey, 'public');
639
+ }
640
+ }
641
+
642
+ /**
643
+ * After a fresh scan, SonarCloud can take several minutes to process. Poll for measures with the exact scan
644
+ * project key (and branches). First-time scans: poll more frequently for the first 2 minutes so results
645
+ * appear as soon as SonarCloud is ready; then use the configured interval.
646
+ * Configure: SONAR_METRICS_WAIT_MAX_MINUTES (default 5, min 3), SONAR_METRICS_WAIT_INTERVAL_SEC (default 30),
647
+ * SONAR_METRICS_WAIT_FAST_SEC (default 12) — interval for the first 2 minutes.
648
+ * @returns {Promise<{component: object, projectKey: string} | null>}
649
+ */
650
+ async waitForMeasuresAfterScan(owner, repo) {
651
+ const scanKey = this.getScanProjectKey(owner, repo);
652
+ const branches = ['master', 'main', null];
653
+ const rawMinutes = parseInt(process.env.SONAR_METRICS_WAIT_MAX_MINUTES || '5', 10) || 5;
654
+ const maxMinutes = Math.min(15, Math.max(3, rawMinutes));
655
+ const intervalSec = Math.min(120, Math.max(15, parseInt(process.env.SONAR_METRICS_WAIT_INTERVAL_SEC || '30', 10) || 30));
656
+ const fastSec = Math.min(20, Math.max(8, parseInt(process.env.SONAR_METRICS_WAIT_FAST_SEC || '12', 10) || 12));
657
+ const fastPhaseMinutes = 2;
658
+ const fastPhaseAttempts = Math.max(4, Math.ceil((fastPhaseMinutes * 60) / fastSec));
659
+ const slowPhaseTotalSec = Math.max(0, (maxMinutes - fastPhaseMinutes) * 60);
660
+ const slowAttempts = Math.max(2, Math.floor(slowPhaseTotalSec / intervalSec));
661
+ const maxAttempts = fastPhaseAttempts + slowAttempts;
662
+ const getWaitMs = (attempt) => {
663
+ if (attempt <= 0) return 0;
664
+ return (attempt <= fastPhaseAttempts ? fastSec : intervalSec) * 1000;
665
+ };
666
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
667
+ if (attempt > 0) {
668
+ const waitSec = attempt <= fastPhaseAttempts ? fastSec : intervalSec;
669
+ console.warn(`⏳ SonarCloud: checking again for metrics (attempt ${attempt + 1}/${maxAttempts}, next in ${waitSec}s, up to ${maxMinutes} min)...`);
670
+ await new Promise((r) => setTimeout(r, getWaitMs(attempt)));
671
+ }
672
+ for (const branch of branches) {
673
+ try {
674
+ const freeTier = await this.client.fetchQualityGateAndMeasures(scanKey, branch);
675
+ if (freeTier?.measures?.length) {
676
+ const component = { key: scanKey, measures: freeTier.measures };
677
+ return { component, projectKey: scanKey };
678
+ }
679
+ } catch (_) {}
680
+ try {
681
+ const data = await this.client.getMeasures(scanKey, DEFAULT_METRIC_KEYS, branch);
682
+ if (data?.component?.measures?.length) return { component: data.component, projectKey: scanKey };
683
+ } catch (_) {}
684
+ for (const useBasic of [false, true]) {
685
+ try {
686
+ const data = await this.client.getMeasuresWithAuth(scanKey, DEFAULT_METRIC_KEYS, useBasic, branch);
687
+ if (data?.component?.measures?.length) return { component: data.component, projectKey: scanKey };
688
+ } catch (_) {}
689
+ }
690
+ }
691
+ }
692
+ return null;
693
+ }
694
+
695
+ /**
696
+ * Get-or-clone repo (cache), run SonarScanner, wait for SonarCloud to process, then fetch and return results.
697
+ * Returns null on failure so caller can show unavailable message.
698
+ */
699
+ async runScannerAndFetch(owner, repo, _candidateKey) {
700
+ const repoUrl = `https://github.com/${owner}/${repo}`;
701
+ const orgRaw = (process.env.SONAR_ORGANIZATION && !/^[a-f0-9]{32,64}$/i.test(process.env.SONAR_ORGANIZATION))
702
+ ? process.env.SONAR_ORGANIZATION
703
+ : owner;
704
+ const organization = orgRaw.trim().toLowerCase();
705
+ const projectKey = `${organization}_${repo}`.replace(/\//g, '-');
706
+ const projectName = `${owner}/${repo}`;
707
+ try {
708
+ console.warn(`📦 SonarCloud: project not found. Using cached clone or cloning ${repoUrl}, running SonarScanner (org: ${organization})...`);
709
+ await cloneAndScan(repoUrl, projectKey, projectName, organization, this.client.token);
710
+ await new Promise((r) => setTimeout(r, 5000));
711
+ await this.client.updateProjectVisibility(projectKey, 'public');
712
+ console.warn(`⏳ SonarCloud: scan uploaded. Waiting for processing (up to ~3 min)...`);
713
+ await this.waitForProject(projectKey);
714
+ let resolvedKey = projectKey;
715
+ const projects = await this.client.searchProjects(organization);
716
+ const match = projects.find(
717
+ (p) => p.key === projectKey || p.key.toLowerCase().endsWith(repo.toLowerCase()) || p.key.includes(repo)
718
+ );
719
+ if (match && match.key !== projectKey) {
720
+ resolvedKey = match.key;
721
+ console.warn(`📊 SonarCloud: using project key ${resolvedKey}`);
722
+ }
723
+ let measuresData = await this.client.getMeasures(resolvedKey, DEFAULT_METRIC_KEYS).catch((e) => ({ component: null, error: e.message }));
724
+ let component = measuresData?.component;
725
+ const retryDelays = [90000, 90000, 120000];
726
+ for (let i = 0; i < retryDelays.length && (!component?.measures?.length); i++) {
727
+ console.warn(`⏳ SonarCloud: measures not ready, retrying in ${retryDelays[i] / 1000}s...`);
728
+ await new Promise((r) => setTimeout(r, retryDelays[i]));
729
+ measuresData = await this.client.getMeasures(resolvedKey, DEFAULT_METRIC_KEYS).catch((e) => ({ component: null, error: e.message }));
730
+ component = measuresData?.component;
731
+ }
732
+ if (!component?.measures?.length) {
733
+ const link = `https://sonarcloud.io/project/overview?id=${encodeURIComponent(resolvedKey)}`;
734
+ return {
735
+ result: null,
736
+ error: `Measures not ready after retries. (1) Wait 2–3 min. (2) Run the same analysis again (same repo, click Analyze). (3) Ensure SONAR_ORGANIZATION in .env matches your SonarCloud org so project key ${resolvedKey} is correct. Project: ${link}`,
737
+ };
738
+ }
739
+ const [qualityGateData, issuesData] = await Promise.all([
740
+ this.client.getQualityGateStatus(resolvedKey).catch(() => null),
741
+ this.client.getIssues(resolvedKey, { ps: 100 }).catch(() => ({ issues: [], total: 0 })),
742
+ ]);
743
+ const metrics = this.parseMeasures(component.measures);
744
+ const qualityGate = this.parseQualityGate(qualityGateData);
745
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0);
746
+ const score = this.calculateScore(metrics, qualityGate, issues);
747
+ const rating = this.getRating(score);
748
+ const scanResult = this.buildSonarResult(resolvedKey, metrics, qualityGate, issues, score, rating);
749
+ this.logFetchedSonarCloudData(scanResult);
750
+ return {
751
+ result: scanResult,
752
+ error: null,
753
+ };
754
+ } catch (err) {
755
+ console.warn('SonarCloud scanner error:', err.message);
756
+ return { result: null, error: err.message };
757
+ }
758
+ }
759
+ ma
760
+ /** Poll SonarCloud API until project has measures or timeout (~5 min). */
761
+ async waitForProject(projectKey) {
762
+ const delays = [0, 5000, 10000, 15000, 20000, 25000, 30000, 30000, 30000, 30000, 30000];
763
+ for (let i = 0; i < delays.length; i++) {
764
+ if (delays[i] > 0) await new Promise((r) => setTimeout(r, delays[i]));
765
+ try {
766
+ const data = await this.client.getMeasures(projectKey, ['ncloc']);
767
+ if (data?.component?.measures?.length) return;
768
+ } catch (_) {}
769
+ }
770
+ }
771
+
772
+ /** @param {string} [projectKey] - Optional project key so UI can offer "Load report" even when unavailable */
773
+ getUnavailableResult(owner, repo, reason, projectKey = null) {
774
+ const scanKey = projectKey ?? this.getScanProjectKey(owner, repo);
775
+ const viewOnSonarCloud = scanKey ? `https://sonarcloud.io/project/overview?id=${encodeURIComponent(scanKey)}` : undefined;
776
+ const qualityGate = { status: null, conditions: [] };
777
+ const issues = { total: 0, items: [] };
778
+ const recommendations = [];
779
+ return {
780
+ source: 'SONARCLOUD',
781
+ available: false,
782
+ projectKey: scanKey,
783
+ score: null,
784
+ rating: null,
785
+ qualityGate,
786
+ metrics: {},
787
+ issues,
788
+ recommendations,
789
+ unavailableReason: reason,
790
+ viewOnSonarCloud,
791
+ analysis: {
792
+ summary: {
793
+ score: null,
794
+ rating: null,
795
+ qualityGateStatus: null,
796
+ overallStatus: 'Unavailable',
797
+ projectKey: scanKey,
798
+ viewOnSonarCloud,
799
+ unavailableReason: reason,
800
+ },
801
+ metrics: {},
802
+ qualityGate,
803
+ issues,
804
+ recommendations,
805
+ overallSummary: null,
806
+ },
807
+ };
808
+ }
809
+
810
+ /**
811
+ * Fetch full report data by project key only (for public projects or when analysis didn't return metrics).
812
+ * Tries with token, then without (public), and with branches main/master.
813
+ */
814
+ async fetchReportByProjectKey(projectKey) {
815
+ const branches = ['master', 'main', null];
816
+ let component = null;
817
+ let keyUsed = projectKey;
818
+
819
+ const tryMeasures = async (withAuth, useBasic, branch) => {
820
+ if (withAuth && this.client.token) {
821
+ try {
822
+ return await this.client.getMeasuresWithAuth(projectKey, DEFAULT_METRIC_KEYS, useBasic, branch);
823
+ } catch (_) {
824
+ return null;
825
+ }
826
+ }
827
+ try {
828
+ return await this.client.getMeasuresWithoutAuth(projectKey, DEFAULT_METRIC_KEYS, branch);
829
+ } catch (_) {
830
+ return null;
831
+ }
832
+ };
833
+
834
+ for (const branch of branches) {
835
+ if (component?.measures?.length) break;
836
+ try {
837
+ const data = await this.client.getMeasures(projectKey, DEFAULT_METRIC_KEYS);
838
+ if (data?.component?.measures?.length) {
839
+ component = data.component;
840
+ keyUsed = data.component.key || projectKey;
841
+ break;
842
+ }
843
+ } catch (_) {}
844
+ }
845
+ for (const branch of branches) {
846
+ if (component?.measures?.length) break;
847
+ for (const useBasic of [false, true]) {
848
+ const data = await tryMeasures(true, useBasic, branch);
849
+ if (data?.component?.measures?.length) {
850
+ component = data.component;
851
+ keyUsed = data.component.key || projectKey;
852
+ break;
853
+ }
854
+ }
855
+ }
856
+ for (const branch of branches) {
857
+ if (component?.measures?.length) break;
858
+ const data = await tryMeasures(false, false, branch);
859
+ if (data?.component?.measures?.length) {
860
+ component = data.component;
861
+ keyUsed = data.component.key || projectKey;
862
+ break;
863
+ }
864
+ }
865
+
866
+ if (!component?.measures?.length && this.client.token) {
867
+ const orgFromKey = projectKey.includes('_') ? projectKey.split('_')[0] : (projectKey.includes('-') ? projectKey.split('-')[0] : null);
868
+ const org = (config.sonarOrgKey && config.sonarOrgKey.trim()) ? config.sonarOrgKey.trim().toLowerCase() : (orgFromKey || '').toLowerCase();
869
+ const repoPart = projectKey.includes('_') ? projectKey.replace(/^[^_]+_/, '') : (projectKey.includes('-') ? projectKey.split('-').slice(1).join('-') : projectKey);
870
+ if (org) {
871
+ try {
872
+ const projects = await this.client.searchProjects(org);
873
+ for (const p of projects || []) {
874
+ const key = p.key || p.name;
875
+ if (!key || key === projectKey) continue;
876
+ const keyLower = key.toLowerCase();
877
+ const match = keyLower.includes(repoPart.toLowerCase().replace(/-/g, '_')) || keyLower.includes(repoPart.toLowerCase().replace(/_/g, '-')) || (p.name && p.name.toLowerCase().includes(repoPart.toLowerCase()));
878
+ if (!match) continue;
879
+ for (const branch of branches) {
880
+ try {
881
+ const data = await this.client.getMeasuresWithAuth(key, DEFAULT_METRIC_KEYS, false, branch);
882
+ if (data?.component?.measures?.length) {
883
+ component = data.component;
884
+ keyUsed = data.component.key || key;
885
+ break;
886
+ }
887
+ } catch (_) {}
888
+ if (component?.measures?.length) break;
889
+ }
890
+ if (component?.measures?.length) break;
891
+ }
892
+ } catch (_) {}
893
+ }
894
+ }
895
+
896
+ if (!component?.measures?.length) {
897
+ const viewLink = `https://sonarcloud.io/project/overview?id=${encodeURIComponent(projectKey)}`;
898
+ return {
899
+ source: 'SONARCLOUD',
900
+ available: false,
901
+ projectKey,
902
+ score: null,
903
+ rating: null,
904
+ qualityGate: { status: null, conditions: [] },
905
+ metrics: {},
906
+ issues: { total: 0, items: [] },
907
+ recommendations: [],
908
+ unavailableReason: `Could not load measures for key "${projectKey}". Ensure SONAR_ORGANIZATION in .env matches your SonarCloud org (project key is derived as org_repo). Optionally set SONAR_PROJECT_KEY to the exact key from SonarCloud (?id=...) if it differs.`,
909
+ viewOnSonarCloud: viewLink,
910
+ };
911
+ }
912
+
913
+ const [qualityGateData, issuesData] = await Promise.all([
914
+ this.client.getQualityGateStatus(keyUsed).catch(() => null),
915
+ this.client.getIssues(keyUsed, { ps: 100 }).catch(() => ({ issues: [], total: 0 })),
916
+ ]);
917
+ const metrics = this.parseMeasures(component.measures);
918
+ const qualityGate = this.parseQualityGate(qualityGateData);
919
+ const issues = this.parseIssues(issuesData?.issues || [], issuesData?.total ?? 0, 100);
920
+ const score = this.calculateScore(metrics, qualityGate, issues);
921
+ const rating = this.getRating(score);
922
+ const result = this.buildSonarResult(keyUsed, metrics, qualityGate, issues, score, rating);
923
+ this.logFetchedSonarCloudData(result);
924
+ return result;
925
+ }
926
+ }
927
+
928
+ export default SonarCloudAnalyzer;