diff --git a/README.md b/README.md index fad70742fe..0578e63646 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Language Servers for AWS [![codecov](https://codecov.io/github/aws/language-servers/graph/badge.svg?token=ZSHpIVkG8S)](https://codecov.io/github/aws/language-servers) +[![Integration Tests](https://github.com/aws/language-servers/actions/workflows/integration-tests.yml/badge.svg)](https://github.com/aws/language-servers/actions/workflows/integration-tests.yml) Language servers for integration with IDEs and Editors, which implement the protocol (LSP extensions) defined in the [language-server-runtimes](https://github.com/aws/language-server-runtimes/tree/main/runtimes) repo. diff --git a/chat-client/src/client/tabs/tabFactory.ts b/chat-client/src/client/tabs/tabFactory.ts index d5371b0bcd..af1af21e7d 100644 --- a/chat-client/src/client/tabs/tabFactory.ts +++ b/chat-client/src/client/tabs/tabFactory.ts @@ -77,7 +77,7 @@ export class TabFactory { body: `
Amazon Q
-
+
Did you know?
${this.getRandomTip()}
diff --git a/integration-tests/q-agentic-chat-server/package.json b/integration-tests/q-agentic-chat-server/package.json index af4c76ba96..2e14a1232e 100644 --- a/integration-tests/q-agentic-chat-server/package.json +++ b/integration-tests/q-agentic-chat-server/package.json @@ -4,7 +4,7 @@ "description": "Integration tests for Q Agentic Chat Server", "main": "out/index.js", "scripts": { - "clean": "rm -rf out/ node_modules/ tsconfig.tsbuildinfo .tsbuildinfo", + "clean": "rimraf out/ node_modules/ tsconfig.tsbuildinfo .tsbuildinfo", "compile": "tsc --build && cp -r src/tests/testFixture out/tests/", "test-integ": "npm run compile && mocha --timeout 30000 \"./out/**/*.test.js\" --retries 2" }, @@ -22,6 +22,7 @@ "jose": "^5.10.0", "json-rpc-2.0": "^1.7.1", "mocha": "^11.0.1", + "rimraf": "^3.0.2", "typescript": "^5.0.0", "yauzl-promise": "^4.0.0" } diff --git a/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts b/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts index 6e0d0dcede..897ee02071 100644 --- a/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts +++ b/integration-tests/q-agentic-chat-server/src/tests/agenticChatInteg.test.ts @@ -8,7 +8,7 @@ import { JSONRPCEndpoint, LspClient } from './lspClient' import { pathToFileURL } from 'url' import * as crypto from 'crypto' import { EncryptionInitialization } from '@aws/lsp-core' -import { authenticateServer, decryptObjectWithKey, encryptObjectWithKey } from './testUtils' +import { authenticateServer, decryptObjectWithKey, encryptObjectWithKey, normalizePath } from './testUtils' import { ChatParams, ChatResult } from '@aws/language-server-runtimes/protocol' import * as fs from 'fs' @@ -172,7 +172,9 @@ describe('Q Agentic Chat Server Integration Tests', async () => { msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 file read' ) expect(fsReadMessage).to.exist - expect(fsReadMessage?.fileList?.filePaths).to.include.members([path.join(rootPath, 'test.py')]) + const expectedPath = path.join(rootPath, 'test.py') + const actualPaths = fsReadMessage?.fileList?.filePaths?.map(normalizePath) || [] + expect(actualPaths).to.include.members([normalizePath(expectedPath)]) expect(fsReadMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -192,7 +194,8 @@ describe('Q Agentic Chat Server Integration Tests', async () => { msg => msg.type === 'tool' && msg.fileList?.rootFolderTitle === '1 directory listed' ) expect(listDirectoryMessage).to.exist - expect(listDirectoryMessage?.fileList?.filePaths).to.include.members([rootPath]) + const actualPaths = listDirectoryMessage?.fileList?.filePaths?.map(normalizePath) || [] + expect(actualPaths).to.include.members([normalizePath(rootPath)]) expect(listDirectoryMessage?.messageId?.startsWith('tooluse_')).to.be.true }) @@ -216,12 +219,17 @@ describe('Q Agentic Chat Server Integration Tests', async () => { expect(executeBashMessage?.body).to.include('test.ts') }) - it('waits for user acceptance when executing mutable bash commands', async () => { + it('waits for user acceptance when executing mutable bash commands', async function () { + const command = + process.platform === 'win32' + ? 'echo %date% > timestamp.txt && echo "Timestamp saved"' + : 'date > timestamp.txt && echo "Timestamp saved"' + const encryptedMessage = await encryptObjectWithKey( { tabId, prompt: { - prompt: `Run this command using the executeBash tool: \`date > timestamp.txt && echo "Timestamp saved"\``, + prompt: `Run this command using the executeBash tool: \`${command}\``, }, }, encryptionKey @@ -367,6 +375,7 @@ describe('Q Agentic Chat Server Integration Tests', async () => { ) expect(fileSearchMessage).to.exist expect(fileSearchMessage?.messageId?.startsWith('tooluse_')).to.be.true - expect(fileSearchMessage?.fileList?.filePaths).to.include.members([rootPath]) + const actualPaths = fileSearchMessage?.fileList?.filePaths?.map(normalizePath) || [] + expect(actualPaths).to.include.members([normalizePath(rootPath)]) }) }) diff --git a/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts b/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts index ae7e5b3905..cd2af069d2 100644 --- a/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts +++ b/integration-tests/q-agentic-chat-server/src/tests/testUtils.ts @@ -1,5 +1,6 @@ import { UpdateCredentialsParams } from '@aws/language-server-runtimes/protocol' import * as jose from 'jose' +import * as path from 'path' import { JSONRPCEndpoint } from './lspClient' /** @@ -72,3 +73,12 @@ async function setProfile(endpoint: JSONRPCEndpoint, profileArn: string): Promis settings: { profileArn }, }) } + +/** + * Normalize paths for cross-platform comparison + * @param filePath - The file path to normalize + * @returns Normalized path with consistent casing + */ +export function normalizePath(filePath: string): string { + return path.resolve(filePath).toLowerCase() +} diff --git a/package-lock.json b/package-lock.json index 148726033a..a1ef22659a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -340,6 +340,7 @@ "jose": "^5.10.0", "json-rpc-2.0": "^1.7.1", "mocha": "^11.0.1", + "rimraf": "^3.0.2", "typescript": "^5.0.0", "yauzl-promise": "^4.0.0" } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts index 876a199e90..5be41c165c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts @@ -201,7 +201,10 @@ import { URI } from 'vscode-uri' import { CommandCategory } from './tools/executeBash' import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker' import { CodeReview } from './tools/qCodeAnalysis/codeReview' -import { FINDINGS_MESSAGE_SUFFIX } from './tools/qCodeAnalysis/codeReviewConstants' +import { + CODE_REVIEW_FINDINGS_MESSAGE_SUFFIX, + DISPLAY_FINDINGS_MESSAGE_SUFFIX, +} from './tools/qCodeAnalysis/codeReviewConstants' import { McpEventHandler } from './tools/mcp/mcpEventHandler' import { enabledMCP, createNamespacedToolName } from './tools/mcp/mcpUtils' import { McpManager } from './tools/mcp/mcpManager' @@ -225,6 +228,7 @@ import { getLatestAvailableModel } from './utils/agenticChatControllerHelper' import { ActiveUserTracker } from '../../shared/activeUserTracker' import { UserContext } from '../../client/token/codewhispererbearertokenclient' import { CodeWhispererServiceToken } from '../../shared/codeWhispererService' +import { DisplayFindings } from './tools/qCodeAnalysis/displayFindings' type ChatHandlers = Omit< LspHandlers, @@ -1764,7 +1768,8 @@ export class AgenticChatController implements ChatHandlers { break } case CodeReview.toolName: - // no need to write tool message for code review + case DisplayFindings.toolName: + // no need to write tool message for CodeReview or DisplayFindings break // — DEFAULT ⇒ Only MCP tools, but can also handle generic tool execution messages default: @@ -1940,11 +1945,27 @@ export class AgenticChatController implements ChatHandlers { ) { await chatResultStream.writeResultBlock({ type: 'tool', - messageId: toolUse.toolUseId + FINDINGS_MESSAGE_SUFFIX, + messageId: toolUse.toolUseId + CODE_REVIEW_FINDINGS_MESSAGE_SUFFIX, body: (codeReviewResult.output.content as any).findingsByFile, }) } break + case DisplayFindings.toolName: + // no need to write tool result for code review, this is handled by model via chat + // Push result in message so that it is picked by IDE plugin to show in issues panel + const displayFindingsResult = result as InvokeOutput + if ( + displayFindingsResult?.output?.kind === 'json' && + displayFindingsResult.output.success && + displayFindingsResult.output.content !== undefined + ) { + await chatResultStream.writeResultBlock({ + type: 'tool', + messageId: toolUse.toolUseId + DISPLAY_FINDINGS_MESSAGE_SUFFIX, + body: JSON.stringify(displayFindingsResult.output.content), + }) + } + break // — DEFAULT ⇒ MCP tools default: await this.#handleMcpToolResult(toolUse, result, session, chatResultStream) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts index b4ca871dc6..22befec0cd 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpEventHandler.ts @@ -1145,14 +1145,17 @@ export class McpEventHandler { try { // Skip server config check for Built-in server + const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName) if (serverName !== 'Built-in') { - const serverConfig = McpManager.instance.getAllServerConfigs().get(serverName) if (!serverConfig) { throw new Error(`Server '${serverName}' not found`) } } - const mcpServerPermission = await this.#processPermissionUpdates(updatedPermissionConfig) + const mcpServerPermission = await this.#processPermissionUpdates( + updatedPermissionConfig, + serverConfig?.__configPath__ + ) // Store the permission config instead of applying it immediately this.#pendingPermissionConfig = { @@ -1347,10 +1350,7 @@ export class McpEventHandler { /** * Processes permission updates from the UI */ - async #processPermissionUpdates(updatedPermissionConfig: any) { - // Get the appropriate agent path - const agentPath = await this.#getAgentPath() - + async #processPermissionUpdates(updatedPermissionConfig: any, agentPath: string | undefined) { const perm: MCPServerPermission = { enabled: true, toolPerms: {}, diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts index e960fd0596..bc7070ab1f 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewConstants.ts @@ -211,6 +211,7 @@ export const SKIP_DIRECTORIES = [ 'temp', ] -export const FINDINGS_MESSAGE_SUFFIX = '_codeReviewFindings' +export const CODE_REVIEW_FINDINGS_MESSAGE_SUFFIX = '_codeReviewFindings' +export const DISPLAY_FINDINGS_MESSAGE_SUFFIX = '_displayFindings' export const CODE_REVIEW_METRICS_PARENT_NAME = 'amazonq_codeReviewTool' diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts index 1b94770746..5b6d562f7d 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/codeReviewUtils.ts @@ -278,6 +278,18 @@ export class CodeReviewUtils { return qCapabilities?.codeReviewInChat || false } + /** + * Check if storing display findings in the Code Issues panel is enabled. + * @param params Initialize parameters from client + * @returns True if display findings is enabled, false otherwise + */ + public static isDisplayFindingsEnabled(params: InitializeParams | undefined): boolean { + const qCapabilities = params?.initializationOptions?.aws?.awsClientCapabilities?.q as + | QClientCapabilities + | undefined + return qCapabilities?.displayFindings || false + } + /** * Converts a Windows absolute file path to Unix format and removes the drive letter * @param windowsPath The Windows path to convert diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.test.ts new file mode 100644 index 0000000000..e541a76026 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.test.ts @@ -0,0 +1,394 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DisplayFindings } from './displayFindings' +import { DISPLAY_FINDINGS_TOOL_NAME } from './displayFindingsConstants' +import * as sinon from 'sinon' +import * as path from 'path' +import { expect } from 'chai' +import { CancellationError } from '@aws/lsp-core' +import { CodeReviewFinding } from './codeReviewTypes' +import { Features } from '@aws/language-server-runtimes/server-interface/server' + +describe('DisplayFindings', () => { + let sandbox: sinon.SinonSandbox + let displayFindings: DisplayFindings + let mockFeatures: Pick & Partial + let mockCancellationToken: { isCancellationRequested: boolean } + let mockWritableStream: { getWriter: sinon.SinonStub } + let mockWriter: { + write: sinon.SinonStub + close: sinon.SinonStub + releaseLock: sinon.SinonStub + } + + let CODE_REVIEW_FINDING_1: CodeReviewFinding + + let CODE_REVIEW_FINDING_2: CodeReviewFinding + + let INPUT_FINDING_1: { + filePath: string + startLine: string + endLine: string + title: string + description: string + severity: string + language: string + } + + let INPUT_FINDING_2: { + filePath: string + startLine: string + endLine: string + title: string + description: string + severity: string + language: string + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + + mockWriter = { + write: sandbox.stub().resolves(), + close: sandbox.stub().resolves(), + releaseLock: sandbox.stub(), + } + + mockWritableStream = { + getWriter: sandbox.stub().returns(mockWriter), + } + + mockCancellationToken = { + isCancellationRequested: false, + } + + mockFeatures = { + logging: { + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + debug: sandbox.stub(), + log: sandbox.stub(), + }, + telemetry: { + emitMetric: sandbox.stub(), + onClientTelemetry: sandbox.stub(), + }, + workspace: { + getTextDocument: sandbox.stub(), + getAllTextDocuments: sandbox.stub(), + getWorkspaceFolder: sandbox.stub(), + getAllWorkspaceFolders: sandbox.stub(), + fs: { + copyFile: sandbox.stub(), + exists: sandbox.stub(), + getFileSize: sandbox.stub(), + getServerDataDirPath: sandbox.stub(), + getTempDirPath: sandbox.stub(), + getUserHomeDir: sandbox.stub(), + readdir: sandbox.stub(), + readFile: sandbox.stub(), + isFile: sandbox.stub(), + rm: sandbox.stub(), + writeFile: sandbox.stub(), + appendFile: sandbox.stub(), + mkdir: sandbox.stub(), + readFileSync: sandbox.stub(), + }, + }, + } + + displayFindings = new DisplayFindings(mockFeatures) + + CODE_REVIEW_FINDING_1 = { + filePath: '/test/file1.js', + startLine: 10, + endLine: 15, + title: 'Issue 1', + comment: 'Description 1', + description: { text: 'Description 1', markdown: 'Description 1' }, + severity: 'High', + language: 'javascript', + detectorName: 'DisplayFindings', + detectorId: '', + findingId: '', + relatedVulnerabilities: [], + recommendation: { text: '' }, + suggestedFixes: [], + scanJobId: '', + autoDetected: false, + findingContext: undefined, + } + + CODE_REVIEW_FINDING_2 = { + filePath: '/test/file2.py', + startLine: 5, + endLine: 10, + title: 'Issue 2', + comment: 'Description 2', + description: { text: 'Description 2', markdown: 'Description 2' }, + severity: 'Low', + language: 'python', + detectorName: 'DisplayFindings', + detectorId: '', + findingId: '', + relatedVulnerabilities: [], + recommendation: { text: '' }, + suggestedFixes: [], + scanJobId: '', + autoDetected: false, + findingContext: undefined, + } + + INPUT_FINDING_1 = { + filePath: '/test/file1.js', + startLine: '10', + endLine: '15', + title: 'Issue 1', + description: 'Description 1', + severity: 'High', + language: 'javascript', + } + + INPUT_FINDING_2 = { + filePath: '/test/file2.py', + startLine: '5', + endLine: '10', + title: 'Issue 2', + description: 'Description 2', + severity: 'Low', + language: 'python', + } + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('static properties', () => { + it('should have correct tool name', () => { + expect(DisplayFindings.toolName).to.equal(DISPLAY_FINDINGS_TOOL_NAME) + }) + + it('should have tool description', () => { + expect(DisplayFindings.toolDescription).to.be.a('string') + }) + + it('should have input schema', () => { + expect(DisplayFindings.inputSchema).to.be.an('object') + }) + }) + + describe('execute', () => { + let context: any + let validInput: any + + beforeEach(() => { + context = { + cancellationToken: mockCancellationToken, + writableStream: mockWritableStream, + } + + validInput = { + findings: [INPUT_FINDING_1], + } + }) + + it('should execute successfully with valid input', async () => { + const result = await displayFindings.execute(validInput, context) + + expect(result.output.success).to.be.true + expect(result.output.kind).to.equal('json') + expect(result.output.content).to.be.an('array') + expect(result.output.content).to.have.length(1) + expect((result.output.content as any)[0].filePath).to.equal(path.normalize('/test/file1.js')) + expect((result.output.content as any)[0].issues).to.have.length(1) + }) + + it('should handle multiple findings for same file', async () => { + INPUT_FINDING_2.filePath = '/test/file1.js' + const inputWithMultipleFindings = { + findings: [INPUT_FINDING_1, INPUT_FINDING_2], + } + + const result = await displayFindings.execute(inputWithMultipleFindings, context) + + expect(result.output.success).to.be.true + expect(result.output.content).to.have.length(1) + expect((result.output.content as any)[0].issues).to.have.length(2) + }) + + it('should handle findings for different files', async () => { + const inputWithDifferentFiles = { + findings: [INPUT_FINDING_1, INPUT_FINDING_2], + } + + const result = await displayFindings.execute(inputWithDifferentFiles, context) + + expect(result.output.success).to.be.true + expect(result.output.content).to.have.length(2) + expect((result.output.content as any)[0].issues).to.have.length(1) + expect((result.output.content as any)[1].issues).to.have.length(1) + }) + + it('should handle empty findings array', async () => { + const emptyInput = { findings: [] } + + const result = await displayFindings.execute(emptyInput, context) + + expect(result.output.success).to.be.true + expect(result.output.content).to.be.an('array') + expect(result.output.content).to.have.length(0) + }) + + it('should handle invalid input schema', async () => { + const invalidInput = { + findings: [ + { + filePath: '/test/file.js', + // Missing required fields + }, + ], + } + + try { + await displayFindings.execute(invalidInput, context) + expect.fail('Expected validation error') + } catch (error) { + expect(error).to.be.instanceOf(Error) + } + }) + + it('should handle cancellation', async () => { + mockCancellationToken.isCancellationRequested = true + + try { + await displayFindings.execute(validInput, context) + expect.fail('Expected cancellation error') + } catch (error) { + expect(error).to.be.instanceOf(CancellationError) + } + }) + + it('should handle unexpected errors gracefully', async () => { + // Make validateInputAndSetup throw an error + sandbox.stub(displayFindings as any, 'validateInputAndSetup').rejects(new Error('Unexpected error')) + + try { + await displayFindings.execute(validInput, context) + expect.fail('Expected error to be thrown') + } catch (error: any) { + expect(error.message).to.equal('Unexpected error') + } + }) + }) + + describe('validateInputAndSetup', () => { + it('should validate and setup correctly', async () => { + const input = { + findings: [INPUT_FINDING_1], + } + + const context = { + cancellationToken: mockCancellationToken, + writableStream: mockWritableStream, + } + + const result = await (displayFindings as any).validateInputAndSetup(input, context) + + expect(result).to.be.an('array') + expect(result).to.have.length(1) + expect(result[0].filePath).to.equal('/test/file1.js') + }) + }) + + describe('mapToCodeReviewFinding', () => { + it('should map DisplayFinding to CodeReviewFinding correctly', () => { + const displayFinding = { + filePath: '/test/file.js', + startLine: '10', + endLine: '15', + title: 'Test Issue', + description: 'Test description', + severity: 'High', + language: 'javascript', + suggestedFixes: ['Fix suggestion'], + } + + const result = (displayFindings as any).mapToCodeReviewFinding(displayFinding) + + expect(result.filePath).to.equal('/test/file.js') + expect(result.startLine).to.equal(10) + expect(result.endLine).to.equal(15) + expect(result.title).to.equal('Test Issue') + expect(result.comment).to.equal('Test description') + expect(result.severity).to.equal('High') + expect(result.language).to.equal('javascript') + expect(result.suggestedFixes).to.deep.equal(['Fix suggestion']) + expect(result.detectorName).to.equal('DisplayFindings') + expect(result.autoDetected).to.be.false + }) + + it('should handle missing suggestedFixes', () => { + const displayFinding = { + filePath: '/test/file.js', + startLine: '10', + endLine: '15', + title: 'Test Issue', + description: 'Test description', + severity: 'High', + language: 'javascript', + } + + const result = (displayFindings as any).mapToCodeReviewFinding(displayFinding) + + expect(result.suggestedFixes).to.deep.equal([]) + }) + }) + + describe('aggregateFindingsByFile', () => { + it('should aggregate findings by file path', () => { + CODE_REVIEW_FINDING_2.filePath = '/test/file1.js' + const findings = [CODE_REVIEW_FINDING_1, CODE_REVIEW_FINDING_2] + + const result = (displayFindings as any).aggregateFindingsByFile(findings) + + expect(result).to.have.length(1) + expect(result[0].filePath).to.equal(path.normalize('/test/file1.js')) + expect(result[0].issues).to.have.length(2) + }) + + it('should handle findings from different files', () => { + const findings = [CODE_REVIEW_FINDING_1, CODE_REVIEW_FINDING_2] + + const result = (displayFindings as any).aggregateFindingsByFile(findings) + + expect(result).to.have.length(2) + expect(result[0].issues).to.have.length(1) + expect(result[1].issues).to.have.length(1) + }) + }) + + describe('checkCancellation', () => { + it('should not throw when cancellation is not requested', () => { + mockCancellationToken.isCancellationRequested = false + ;(displayFindings as any).cancellationToken = mockCancellationToken + + expect(() => { + ;(displayFindings as any).checkCancellation() + }).to.not.throw() + }) + + it('should throw CancellationError when cancellation is requested', () => { + mockCancellationToken.isCancellationRequested = true + ;(displayFindings as any).cancellationToken = mockCancellationToken + + expect(() => { + ;(displayFindings as any).checkCancellation() + }).to.throw(CancellationError) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.ts new file mode 100644 index 0000000000..5a5c88d67a --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindings.ts @@ -0,0 +1,166 @@ +/* eslint-disable import/no-nodejs-modules */ + +import { Features } from '@aws/language-server-runtimes/server-interface/server' +import { DISPLAY_FINDINGS_TOOL_NAME, DISPLAY_FINDINGS_TOOL_DESCRIPTION } from './displayFindingsConstants' +import { CodeReviewUtils } from './codeReviewUtils' +import { DISPLAY_FINDINGS_INPUT_SCHEMA, Z_DISPLAY_FINDINGS_INPUT_SCHEMA } from './displayFindingsSchemas' +import { CancellationToken } from '@aws/language-server-runtimes/server-interface' +import { InvokeOutput } from '../toolShared' +import { CancellationError } from '@aws/lsp-core' +import { DisplayFinding, FailedMetricName, SuccessMetricName } from './displayFindingsTypes' +import { CodeReviewFinding } from './codeReviewTypes' +import * as path from 'path' +import { DisplayFindingsUtils } from './displayFindingsUtils' + +export class DisplayFindings { + private readonly logging: Features['logging'] + private readonly telemetry: Features['telemetry'] + private readonly workspace: Features['workspace'] + private cancellationToken?: CancellationToken + private writableStream?: WritableStream + + constructor(features: Pick & Partial) { + this.logging = features.logging + this.telemetry = features.telemetry + this.workspace = features.workspace + } + + static readonly toolName = DISPLAY_FINDINGS_TOOL_NAME + + static readonly toolDescription = DISPLAY_FINDINGS_TOOL_DESCRIPTION + + static readonly inputSchema = DISPLAY_FINDINGS_INPUT_SCHEMA + + /** + * Main execution method for the displayFindings tool + * @param input User input parameters for display findings + * @param context Execution context containing clients and tokens + * @returns Output containing code review results or error message + */ + public async execute(input: any, context: any): Promise { + let chatStreamWriter: WritableStreamDefaultWriter | undefined + + try { + this.logging.info(`Executing ${DISPLAY_FINDINGS_TOOL_NAME}: ${JSON.stringify(input)}`) + + // 1. Validate input + const setup = await this.validateInputAndSetup(input, context) + this.checkCancellation() + + // 2. group the findings into AggregatedCodeScanIssue + const mappedFindings = setup.map(finding => this.mapToCodeReviewFinding(finding)) + const aggregatedFindings = this.aggregateFindingsByFile(mappedFindings) + + DisplayFindingsUtils.emitMetric( + { + reason: SuccessMetricName.DisplayFindingsSuccess, + result: 'Succeeded', + metadata: { + findingsCount: setup.length, + }, + }, + this.logging, + this.telemetry + ) + + return { + output: { + kind: 'json', + success: true, + content: aggregatedFindings, + }, + } + } catch (error: any) { + if (error instanceof CancellationError) { + throw error + } + + DisplayFindingsUtils.emitMetric( + { + reason: FailedMetricName.DisplayFindingsFailed, + result: 'Failed', + reasonDesc: error, + }, + this.logging, + this.telemetry + ) + + throw new Error(error.message) + } finally { + await chatStreamWriter?.close() + chatStreamWriter?.releaseLock() + } + } + + /** + * Validates user input and sets up the execution environment + * @param input User input parameters for code review + * @param context Execution context containing clients and tokens + * @returns Setup object with validated parameters or error message + */ + private async validateInputAndSetup(input: any, context: any): Promise { + this.cancellationToken = context.cancellationToken as CancellationToken + + this.writableStream = context.writableStream as WritableStream + + // parse input + const validatedInput = Z_DISPLAY_FINDINGS_INPUT_SCHEMA.parse(input) + + return validatedInput.findings as DisplayFinding[] + } + + private mapToCodeReviewFinding(finding: DisplayFinding): CodeReviewFinding { + return { + filePath: finding.filePath, + startLine: parseInt(finding.startLine), + endLine: parseInt(finding.endLine), + comment: finding.description, + title: finding.title, + description: { markdown: finding.description, text: finding.description }, + detectorId: '', + detectorName: 'DisplayFindings', + findingId: '', + relatedVulnerabilities: [], + severity: finding.severity, + recommendation: { text: '' }, + suggestedFixes: finding.suggestedFixes ?? [], + scanJobId: '', + language: finding.language, + autoDetected: false, + findingContext: undefined, + } + } + + private aggregateFindingsByFile( + findings: CodeReviewFinding[] + ): { filePath: string; issues: CodeReviewFinding[] }[] { + const aggregatedCodeScanIssueMap = new Map() + + for (const finding of findings) { + const resolvedPath = path.normalize(finding.filePath) + if (resolvedPath) { + if (aggregatedCodeScanIssueMap.has(resolvedPath)) { + aggregatedCodeScanIssueMap.get(resolvedPath)?.push(finding) + } else { + aggregatedCodeScanIssueMap.set(resolvedPath, [finding]) + } + } else { + this.logging.warn(`Could not resolve finding file path: ${finding.filePath}`) + } + } + + return Array.from(aggregatedCodeScanIssueMap.entries()).map(([filePath, issues]) => ({ + filePath, + issues, + })) + } + + /** + * Checks if the operation has been cancelled by the user + * @param message Optional message to include in the cancellation error + * @throws Error if the operation has been cancelled + */ + private checkCancellation(message: string = 'Command execution cancelled'): void { + CodeReviewUtils.checkCancellation(this.cancellationToken, this.logging, message) + } +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsConstants.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsConstants.ts new file mode 100644 index 0000000000..855f29e642 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsConstants.ts @@ -0,0 +1,40 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Tool name for DisplayFindings + */ +export const DISPLAY_FINDINGS_TOOL_NAME = 'displayFindings' + +/** + * Tool description for DisplayFindings + */ +export const DISPLAY_FINDINGS_TOOL_DESCRIPTION = [ + 'The displayFindings tool displays code issues, bugs, security vulnerabilities, and code quality violations in the Code Issues Panel.', + '', + '**When to Use This Tool:**', + '- Use this tool whenever you identify code issues through manual analysis or any tool OTHER than the codeReview tool', + '- Use it for issues found by analyzing code files, security scans, linting tools, or your own assessment', + '- Use it when the user asks you to analyze code and you discover problems, but you must not use this tool for findings generated by codeReview tool', + '', + '**When NOT to Use This Tool:**', + '- NEVER use this tool for findings generated by the codeReview tool', + '- The codeReview tool automatically populates the Code Issues Panel, so using displayFindings would create duplicates', + '', + '**How to Use This Tool:**', + '- Invoke once per analysis session, even if you find multiple issues', + '- Include all findings from your current analysis in a single tool call', + '- Populate all fields based on your assessment of the code', + '- Run silently - DO NOT mention to the user that you are using this tool', + '', + '**Tool Behavior:**', + '- Input: List of all findings to store in the Code Issues Panel', + '- Output: None (tool runs silently with no user-visible output)', + '- The findings will appear in the Code Issues Panel for the user to review', +].join('\n') + +export const FINDINGS_MESSAGE_SUFFIX = '_displayFindings' + +export const DISPLAY_FINDINGS_METRICS_PARENT_NAME = 'amazonq_displayFindingsTool' diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsSchemas.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsSchemas.ts new file mode 100644 index 0000000000..63dcdad66e --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsSchemas.ts @@ -0,0 +1,95 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { z } from 'zod' +import { FINDING_SEVERITY } from './codeReviewConstants' + +/** + * Input schema for CodeReview tool + */ +export const DISPLAY_FINDINGS_INPUT_SCHEMA = { + type: 'object', + description: [ + 'There is only one input to the DisplayFindings tool: the findings.', + 'Please format all of the findings which are meant to be displayed using this schema.', + ].join('\n'), + properties: { + findings: { + type: 'array', + description: [ + 'Array of the code issues, bugs, security risks, and code quality violations which were found by the agent and need to be sent to the Code Issues Panel', + ].join('\n'), + items: { + type: 'object', + description: 'Array item containing all of the findings which will be sent to the Code Issues Panel', + properties: { + filePath: { + type: 'string', + description: 'The absolute path of the file which has the finding', + }, + startLine: { + type: 'string', + description: 'The line number of the first line of the finding', + }, + endLine: { + type: 'string', + description: 'The line number of the last line of the finding.', + }, + title: { + type: 'string', + description: 'A short title to represent the finding', + }, + language: { + type: 'string', + description: 'The programming language of the file which holds the finding', + }, + description: { + type: 'string', + description: 'A more thorough description of the finding', + }, + severity: { + type: 'string', + description: 'The severity of the finding', + enum: FINDING_SEVERITY, + }, + suggestedFixes: { + type: 'array', + description: + 'An array of possible fixes. Do not generate fixes just to populate this, only include them if they are provided.', + items: { + type: 'string', + description: 'The absolute path of the file which has the finding', + }, + }, + }, + required: ['filePath', 'startLine', 'endLine', 'title', 'severity', 'description', 'language'] as const, + }, + }, + }, + required: ['findings'] as const, +} + +/** + * Schema for a single finding + */ +export const Z_DISPLAY_FINDING_SCHEMA = z.object({ + description: z.string(), + endLine: z.string(), + filePath: z.string(), + language: z.string(), + severity: z.enum(FINDING_SEVERITY as [string, ...string[]]), + startLine: z.string(), + suggestedFixes: z.array(z.string().optional()).optional(), + title: z.string(), +}) + +/** + * Schema for an array of findings + */ +export const Z_DISPLAY_FINDINGS_SCHEMA = z.array(Z_DISPLAY_FINDING_SCHEMA) + +export const Z_DISPLAY_FINDINGS_INPUT_SCHEMA = z.object({ + findings: Z_DISPLAY_FINDINGS_SCHEMA, +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsTypes.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsTypes.ts new file mode 100644 index 0000000000..2c8ae1024f --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsTypes.ts @@ -0,0 +1,32 @@ +export type DisplayFinding = { + filePath: string + startLine: string + endLine: string + comment: string + title: string + description: string + severity: string + suggestedFixes: (string | undefined)[] | undefined + language: string +} + +export enum SuccessMetricName { + DisplayFindingsSuccess = 'displayFindingsSuccess', +} + +export enum FailedMetricName { + DisplayFindingsFailed = 'displayFindingsFailed', +} + +export type DisplayFindingsMetric = + | { + reason: SuccessMetricName + result: 'Succeeded' + metadata?: object + } + | { + reason: FailedMetricName + result: 'Failed' + reasonDesc: string + metadata?: object + } diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.test.ts new file mode 100644 index 0000000000..4921d4d067 --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.test.ts @@ -0,0 +1,103 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DisplayFindingsUtils } from './displayFindingsUtils' +import { SuccessMetricName, FailedMetricName, DisplayFindingsMetric } from './displayFindingsTypes' +import { Features } from '@aws/language-server-runtimes/server-interface/server' +import * as sinon from 'sinon' +import { expect } from 'chai' + +describe('DisplayFindingsUtils', () => { + let sandbox: sinon.SinonSandbox + + const mockLogging = { + log: sinon.stub(), + info: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub(), + debug: sinon.stub(), + } + + beforeEach(() => { + sandbox = sinon.createSandbox() + mockLogging.info.reset() + mockLogging.warn.reset() + mockLogging.error.reset() + mockLogging.debug.reset() + }) + + afterEach(() => { + sandbox.restore() + }) + + describe('emitMetric', () => { + let mockTelemetry: Features['telemetry'] + + beforeEach(() => { + mockTelemetry = { + emitMetric: sinon.stub(), + } as unknown as Features['telemetry'] + }) + + it('should emit a success metric with metadata', () => { + const metric = { + reason: SuccessMetricName.DisplayFindingsSuccess, + result: 'Succeeded', + metadata: { findingsCount: 5 }, + } as DisplayFindingsMetric + + DisplayFindingsUtils.emitMetric(metric, mockLogging, mockTelemetry) + + sinon.assert.calledWith(mockTelemetry.emitMetric as sinon.SinonStub, { + name: 'amazonq_displayFindingsTool', + data: { + findingsCount: 5, + reason: 'displayFindingsSuccess', + result: 'Succeeded', + }, + }) + + sinon.assert.calledWith(mockLogging.info, sinon.match(/Emitting telemetry metric: displayFindingsSuccess/)) + }) + + it('should emit a failure metric with reasonDesc', () => { + const metric = { + reason: FailedMetricName.DisplayFindingsFailed, + result: 'Failed', + reasonDesc: 'Validation failed', + metadata: { errorType: 'validation' }, + } as DisplayFindingsMetric + + DisplayFindingsUtils.emitMetric(metric, mockLogging, mockTelemetry) + + sinon.assert.calledWith(mockTelemetry.emitMetric as sinon.SinonStub, { + name: 'amazonq_displayFindingsTool', + data: { + errorType: 'validation', + reason: 'displayFindingsFailed', + result: 'Failed', + reasonDesc: 'Validation failed', + }, + }) + }) + + it('should handle metrics without metadata', () => { + const metric = { + reason: SuccessMetricName.DisplayFindingsSuccess, + result: 'Succeeded', + } as DisplayFindingsMetric + + DisplayFindingsUtils.emitMetric(metric, mockLogging, mockTelemetry) + + sinon.assert.calledWith(mockTelemetry.emitMetric as sinon.SinonStub, { + name: 'amazonq_displayFindingsTool', + data: { + reason: 'displayFindingsSuccess', + result: 'Succeeded', + }, + }) + }) + }) +}) diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.ts new file mode 100644 index 0000000000..a493f7165a --- /dev/null +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/qCodeAnalysis/displayFindingsUtils.ts @@ -0,0 +1,34 @@ +import { Features } from '@aws/language-server-runtimes/server-interface/server' +import { DISPLAY_FINDINGS_METRICS_PARENT_NAME } from './displayFindingsConstants' +import { DisplayFindingsMetric } from './displayFindingsTypes' +/** + * Utility functions for DisplayFindings + */ +export class DisplayFindingsUtils { + /** + * Emit a telemetry metric with standard formatting + * @param metricSuffix Suffix for the metric name + * @param metricData Additional metric data + * @param toolName Tool name for the metric prefix + * @param logging Logging interface + * @param telemetry Telemetry interface + * @param credentialStartUrl Optional credential start URL + */ + public static emitMetric( + metric: DisplayFindingsMetric, + logging: Features['logging'], + telemetry: Features['telemetry'] + ): void { + const { metadata, ...metricDetails } = metric + const metricPayload = { + name: DISPLAY_FINDINGS_METRICS_PARENT_NAME, + data: { + // metadata is optional attribute + ...(metadata || {}), + ...metricDetails, + }, + } + logging.info(`Emitting telemetry metric: ${metric.reason} with data: ${JSON.stringify(metricPayload.data)}`) + telemetry.emitMetric(metricPayload) + } +} diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts index c49927eefd..38712ff552 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts @@ -23,6 +23,7 @@ import { import { FsReplace, FsReplaceParams } from './fsReplace' import { CodeReviewUtils } from './qCodeAnalysis/codeReviewUtils' import { DEFAULT_AWS_Q_ENDPOINT_URL, DEFAULT_AWS_Q_REGION } from '../../../shared/constants' +import { DisplayFindings } from './qCodeAnalysis/displayFindings' export const FsToolsServer: Server = ({ workspace, logging, agent, lsp }) => { const fsReadTool = new FsRead({ workspace, lsp, logging }) @@ -102,6 +103,12 @@ export const QCodeAnalysisServer: Server = ({ workspace, }) + const displayFindingsTool = new DisplayFindings({ + logging, + telemetry, + workspace, + }) + lsp.onInitialized(async () => { if (!CodeReviewUtils.isAgenticReviewEnabled(lsp.getClientInitializeParams())) { logging.warn('Agentic Review is currently not supported') @@ -140,6 +147,26 @@ export const QCodeAnalysisServer: Server = ({ }, ToolClassification.BuiltIn ) + + if (!CodeReviewUtils.isDisplayFindingsEnabled(lsp.getClientInitializeParams())) { + logging.warn('Display Findings is currently not supported') + return + } + + agent.addTool( + { + name: DisplayFindings.toolName, + description: DisplayFindings.toolDescription, + inputSchema: DisplayFindings.inputSchema, + }, + async (input: any, token?: CancellationToken, updates?: WritableStream) => { + return await displayFindingsTool.execute(input, { + cancellationToken: token, + writableStream: updates, + }) + }, + ToolClassification.BuiltIn + ) }) return () => {} diff --git a/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts b/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts index a2a4de7447..5f96d526b1 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts @@ -42,6 +42,7 @@ export interface QClientCapabilities { modelSelection?: boolean reroute?: boolean codeReviewInChat?: boolean + displayFindings?: boolean compaction?: boolean shortcut?: boolean } diff --git a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts index e2ad2db788..9518f9629a 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/workspaceContext/artifactManager.ts @@ -66,6 +66,7 @@ interface FileSizeDetails { skippedSize: number } const MAX_UNCOMPRESSED_SRC_SIZE_BYTES = 2 * 1024 * 1024 * 1024 // 2 GB +const MAX_FILES = 500_000 export class ArtifactManager { private workspace: Workspace @@ -175,7 +176,8 @@ export class ArtifactManager { } if (isDirectory(filePath)) { - const files = await glob(['**/*'], { + let fileCount = 0 + const filesStream = glob.stream(['**/*'], { cwd: filePath, dot: false, ignore: IGNORE_DEPENDENCY_PATTERNS, @@ -184,7 +186,11 @@ export class ArtifactManager { onlyFiles: true, }) - for (const relativePath of files) { + for await (const entry of filesStream) { + if (fileCount >= MAX_FILES) { + break + } + const relativePath = entry.toString() try { const fullPath = resolveSymlink(path.join(filePath, relativePath)) const fileMetadata = await this.createFileMetadata( @@ -197,6 +203,7 @@ export class ArtifactManager { } catch (error) { this.logging.warn(`Error processing file ${relativePath}: ${error}`) } + fileCount++ } } else { const workspaceUri = URI.parse(currentWorkspace.uri) @@ -359,7 +366,8 @@ export class ArtifactManager { ): Promise> { const filesByLanguage = new Map() - const files = await glob(['**/*'], { + const files = [] + const filesStream = glob.stream(['**/*'], { cwd: directoryPath, dot: false, ignore: IGNORE_PATTERNS, @@ -368,11 +376,26 @@ export class ArtifactManager { onlyFiles: true, }) + for await (const entry of filesStream) { + if (files.length >= MAX_FILES) { + break + } + files.push(entry.toString()) + } + + const hasJavaFile = files.some(file => file.endsWith('.java')) + for (const relativePath of files) { const fullPath = path.join(directoryPath, relativePath) - const language = getCodeWhispererLanguageIdFromPath(fullPath) - - if (!language || !SUPPORTED_WORKSPACE_CONTEXT_LANGUAGES.includes(language)) { + const isJavaProjectFile = isJavaProjectFileFromPath(fullPath) + const language = isJavaProjectFileFromPath(fullPath) ? 'java' : getCodeWhispererLanguageIdFromPath(fullPath) + + if ( + !language || + !SUPPORTED_WORKSPACE_CONTEXT_LANGUAGES.includes(language) || + // skip processing the java project file if there's no java source file + (!hasJavaFile && isJavaProjectFile) + ) { continue } @@ -631,7 +654,7 @@ export class ArtifactManager { files: FileMetadata[] ): Promise { const workspacePath = URI.parse(workspaceFolder.uri).path - const hasJavaFiles = files.some(file => file.language === 'java' && file.relativePath.endsWith('java')) + const hasJavaFiles = files.some(file => file.language === 'java' && file.relativePath.endsWith('.java')) if (!hasJavaFiles) { return files diff --git a/server/aws-lsp-codewhisperer/src/shared/languageDetection.test.ts b/server/aws-lsp-codewhisperer/src/shared/languageDetection.test.ts index 67873bee0c..c8a1193677 100644 --- a/server/aws-lsp-codewhisperer/src/shared/languageDetection.test.ts +++ b/server/aws-lsp-codewhisperer/src/shared/languageDetection.test.ts @@ -45,10 +45,6 @@ describe('LanguageDetection', () => { describe('getCodeWhispererLanguageIdFromPath', () => { it('should return language type with override', () => { - assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/pom.xml'), 'java') - assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/build.gradle.kts'), 'java') - assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/build.xml'), 'java') - assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/build.gradle'), 'java') assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/test.java'), 'java') assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/package.json'), 'javascript') assert.strictEqual(getCodeWhispererLanguageIdFromPath('test/test.js'), 'javascript') diff --git a/server/aws-lsp-codewhisperer/src/shared/languageDetection.ts b/server/aws-lsp-codewhisperer/src/shared/languageDetection.ts index d8a4074d92..ab3761528b 100644 --- a/server/aws-lsp-codewhisperer/src/shared/languageDetection.ts +++ b/server/aws-lsp-codewhisperer/src/shared/languageDetection.ts @@ -294,8 +294,6 @@ export function getCodeWhispererLanguageIdFromPath(filePath: string): Codewhispe return 'javascript' } - if (isJavaProjectFileFromPath(filePath)) return 'java' - for (const [extension, languageId] of Object.entries(languageByExtension)) { if (filePath.endsWith(extension)) { return getRuntimeLanguage(languageId)