@aws/lsp-codewhisperer 0.0.64 → 0.0.66

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 (109) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/out/client/token/bearer-token-service.json +1 -1
  3. package/out/language-server/agenticChat/agenticChatController.d.ts +13 -0
  4. package/out/language-server/agenticChat/agenticChatController.js +547 -125
  5. package/out/language-server/agenticChat/agenticChatController.js.map +1 -1
  6. package/out/language-server/agenticChat/constants/constants.d.ts +12 -0
  7. package/out/language-server/agenticChat/constants/constants.js +73 -1
  8. package/out/language-server/agenticChat/constants/constants.js.map +1 -1
  9. package/out/language-server/agenticChat/constants/toolConstants.d.ts +24 -0
  10. package/out/language-server/agenticChat/constants/toolConstants.js +35 -0
  11. package/out/language-server/agenticChat/constants/toolConstants.js.map +1 -0
  12. package/out/language-server/agenticChat/context/additionalContextProvider.d.ts +1 -1
  13. package/out/language-server/agenticChat/context/additionalContextProvider.js +44 -4
  14. package/out/language-server/agenticChat/context/additionalContextProvider.js.map +1 -1
  15. package/out/language-server/agenticChat/context/agenticChatTriggerContext.d.ts +11 -2
  16. package/out/language-server/agenticChat/context/agenticChatTriggerContext.js +32 -2
  17. package/out/language-server/agenticChat/context/agenticChatTriggerContext.js.map +1 -1
  18. package/out/language-server/agenticChat/context/contextCommandsProvider.js +1 -1
  19. package/out/language-server/agenticChat/context/contextCommandsProvider.js.map +1 -1
  20. package/out/language-server/agenticChat/context/contextUtils.d.ts +16 -0
  21. package/out/language-server/agenticChat/context/contextUtils.js +29 -0
  22. package/out/language-server/agenticChat/context/contextUtils.js.map +1 -1
  23. package/out/language-server/agenticChat/qAgenticChatServer.js +6 -2
  24. package/out/language-server/agenticChat/qAgenticChatServer.js.map +1 -1
  25. package/out/language-server/agenticChat/tools/chatDb/chatDb.d.ts +11 -3
  26. package/out/language-server/agenticChat/tools/chatDb/chatDb.js +103 -31
  27. package/out/language-server/agenticChat/tools/chatDb/chatDb.js.map +1 -1
  28. package/out/language-server/agenticChat/tools/chatDb/util.d.ts +4 -0
  29. package/out/language-server/agenticChat/tools/chatDb/util.js.map +1 -1
  30. package/out/language-server/agenticChat/tools/executeBash.d.ts +15 -5
  31. package/out/language-server/agenticChat/tools/executeBash.js +97 -22
  32. package/out/language-server/agenticChat/tools/executeBash.js.map +1 -1
  33. package/out/language-server/agenticChat/tools/mcp/mcpManager.js +1 -1
  34. package/out/language-server/agenticChat/tools/mcp/mcpManager.js.map +1 -1
  35. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReview.d.ts +211 -0
  36. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReview.js +630 -0
  37. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReview.js.map +1 -0
  38. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewConstants.d.ts +34 -0
  39. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewConstants.js +200 -0
  40. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewConstants.js.map +1 -0
  41. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewErrors.d.ts +12 -0
  42. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewErrors.js +32 -0
  43. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewErrors.js.map +1 -0
  44. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewSchemas.d.ts +289 -0
  45. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewSchemas.js +140 -0
  46. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewSchemas.js.map +1 -0
  47. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewTypes.d.ts +58 -0
  48. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewTypes.js +3 -0
  49. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewTypes.js.map +1 -0
  50. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewUtils.d.ts +156 -0
  51. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewUtils.js +363 -0
  52. package/out/language-server/agenticChat/tools/qCodeAnalysis/qCodeReviewUtils.js.map +1 -0
  53. package/out/language-server/agenticChat/tools/toolServer.d.ts +1 -0
  54. package/out/language-server/agenticChat/tools/toolServer.js +90 -39
  55. package/out/language-server/agenticChat/tools/toolServer.js.map +1 -1
  56. package/out/language-server/chat/chatSessionService.d.ts +1 -0
  57. package/out/language-server/chat/chatSessionService.js +5 -2
  58. package/out/language-server/chat/chatSessionService.js.map +1 -1
  59. package/out/language-server/chat/quickActions.d.ts +7 -1
  60. package/out/language-server/chat/quickActions.js +7 -1
  61. package/out/language-server/chat/quickActions.js.map +1 -1
  62. package/out/language-server/chat/telemetry/chatTelemetryController.d.ts +4 -3
  63. package/out/language-server/chat/telemetry/chatTelemetryController.js +22 -3
  64. package/out/language-server/chat/telemetry/chatTelemetryController.js.map +1 -1
  65. package/out/language-server/configuration/qConfigurationServer.d.ts +1 -0
  66. package/out/language-server/configuration/qConfigurationServer.js.map +1 -1
  67. package/out/language-server/inline-completion/auto-trigger/autoTrigger.d.ts +2 -0
  68. package/out/language-server/inline-completion/auto-trigger/autoTrigger.js +69 -1
  69. package/out/language-server/inline-completion/auto-trigger/autoTrigger.js.map +1 -1
  70. package/out/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.js +6 -1
  71. package/out/language-server/inline-completion/auto-trigger/editPredictionAutoTrigger.js.map +1 -1
  72. package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.d.ts +1 -0
  73. package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.js +1 -0
  74. package/out/language-server/inline-completion/auto-trigger/editPredictionConfig.js.map +1 -1
  75. package/out/language-server/inline-completion/codeWhispererServer.js +72 -43
  76. package/out/language-server/inline-completion/codeWhispererServer.js.map +1 -1
  77. package/out/language-server/workspaceContext/artifactManager.d.ts +4 -1
  78. package/out/language-server/workspaceContext/artifactManager.js +16 -1
  79. package/out/language-server/workspaceContext/artifactManager.js.map +1 -1
  80. package/out/language-server/workspaceContext/dependency/dependencyDiscoverer.d.ts +2 -1
  81. package/out/language-server/workspaceContext/dependency/dependencyDiscoverer.js +9 -6
  82. package/out/language-server/workspaceContext/dependency/dependencyDiscoverer.js.map +1 -1
  83. package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.d.ts +7 -2
  84. package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.js +20 -7
  85. package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.js.map +1 -1
  86. package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandlerFactory.d.ts +2 -2
  87. package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandlerFactory.js +4 -4
  88. package/out/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandlerFactory.js.map +1 -1
  89. package/out/language-server/workspaceContext/fileUploadJobManager.js +3 -1
  90. package/out/language-server/workspaceContext/fileUploadJobManager.js.map +1 -1
  91. package/out/language-server/workspaceContext/workspaceContextServer.js +32 -19
  92. package/out/language-server/workspaceContext/workspaceContextServer.js.map +1 -1
  93. package/out/language-server/workspaceContext/workspaceFolderManager.d.ts +5 -3
  94. package/out/language-server/workspaceContext/workspaceFolderManager.js +80 -59
  95. package/out/language-server/workspaceContext/workspaceFolderManager.js.map +1 -1
  96. package/out/shared/activeUserTracker.d.ts +52 -0
  97. package/out/shared/activeUserTracker.js +164 -0
  98. package/out/shared/activeUserTracker.js.map +1 -0
  99. package/out/shared/amazonQServiceManager/configurationUtils.js +6 -0
  100. package/out/shared/amazonQServiceManager/configurationUtils.js.map +1 -1
  101. package/out/shared/codeWhispererService.js +3 -1
  102. package/out/shared/codeWhispererService.js.map +1 -1
  103. package/out/shared/telemetry/telemetryService.d.ts +2 -0
  104. package/out/shared/telemetry/telemetryService.js +2 -0
  105. package/out/shared/telemetry/telemetryService.js.map +1 -1
  106. package/out/shared/telemetry/types.d.ts +9 -1
  107. package/out/shared/telemetry/types.js +1 -0
  108. package/out/shared/telemetry/types.js.map +1 -1
  109. package/package.json +8 -6
@@ -0,0 +1,630 @@
1
+ "use strict";
2
+ /* eslint-disable import/no-nodejs-modules */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.QCodeReview = void 0;
5
+ const qCodeReviewConstants_1 = require("./qCodeReviewConstants");
6
+ const qCodeReviewUtils_1 = require("./qCodeReviewUtils");
7
+ const qCodeReviewSchemas_1 = require("./qCodeReviewSchemas");
8
+ const crypto_1 = require("crypto");
9
+ const crypto = require("crypto");
10
+ const path = require("path");
11
+ const JSZip = require("jszip");
12
+ const fs_1 = require("fs");
13
+ const qCodeReviewErrors_1 = require("./qCodeReviewErrors");
14
+ const lsp_core_1 = require("@aws/lsp-core");
15
+ class QCodeReview {
16
+ static CUSTOMER_CODE_BASE_PATH = 'customerCodeBaseFolder';
17
+ static CODE_ARTIFACT_PATH = 'code_artifact';
18
+ static CUSTOMER_CODE_ZIP_NAME = 'customerCode.zip';
19
+ static CODE_DIFF_PATH = 'code_artifact/codeDiff/customerCodeDiff.diff';
20
+ static RULE_ARTIFACT_PATH = '.amazonq/rules';
21
+ static MAX_POLLING_ATTEMPTS = 30;
22
+ static MID_POLLING_ATTEMPTS = 15;
23
+ static POLLING_INTERVAL_MS = 10000;
24
+ static UPLOAD_INTENT = 'AGENTIC_CODE_REVIEW';
25
+ static SCAN_SCOPE = 'AGENTIC';
26
+ static MAX_FINDINGS_COUNT = 50;
27
+ static ERROR_MESSAGES = {
28
+ MISSING_CLIENT: 'CodeWhisperer client not available',
29
+ MISSING_ARTIFACTS: `Missing fileLevelArtifacts and folderLevelArtifacts for ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool. Ask user to provide a specific file / folder / workspace which has code that can be scanned.`,
30
+ MISSING_FILES_TO_SCAN: `There are no valid files to scan. Ask user to provide a specific file / folder / workspace which has code that can be scanned.`,
31
+ UPLOAD_FAILED: `Failed to upload artifact for code review in ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool.`,
32
+ START_CODE_ANALYSIS_FAILED: (jobId) => `Failed to start code analysis in ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool for jobId - ${jobId}`,
33
+ CODE_ANALYSIS_FAILED: (jobId, message) => `Code analysis failed for jobId - ${jobId} due to ${message}`,
34
+ SCAN_FAILED: 'Code scan failed',
35
+ TIMEOUT: (attempts) => `Code scan timed out after ${attempts} attempts. Ask user to provide a smaller size of code to scan.`,
36
+ };
37
+ credentialsProvider;
38
+ logging;
39
+ telemetry;
40
+ workspace;
41
+ codeWhispererClient;
42
+ cancellationToken;
43
+ writableStream;
44
+ constructor(features) {
45
+ this.credentialsProvider = features.credentialsProvider;
46
+ this.logging = features.logging;
47
+ this.telemetry = features.telemetry;
48
+ this.workspace = features.workspace;
49
+ }
50
+ static toolName = qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME;
51
+ static toolDescription = qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_DESCRIPTION;
52
+ static inputSchema = qCodeReviewSchemas_1.Q_CODE_REVIEW_INPUT_SCHEMA;
53
+ /**
54
+ * Main execution method for the QCodeReview tool
55
+ * @param input User input parameters for code review
56
+ * @param context Execution context containing clients and tokens
57
+ * @returns Output containing code review results or error message
58
+ */
59
+ async execute(input, context) {
60
+ let chatStreamWriter;
61
+ try {
62
+ this.logging.info(`Executing ${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME}: ${JSON.stringify(input)}`);
63
+ // 1. Validate input
64
+ const setup = await this.validateInputAndSetup(input, context);
65
+ this.checkCancellation();
66
+ chatStreamWriter = this.writableStream?.getWriter();
67
+ await chatStreamWriter?.write('Initiating code review...');
68
+ // 2. Prepare code artifact and upload to service
69
+ const uploadResult = await this.prepareAndUploadArtifacts(setup);
70
+ this.checkCancellation();
71
+ // 3. Start code analysis
72
+ const analysisResult = await this.startCodeAnalysis(setup, uploadResult);
73
+ this.checkCancellation();
74
+ await chatStreamWriter?.write('Reviewing your code...');
75
+ // 4. Wait for scan to complete
76
+ await this.pollForCompletion(analysisResult.jobId, setup.scanName, setup.artifactType, chatStreamWriter);
77
+ this.checkCancellation();
78
+ // 5. Process scan result
79
+ const results = await this.processResults(setup, uploadResult.isCodeDiffPresent, analysisResult.jobId);
80
+ return {
81
+ output: {
82
+ kind: 'json',
83
+ success: true,
84
+ content: results,
85
+ },
86
+ };
87
+ }
88
+ catch (error) {
89
+ if (error instanceof lsp_core_1.CancellationError) {
90
+ throw error;
91
+ }
92
+ return {
93
+ output: {
94
+ kind: 'json',
95
+ success: false,
96
+ content: {
97
+ errorMessage: error.message,
98
+ },
99
+ },
100
+ };
101
+ }
102
+ finally {
103
+ await chatStreamWriter?.close();
104
+ chatStreamWriter?.releaseLock();
105
+ }
106
+ }
107
+ /**
108
+ * Validates user input and sets up the execution environment
109
+ * @param input User input parameters for code review
110
+ * @param context Execution context containing clients and tokens
111
+ * @returns Setup object with validated parameters or error message
112
+ */
113
+ async validateInputAndSetup(input, context) {
114
+ this.cancellationToken = context.cancellationToken;
115
+ this.writableStream = context.writableStream;
116
+ this.codeWhispererClient = context.codeWhispererClient;
117
+ if (!this.codeWhispererClient) {
118
+ throw new Error(QCodeReview.ERROR_MESSAGES.MISSING_CLIENT);
119
+ }
120
+ // parse input
121
+ const validatedInput = qCodeReviewSchemas_1.Z_Q_CODE_REVIEW_INPUT_SCHEMA.parse(input);
122
+ const fileArtifacts = validatedInput.fileLevelArtifacts || [];
123
+ const folderArtifacts = validatedInput.folderLevelArtifacts || [];
124
+ const ruleArtifacts = validatedInput.ruleArtifacts || [];
125
+ if (fileArtifacts.length === 0 && folderArtifacts.length === 0) {
126
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('MissingFilesOrFolders', {}, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
127
+ throw new qCodeReviewErrors_1.QCodeReviewValidationError(QCodeReview.ERROR_MESSAGES.MISSING_ARTIFACTS);
128
+ }
129
+ const isFullReviewRequest = validatedInput.scopeOfReview?.toUpperCase() === qCodeReviewConstants_1.FULL_REVIEW;
130
+ const artifactType = fileArtifacts.length > 0 ? 'FILE' : 'FOLDER';
131
+ // Setting java as default language
132
+ // TODO: Remove requirement of programming language
133
+ const programmingLanguage = 'java';
134
+ const scanName = 'Standard-' + (0, crypto_1.randomUUID)();
135
+ this.logging.info(`Agentic scan name: ${scanName}`);
136
+ return {
137
+ fileArtifacts,
138
+ folderArtifacts,
139
+ isFullReviewRequest,
140
+ artifactType,
141
+ programmingLanguage,
142
+ scanName,
143
+ ruleArtifacts,
144
+ };
145
+ }
146
+ /**
147
+ * Prepares and uploads code artifacts for analysis
148
+ * @param setup Setup object with validated parameters
149
+ * @returns Upload result with uploadId or error message
150
+ */
151
+ async prepareAndUploadArtifacts(setup) {
152
+ const { zipBuffer, md5Hash, isCodeDiffPresent } = await this.prepareFilesAndFoldersForUpload(setup.fileArtifacts, setup.folderArtifacts, setup.ruleArtifacts, setup.isFullReviewRequest);
153
+ const uploadUrlResponse = await this.codeWhispererClient.createUploadUrl({
154
+ contentLength: zipBuffer.length,
155
+ contentMd5: md5Hash,
156
+ uploadIntent: QCodeReview.UPLOAD_INTENT,
157
+ uploadContext: {
158
+ codeAnalysisUploadContext: {
159
+ codeScanName: setup.scanName,
160
+ },
161
+ },
162
+ });
163
+ if (!uploadUrlResponse.uploadUrl || !uploadUrlResponse.uploadId) {
164
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('createUploadUrlFailed', {
165
+ codeScanName: setup.scanName,
166
+ contentLength: zipBuffer.length,
167
+ uploadIntent: QCodeReview.UPLOAD_INTENT,
168
+ response: uploadUrlResponse,
169
+ }, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
170
+ throw new qCodeReviewErrors_1.QCodeReviewValidationError(QCodeReview.ERROR_MESSAGES.UPLOAD_FAILED);
171
+ }
172
+ await qCodeReviewUtils_1.QCodeReviewUtils.uploadFileToPresignedUrl(uploadUrlResponse.uploadUrl, zipBuffer, uploadUrlResponse.requestHeaders || {}, this.logging);
173
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('uploadArtifactSuccess', {
174
+ codeScanName: setup.scanName,
175
+ codeArtifactId: uploadUrlResponse.uploadId,
176
+ artifactSize: zipBuffer.length,
177
+ artifactType: setup.artifactType,
178
+ }, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
179
+ return { uploadId: uploadUrlResponse.uploadId, isCodeDiffPresent };
180
+ }
181
+ /**
182
+ * Initiates code analysis with the uploaded artifacts
183
+ * @param setup Setup object with validated parameters
184
+ * @param uploadResult Result from artifact upload containing uploadId
185
+ * @returns Code scan jobId and status
186
+ */
187
+ async startCodeAnalysis(setup, uploadResult) {
188
+ const createResponse = await this.codeWhispererClient.startCodeAnalysis({
189
+ artifacts: { SourceCode: uploadResult.uploadId },
190
+ programmingLanguage: { languageName: setup.programmingLanguage },
191
+ clientToken: qCodeReviewUtils_1.QCodeReviewUtils.generateClientToken(),
192
+ codeScanName: setup.scanName,
193
+ scope: QCodeReview.SCAN_SCOPE,
194
+ codeDiffMetadata: uploadResult.isCodeDiffPresent ? { codeDiffPath: '/code_artifact/codeDiff/' } : undefined,
195
+ });
196
+ if (!createResponse.jobId) {
197
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('startCodeAnalysisFailed', {
198
+ artifacts: { SourceCode: uploadResult.uploadId },
199
+ programmingLanguage: { languageName: setup.programmingLanguage },
200
+ codeScanName: setup.scanName,
201
+ scope: QCodeReview.SCAN_SCOPE,
202
+ artifactType: setup.artifactType,
203
+ response: createResponse,
204
+ }, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
205
+ throw new qCodeReviewErrors_1.QCodeReviewInternalError(QCodeReview.ERROR_MESSAGES.START_CODE_ANALYSIS_FAILED(createResponse.jobId));
206
+ }
207
+ this.logging.info(`Code scan created with job ID: ${createResponse.jobId}`);
208
+ return {
209
+ jobId: createResponse.jobId,
210
+ status: createResponse.status,
211
+ };
212
+ }
213
+ /**
214
+ * Polls for completion of the code analysis job
215
+ * @param jobId ID of the code analysis job
216
+ * @param scanName Name of the code scan
217
+ * @param artifactType Type of artifact being scanned (FILE or FOLDER)
218
+ * @param chatStreamWriter Stream writer for sending progress updates
219
+ */
220
+ async pollForCompletion(jobId, scanName, artifactType, chatStreamWriter) {
221
+ let status = 'Pending';
222
+ let attemptCount = 0;
223
+ while (status === 'Pending' && attemptCount < QCodeReview.MAX_POLLING_ATTEMPTS) {
224
+ this.logging.info(`Code scan status: ${status}, waiting...`);
225
+ await new Promise(resolve => setTimeout(resolve, QCodeReview.POLLING_INTERVAL_MS));
226
+ const statusResponse = await this.getCodeAnalysisStatus(jobId);
227
+ status = statusResponse.status;
228
+ attemptCount++;
229
+ if (statusResponse.errorMessage) {
230
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('codeAnalysisFailed', {
231
+ codeScanName: scanName,
232
+ codeReviewId: jobId,
233
+ status,
234
+ artifactType,
235
+ message: statusResponse.errorMessage,
236
+ }, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
237
+ throw new qCodeReviewErrors_1.QCodeReviewInternalError(QCodeReview.ERROR_MESSAGES.CODE_ANALYSIS_FAILED(jobId, statusResponse.errorMessage));
238
+ }
239
+ if (attemptCount == QCodeReview.MID_POLLING_ATTEMPTS) {
240
+ await chatStreamWriter?.write('Still reviewing your code, it is taking just a bit longer than usual...');
241
+ }
242
+ this.checkCancellation('Command execution cancelled while waiting for scan to complete');
243
+ }
244
+ if (status === 'Pending') {
245
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('codeAnalysisTimeout', {
246
+ codeScanName: scanName,
247
+ codeReviewId: jobId,
248
+ status: 'Timeout',
249
+ maxAttempts: QCodeReview.MAX_POLLING_ATTEMPTS,
250
+ }, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
251
+ throw new qCodeReviewErrors_1.QCodeReviewTimeoutError(QCodeReview.ERROR_MESSAGES.TIMEOUT(QCodeReview.MAX_POLLING_ATTEMPTS));
252
+ }
253
+ this.logging.info(`Code scan completed with status: ${status}`);
254
+ }
255
+ /**
256
+ * Processes the results of the completed code analysis
257
+ * @param setup Setup object with validated parameters
258
+ * @param isCodeDiffPresent If code diff is present in upload artifact
259
+ * @param jobId ID of the code analysis job
260
+ * @returns Processed results with findings grouped by file
261
+ */
262
+ async processResults(setup, isCodeDiffPresent, jobId) {
263
+ const { totalFindings, findingsExceededLimit } = await this.collectFindings(jobId, setup.isFullReviewRequest, isCodeDiffPresent, setup.programmingLanguage);
264
+ qCodeReviewUtils_1.QCodeReviewUtils.emitMetric('codeAnalysisSucces', {
265
+ codeScanName: setup.scanName,
266
+ codeReviewId: jobId,
267
+ findingsCount: totalFindings.length,
268
+ }, qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME, this.logging, this.telemetry, this.credentialsProvider.getConnectionMetadata()?.sso?.startUrl);
269
+ const aggregatedCodeScanIssueList = this.aggregateFindingsByFile(findingsExceededLimit ? totalFindings.slice(0, QCodeReview.MAX_FINDINGS_COUNT) : totalFindings, setup.fileArtifacts, setup.folderArtifacts);
270
+ this.logging.info('Findings count grouped by file');
271
+ aggregatedCodeScanIssueList.forEach(item => this.logging.info(`File path - ${item.filePath} Findings count - ${item.issues.length}`));
272
+ return {
273
+ codeReviewId: jobId,
274
+ message: `${qCodeReviewConstants_1.Q_CODE_REVIEW_TOOL_NAME} tool completed successfully.${findingsExceededLimit ? ` Inform the user that we are limiting findings to top ${QCodeReview.MAX_FINDINGS_COUNT} based on severity.` : ''}`,
275
+ findingsByFile: JSON.stringify(aggregatedCodeScanIssueList),
276
+ };
277
+ }
278
+ /**
279
+ * Collects findings from the code analysis job
280
+ * @param jobId ID of the code analysis job
281
+ * @param isFullReviewRequest Whether this is a full review or diff review
282
+ * @param isCodeDiffPresent Whether code diff is present in the artifacts
283
+ * @param programmingLanguage Programming language
284
+ * @returns Object containing collected findings and whether limit was exceeded
285
+ */
286
+ async collectFindings(jobId, isFullReviewRequest, isCodeDiffPresent, programmingLanguage) {
287
+ let totalFindings = [];
288
+ let nextFindingToken = undefined;
289
+ let findingsExceededLimit = false;
290
+ const lookForCodeDiffFindings = !isFullReviewRequest && isCodeDiffPresent;
291
+ this.logging.info(`Collect findings for jobId: ${jobId}, isFullReviewRequest: ${isFullReviewRequest}, isCodeDiffPresent: ${isCodeDiffPresent}`);
292
+ this.logging.info(`Look for code diff findings only - ${lookForCodeDiffFindings}`);
293
+ do {
294
+ this.logging.info(`GetFindings for job ID: ${jobId}`);
295
+ const findingsResponse = await this.getCodeAnalysisFindings(jobId, nextFindingToken);
296
+ nextFindingToken = findingsResponse.nextToken;
297
+ const parsedFindings = this.parseFindings(findingsResponse.codeAnalysisFindings, jobId, programmingLanguage) || [];
298
+ const filteredFindings = lookForCodeDiffFindings
299
+ ? parsedFindings.filter(finding => 'CodeDiff' === finding.findingContext)
300
+ : parsedFindings;
301
+ totalFindings = totalFindings.concat(filteredFindings);
302
+ if (totalFindings.length > QCodeReview.MAX_FINDINGS_COUNT) {
303
+ findingsExceededLimit = true;
304
+ break;
305
+ }
306
+ } while (nextFindingToken);
307
+ this.logging.info(`Total findings: ${totalFindings.length}`);
308
+ return { totalFindings, findingsExceededLimit };
309
+ }
310
+ /**
311
+ * Gets the current status of a code analysis job
312
+ * @param jobId ID of the code analysis job
313
+ * @returns Status response from the CodeWhisperer service
314
+ */
315
+ async getCodeAnalysisStatus(jobId) {
316
+ return await this.codeWhispererClient.getCodeAnalysis({ jobId });
317
+ }
318
+ /**
319
+ * Retrieves findings from a code analysis job
320
+ * @param jobId ID of the code analysis job
321
+ * @param nextToken Pagination token for retrieving next batch of findings
322
+ * @returns Findings response from the CodeWhisperer service
323
+ */
324
+ async getCodeAnalysisFindings(jobId, nextToken) {
325
+ return await this.codeWhispererClient.listCodeAnalysisFindings({
326
+ jobId,
327
+ nextToken,
328
+ codeAnalysisFindingsSchema: 'codeanalysis/findings/1.0',
329
+ });
330
+ }
331
+ /**
332
+ * Create a zip archive of the files and folders to be scanned and calculate MD5 hash
333
+ * @param fileArtifacts Array of file artifacts containing path and programming language
334
+ * @param folderArtifacts Array of folder artifacts containing path
335
+ * @param ruleArtifacts Array of file paths to user selected rules
336
+ * @param isFullReviewRequest If user asked for Full review or Partial review
337
+ * @returns An object containing the zip file buffer and its MD5 hash
338
+ */
339
+ async prepareFilesAndFoldersForUpload(fileArtifacts, folderArtifacts, ruleArtifacts, isFullReviewRequest) {
340
+ try {
341
+ this.logging.info(`Preparing ${fileArtifacts.length} files and ${folderArtifacts.length} folders for upload`);
342
+ const codeArtifactZip = new JSZip();
343
+ const customerCodeZip = new JSZip();
344
+ // Process files and folders
345
+ const codeDiff = await this.processArtifacts(fileArtifacts, folderArtifacts, ruleArtifacts, customerCodeZip, !isFullReviewRequest);
346
+ let numberOfFilesInCustomerCodeZip = qCodeReviewUtils_1.QCodeReviewUtils.countZipFiles(customerCodeZip);
347
+ if (numberOfFilesInCustomerCodeZip > ruleArtifacts.length) {
348
+ // Validates that there are actual files to scan, other than rule artifacts
349
+ this.logging.info(`Total files in customerCodeZip - ${numberOfFilesInCustomerCodeZip}`);
350
+ }
351
+ else {
352
+ throw new qCodeReviewErrors_1.QCodeReviewValidationError(QCodeReview.ERROR_MESSAGES.MISSING_FILES_TO_SCAN);
353
+ }
354
+ // Generate user code zip buffer
355
+ const customerCodeBuffer = await qCodeReviewUtils_1.QCodeReviewUtils.generateZipBuffer(customerCodeZip);
356
+ qCodeReviewUtils_1.QCodeReviewUtils.logZipStructure(customerCodeZip, 'User code', this.logging);
357
+ // Add user code zip to the main artifact zip
358
+ codeArtifactZip.file(`${QCodeReview.CODE_ARTIFACT_PATH}/${QCodeReview.CUSTOMER_CODE_ZIP_NAME}`, customerCodeBuffer);
359
+ let isCodeDiffPresent = false;
360
+ // Add code diff file if we have any diffs
361
+ if (codeDiff.trim()) {
362
+ this.logging.info(`Adding code diff to zip of size: ${codeDiff.length}`);
363
+ isCodeDiffPresent = true;
364
+ codeArtifactZip.file(QCodeReview.CODE_DIFF_PATH, codeDiff);
365
+ }
366
+ // Generate the final code artifact zip
367
+ const zipBuffer = await qCodeReviewUtils_1.QCodeReviewUtils.generateZipBuffer(codeArtifactZip);
368
+ qCodeReviewUtils_1.QCodeReviewUtils.logZipStructure(codeArtifactZip, 'Code artifact', this.logging);
369
+ // Calculate MD5 hash of the zip buffer
370
+ const md5Hash = crypto.createHash('md5').update(zipBuffer).digest('hex');
371
+ this.logging.info(`Created zip archive, size: ${zipBuffer.byteLength} bytes, MD5: ${md5Hash}`);
372
+ return { zipBuffer, md5Hash, isCodeDiffPresent };
373
+ }
374
+ catch (error) {
375
+ this.logging.error(`Error preparing files for upload: ${error}`);
376
+ throw error;
377
+ }
378
+ }
379
+ /**
380
+ * Processes file, folder, and rule artifacts for inclusion in the zip archive
381
+ * @param fileArtifacts Array of file artifacts to process
382
+ * @param folderArtifacts Array of folder artifacts to process
383
+ * @param ruleArtifacts Array of rule artifacts to process
384
+ * @param customerCodeZip JSZip instance for the customer code
385
+ * @param isCodeDiffScan Whether this is a code diff scan
386
+ * @returns Combined code diff string from all artifacts
387
+ */
388
+ async processArtifacts(fileArtifacts, folderArtifacts, ruleArtifacts, customerCodeZip, isCodeDiffScan) {
389
+ let codeDiff = '';
390
+ // Process files
391
+ codeDiff += await this.processFileArtifacts(fileArtifacts, customerCodeZip, isCodeDiffScan);
392
+ // Process folders
393
+ codeDiff += await this.processFolderArtifacts(folderArtifacts, customerCodeZip, isCodeDiffScan);
394
+ // Process rule artifacts
395
+ await this.processRuleArtifacts(ruleArtifacts, customerCodeZip);
396
+ return codeDiff;
397
+ }
398
+ /**
399
+ * Processes file artifacts for inclusion in the zip archive
400
+ * @param fileArtifacts Array of file artifacts to process
401
+ * @param customerCodeZip JSZip instance for the customer code
402
+ * @param isCodeDiffScan Whether this is a code diff scan
403
+ * @returns Combined code diff string from file artifacts
404
+ */
405
+ async processFileArtifacts(fileArtifacts, customerCodeZip, isCodeDiffScan) {
406
+ let codeDiff = '';
407
+ for (const artifact of fileArtifacts) {
408
+ await qCodeReviewUtils_1.QCodeReviewUtils.withErrorHandling(async () => {
409
+ let fileName = path.basename(artifact.path);
410
+ if (!fileName.startsWith('.') &&
411
+ !qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipFile(fileName) &&
412
+ (0, fs_1.existsSync)(artifact.path)) {
413
+ const fileContent = await this.workspace.fs.readFile(artifact.path);
414
+ let normalizedArtifactPath = qCodeReviewUtils_1.QCodeReviewUtils.convertToUnixPath(artifact.path);
415
+ customerCodeZip.file(`${QCodeReview.CUSTOMER_CODE_BASE_PATH}${normalizedArtifactPath}`, fileContent);
416
+ }
417
+ else {
418
+ this.logging.info(`Skipping file - ${artifact.path}`);
419
+ }
420
+ }, 'Failed to read file', this.logging, artifact.path);
421
+ codeDiff += await qCodeReviewUtils_1.QCodeReviewUtils.processArtifactWithDiff(artifact, isCodeDiffScan, this.logging);
422
+ }
423
+ return codeDiff;
424
+ }
425
+ /**
426
+ * Processes folder artifacts for inclusion in the zip archive
427
+ * @param folderArtifacts Array of folder artifacts to process
428
+ * @param customerCodeZip JSZip instance for the customer code
429
+ * @param isCodeDiffScan Whether this is a code diff scan
430
+ * @returns Combined code diff string from folder artifacts
431
+ */
432
+ async processFolderArtifacts(folderArtifacts, customerCodeZip, isCodeDiffScan) {
433
+ let codeDiff = '';
434
+ for (const folderArtifact of folderArtifacts) {
435
+ await qCodeReviewUtils_1.QCodeReviewUtils.withErrorHandling(async () => {
436
+ await this.addFolderToZip(customerCodeZip, folderArtifact.path, QCodeReview.CUSTOMER_CODE_BASE_PATH);
437
+ }, 'Failed to add folder', this.logging, folderArtifact.path);
438
+ codeDiff += await qCodeReviewUtils_1.QCodeReviewUtils.processArtifactWithDiff(folderArtifact, isCodeDiffScan, this.logging);
439
+ }
440
+ return codeDiff;
441
+ }
442
+ /**
443
+ * Processes rule artifacts for inclusion in the zip archive
444
+ * @param ruleArtifacts Array of rule artifacts to process
445
+ * @param customerCodeZip JSZip instance for the customer code
446
+ */
447
+ async processRuleArtifacts(ruleArtifacts, customerCodeZip) {
448
+ for (const artifact of ruleArtifacts) {
449
+ await qCodeReviewUtils_1.QCodeReviewUtils.withErrorHandling(async () => {
450
+ let fileName = path.basename(artifact.path);
451
+ if (!fileName.startsWith('.') &&
452
+ !qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipFile(fileName) &&
453
+ (0, fs_1.existsSync)(artifact.path)) {
454
+ const fileContent = await this.workspace.fs.readFile(artifact.path);
455
+ customerCodeZip.file(`${QCodeReview.CUSTOMER_CODE_BASE_PATH}/${QCodeReview.RULE_ARTIFACT_PATH}/${fileName}`, fileContent);
456
+ }
457
+ else {
458
+ this.logging.info(`Skipping file - ${artifact.path}`);
459
+ }
460
+ }, 'Failed to read file', this.logging, artifact.path);
461
+ }
462
+ }
463
+ /**
464
+ * Recursively add a folder and its contents to a zip archive
465
+ * @param zip JSZip instance to add files to
466
+ * @param folderPath Path to the folder to add
467
+ * @param zipPath Relative path within the zip archive
468
+ */
469
+ async addFolderToZip(zip, folderPath, zipPath) {
470
+ try {
471
+ const entries = await this.workspace.fs.readdir(folderPath);
472
+ for (const entry of entries) {
473
+ const name = entry.name;
474
+ const fullPath = path.join(entry.parentPath, name);
475
+ if (entry.isFile()) {
476
+ if (name.startsWith('.') || qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipFile(name) || !(0, fs_1.existsSync)(fullPath)) {
477
+ this.logging.info(`Skipping file - ${fullPath}`);
478
+ continue;
479
+ }
480
+ const content = await this.workspace.fs.readFile(fullPath);
481
+ let normalizedArtifactPath = qCodeReviewUtils_1.QCodeReviewUtils.convertToUnixPath(fullPath);
482
+ zip.file(`${zipPath}${normalizedArtifactPath}`, content);
483
+ }
484
+ else if (entry.isDirectory()) {
485
+ if (qCodeReviewUtils_1.QCodeReviewUtils.shouldSkipDirectory(name)) {
486
+ this.logging.info(`Skipping directory - ${fullPath}`);
487
+ continue;
488
+ }
489
+ await this.addFolderToZip(zip, fullPath, zipPath);
490
+ }
491
+ }
492
+ }
493
+ catch (error) {
494
+ this.logging.error(`Error adding folder to zip: ${error}`);
495
+ throw error;
496
+ }
497
+ }
498
+ /**
499
+ * Parse and validate findings JSON response
500
+ * @param findingsJson Raw JSON string from the code analysis response
501
+ * @param jobId Code scan job Id
502
+ * @param programmingLanguage programming language
503
+ * @returns Parsed and validated findings array
504
+ */
505
+ parseFindings(findingsJson, jobId, programmingLanguage) {
506
+ try {
507
+ const findingsResponseJSON = JSON.parse(findingsJson);
508
+ // Normalize ruleId fields
509
+ for (const finding of findingsResponseJSON) {
510
+ if (finding['ruleId'] == null) {
511
+ finding['ruleId'] = undefined;
512
+ }
513
+ }
514
+ return qCodeReviewSchemas_1.Q_FINDINGS_SCHEMA.parse(findingsResponseJSON).map(issue => ({
515
+ startLine: issue.startLine - 1 >= 0 ? issue.startLine - 1 : 0,
516
+ endLine: issue.endLine,
517
+ comment: `${issue.title.trim()}: ${issue.description.text.trim()}`,
518
+ title: issue.title,
519
+ description: issue.description,
520
+ detectorId: issue.detectorId,
521
+ detectorName: issue.detectorName,
522
+ findingId: issue.findingId,
523
+ ruleId: issue.ruleId != null ? issue.ruleId : undefined,
524
+ relatedVulnerabilities: issue.relatedVulnerabilities,
525
+ severity: issue.severity,
526
+ recommendation: issue.remediation.recommendation,
527
+ suggestedFixes: issue.suggestedFixes != undefined ? issue.suggestedFixes : [],
528
+ scanJobId: jobId,
529
+ language: programmingLanguage,
530
+ autoDetected: false,
531
+ filePath: issue.filePath,
532
+ findingContext: issue.findingContext,
533
+ }));
534
+ }
535
+ catch (e) {
536
+ this.logging.error(`Error parsing findings in response: ${e}`);
537
+ throw new qCodeReviewErrors_1.QCodeReviewInternalError('Error parsing findings in response');
538
+ }
539
+ }
540
+ /**
541
+ * Aggregate findings by file path
542
+ * @param findings Array of findings
543
+ * @param fileArtifacts Array of file artifacts being scanned
544
+ * @param folderArtifacts Array of folder artifacts being scanned
545
+ * @returns Array of findings grouped by resolved file path
546
+ */
547
+ aggregateFindingsByFile(findings, fileArtifacts, folderArtifacts) {
548
+ const aggregatedCodeScanIssueMap = new Map();
549
+ for (const finding of findings) {
550
+ const resolvedPath = this.resolveFilePath(finding.filePath, fileArtifacts, folderArtifacts);
551
+ if (resolvedPath) {
552
+ if (aggregatedCodeScanIssueMap.has(resolvedPath)) {
553
+ aggregatedCodeScanIssueMap.get(resolvedPath)?.push(finding);
554
+ }
555
+ else {
556
+ aggregatedCodeScanIssueMap.set(resolvedPath, [finding]);
557
+ }
558
+ }
559
+ else {
560
+ this.logging.warn(`Could not resolve finding file path: ${finding.filePath}`);
561
+ }
562
+ }
563
+ return Array.from(aggregatedCodeScanIssueMap.entries()).map(([filePath, issues]) => ({
564
+ filePath,
565
+ issues,
566
+ }));
567
+ }
568
+ /**
569
+ * Resolve finding file path to actual file path
570
+ * @param findingPath Relative file path from the finding
571
+ * @param fileArtifacts Array of file artifacts being scanned
572
+ * @param folderArtifacts Array of folder artifacts being scanned
573
+ * @returns Resolved absolute file path or null if not found
574
+ */
575
+ resolveFilePath(findingPath, fileArtifacts, folderArtifacts) {
576
+ // 1. Check if finding path matches one of the file artifacts
577
+ for (const fileArtifact of fileArtifacts) {
578
+ const normalizedFilePath = path.normalize(fileArtifact.path);
579
+ const normalizedFindingPath = path.normalize(findingPath);
580
+ if (normalizedFilePath.endsWith(normalizedFindingPath)) {
581
+ return normalizedFilePath;
582
+ }
583
+ }
584
+ // 2. Check if finding path falls under one of the folder artifacts
585
+ for (const folderArtifact of folderArtifacts) {
586
+ const normalizedFolderPath = path.normalize(folderArtifact.path);
587
+ const normalizedFindingPath = path.normalize(findingPath);
588
+ // 2.1. Check if finding path falls under one of the subdirectories in folder artifact path
589
+ const folderSegments = normalizedFolderPath.split(path.sep);
590
+ // Find common suffix between folder path and finding path
591
+ let matchIndex = -1;
592
+ for (let i = folderSegments.length - 1; i >= 0; i--) {
593
+ const folderSuffix = folderSegments.slice(i).join(path.sep);
594
+ if (normalizedFindingPath.startsWith(folderSuffix + path.sep)) {
595
+ matchIndex = i;
596
+ break;
597
+ }
598
+ }
599
+ // If common suffix is found, create the absolute path with it
600
+ if (matchIndex !== -1) {
601
+ const remainingPath = normalizedFindingPath.substring(folderSegments.slice(matchIndex).join(path.sep).length + 1);
602
+ const absolutePath = path.join(normalizedFolderPath, remainingPath);
603
+ if ((0, fs_1.existsSync)(absolutePath) && (0, fs_1.statSync)(absolutePath).isFile()) {
604
+ return absolutePath;
605
+ }
606
+ }
607
+ // 2.2. Check if folder path + finding path gives the absolute file path
608
+ const filePath = path.join(folderArtifact.path, findingPath);
609
+ if ((0, fs_1.existsSync)(filePath) && (0, fs_1.statSync)(filePath).isFile()) {
610
+ return filePath;
611
+ }
612
+ }
613
+ // 3. Check if finding already has absolute file path
614
+ const maybeAbsolutePath = path.normalize(findingPath);
615
+ if ((0, fs_1.existsSync)(maybeAbsolutePath) && (0, fs_1.statSync)(maybeAbsolutePath).isFile()) {
616
+ return maybeAbsolutePath;
617
+ }
618
+ return null;
619
+ }
620
+ /**
621
+ * Checks if the operation has been cancelled by the user
622
+ * @param message Optional message to include in the cancellation error
623
+ * @throws Error if the operation has been cancelled
624
+ */
625
+ checkCancellation(message = 'Command execution cancelled') {
626
+ qCodeReviewUtils_1.QCodeReviewUtils.checkCancellation(this.cancellationToken, this.logging, message);
627
+ }
628
+ }
629
+ exports.QCodeReview = QCodeReview;
630
+ //# sourceMappingURL=qCodeReview.js.map