Skip to content

Commit cce77a2

Browse files
dhasani23David Hasani
andauthored
refactor: do validation checks AFTER user picks project #4093
Problem: We run a shell command (`javap`) to determine the Java version of each of the projects open in the user's workspace. Running this command for each open project is totally unnecessary. Solution: Show options to transform any of the projects open in the workspace, and then once the user has selected a project, the validation checks are run (`javap` etc.) to check that it is a valid project. The solution is mostly a refactoring -- the logic to do the validation checks was pre-existing, it is simply moved to happen after the user selects a project. Co-authored-by: David Hasani <[email protected]>
1 parent c6abde3 commit cce77a2

File tree

5 files changed

+111
-107
lines changed

5 files changed

+111
-107
lines changed

src/codewhisperer/commands/startTransformByQ.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
pollTransformationJob,
2121
convertToTimeString,
2222
convertDateToTimestamp,
23-
getValidModules,
23+
getOpenProjects,
24+
validateProjectSelection,
2425
} from '../service/transformByQHandler'
2526
import { QuickPickItem } from 'vscode'
2627
import { MultiStepInputFlowController } from '../../shared//multiStepInputFlowController'
@@ -59,51 +60,58 @@ interface UserInputState {
5960
totalSteps: number
6061
targetLanguage: QuickPickItem
6162
targetVersion: QuickPickItem
62-
module: QuickPickItem
63+
project: QuickPickItem
6364
}
6465

65-
async function collectInputs(validModules: vscode.QuickPickItem[] | undefined) {
66+
async function collectInputs(validProjects: vscode.QuickPickItem[] | undefined) {
6667
// const targetLanguages: QuickPickItem[] = CodeWhispererConstants.targetLanguages.map(label => ({ label }))
6768
const state = {} as Partial<UserInputState>
6869
// only supporting target language of Java and target version of JDK17 for now, so skip to pickModule prompt
6970
transformByQState.setTargetJDKVersionToJDK17()
70-
await MultiStepInputFlowController.run(input => pickModule(input, state, validModules))
71+
await MultiStepInputFlowController.run(input => pickProject(input, state, validProjects))
7172
// await MultiStepInputFlowController.run(input => pickTargetLanguage(input, state, targetLanguages, validModules))
7273
return state as UserInputState
7374
}
7475

75-
async function pickModule(
76+
async function pickProject(
7677
input: MultiStepInputFlowController,
7778
state: Partial<UserInputState>,
78-
validModules: vscode.QuickPickItem[] | undefined
79+
validProjects: vscode.QuickPickItem[] | undefined
7980
) {
8081
const pick = await input.showQuickPick({
8182
title: CodeWhispererConstants.transformByQWindowTitle,
8283
step: DropdownStep.STEP_1,
8384
totalSteps: DropdownStep.STEP_1,
8485
placeholder: CodeWhispererConstants.selectModulePrompt,
85-
items: validModules!,
86+
items: validProjects!,
8687
shouldResume: () => Promise.resolve(true),
8788
ignoreFocusOut: false,
8889
})
89-
state.module = pick
90-
transformByQState.setModuleName(he.encode(state.module.label)) // encode to avoid HTML injection risk
90+
state.project = pick
91+
transformByQState.setProjectName(he.encode(state.project.label)) // encode to avoid HTML injection risk
9192
}
9293

9394
export async function startTransformByQ() {
9495
await telemetry.amazonq_codeTransformInvoke.run(async span => {
9596
span.record({ codeTransform_SessionId: codeTransformTelemetryState.getSessionId() })
96-
let validModules: vscode.QuickPickItem[] | undefined
97+
98+
let openProjects: vscode.QuickPickItem[] = []
9799
try {
98-
validModules = await getValidModules()
100+
openProjects = await getOpenProjects()
99101
} catch (err) {
100-
getLogger().error('Failed to get valid modules: ', err)
102+
getLogger().error('Failed to get open projects: ', err)
101103
throw err
102104
}
105+
const state = await collectInputs(openProjects)
103106

104-
span.record({ codeTransform_SourceJavaVersion: transformByQState.getSourceJDKVersion() })
107+
try {
108+
await validateProjectSelection(state.project)
109+
} catch (err) {
110+
getLogger().error('Selected project is not Java 8, not Java 11, or does not use Maven', err)
111+
throw err
112+
}
105113

106-
const state = await collectInputs(validModules)
114+
span.record({ codeTransform_SourceJavaVersion: transformByQState.getSourceJDKVersion() })
107115

108116
const selection = await vscode.window.showWarningMessage(
109117
CodeWhispererConstants.dependencyDisclaimer,
@@ -116,7 +124,7 @@ export async function startTransformByQ() {
116124
}
117125

118126
transformByQState.setToRunning()
119-
transformByQState.setModulePath(state.module.description!)
127+
transformByQState.setProjectPath(state.project.description!)
120128
sessionPlanProgress['uploadCode'] = StepProgress.Pending
121129
sessionPlanProgress['buildCode'] = StepProgress.Pending
122130
sessionPlanProgress['transformCode'] = StepProgress.Pending
@@ -146,7 +154,7 @@ export async function startTransformByQ() {
146154
throwIfCancelled()
147155
try {
148156
// TODO: we want to track zip failures separately from uploadPayload failures
149-
const payloadFileName = await zipCode(state.module.description!)
157+
const payloadFileName = await zipCode(state.project.description!)
150158
await vscode.commands.executeCommand('aws.amazonq.refresh') // so that button updates
151159
uploadId = await uploadPayload(payloadFileName)
152160
} catch (error) {
@@ -277,11 +285,11 @@ export async function startTransformByQ() {
277285
vscode.commands.executeCommand('setContext', 'gumby.isTransformAvailable', true)
278286
const durationInMs = new Date().getTime() - startTime.getTime()
279287

280-
if (state.module) {
288+
if (state.project) {
281289
sessionJobHistory = processHistory(
282290
sessionJobHistory,
283291
convertDateToTimestamp(startTime),
284-
transformByQState.getModuleName(),
292+
transformByQState.getProjectName(),
285293
transformByQState.getStatus(),
286294
convertToTimeString(durationInMs),
287295
transformByQState.getJobId()

src/codewhisperer/models/model.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ export class ZipManifest {
256256
export class TransformByQState {
257257
private transformByQState: TransformByQStatus = TransformByQStatus.NotStarted
258258

259-
private moduleName: string = ''
260-
private modulePath: string = ''
259+
private projectName: string = ''
260+
private projectPath: string = ''
261261

262262
private jobId: string = ''
263263

@@ -296,12 +296,12 @@ export class TransformByQState {
296296
return this.transformByQState === TransformByQStatus.PartiallySucceeded
297297
}
298298

299-
public getModuleName() {
300-
return this.moduleName
299+
public getProjectName() {
300+
return this.projectName
301301
}
302302

303-
public getModulePath() {
304-
return this.modulePath
303+
public getProjectPath() {
304+
return this.projectPath
305305
}
306306

307307
public getJobId() {
@@ -360,12 +360,12 @@ export class TransformByQState {
360360
this.transformByQState = TransformByQStatus.PartiallySucceeded
361361
}
362362

363-
public setModuleName(name: string) {
364-
this.moduleName = name
363+
public setProjectName(name: string) {
364+
this.projectName = name
365365
}
366366

367-
public setModulePath(path: string) {
368-
this.modulePath = path
367+
public setProjectPath(path: string) {
368+
this.projectPath = path
369369
}
370370

371371
public setJobId(id: string) {

src/codewhisperer/service/transformByQHandler.ts

Lines changed: 58 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -57,75 +57,70 @@ export function throwIfCancelled() {
5757
}
5858
}
5959

60-
/*
61-
* This function searches for a .class file in each opened module. Then it runs javap on the found .class file to get the JDK version
62-
* for the file, and sets the version in the state variable. Only JDK8 and JDK11 are supported.
63-
*/
64-
export async function getValidModules() {
60+
export async function getOpenProjects() {
6561
const folders = vscode.workspace.workspaceFolders
66-
const validModules: vscode.QuickPickItem[] = []
6762
if (folders === undefined) {
6863
vscode.window.showErrorMessage(CodeWhispererConstants.noSupportedJavaProjectsFoundMessage, { modal: true })
69-
throw Error('No Java projects found since no projects are open')
64+
throw new ToolkitError('No Java projects found since no projects are open', { code: 'NoOpenProjects' })
7065
}
71-
let containsSupportedJava = false // workspace must contain Java 8 or Java 11 code for this to be true
72-
let containsPomXml = false // workspace must contain a 'pom.xml' file for this to be true
73-
let failureReason = 'NoJavaProjectsAvailable'
66+
const openProjects: vscode.QuickPickItem[] = []
7467
for (const folder of folders) {
75-
const compiledJavaFiles = await vscode.workspace.findFiles(
76-
new vscode.RelativePattern(folder, '**/*.class'),
77-
'**/node_modules/**',
78-
1
79-
)
80-
if (compiledJavaFiles.length < 1) {
81-
continue
82-
}
83-
const classFilePath = compiledJavaFiles[0].fsPath
84-
const baseCommand = 'javap'
85-
const args = ['-v', classFilePath]
86-
const spawnResult = spawnSync(baseCommand, args, { shell: false, encoding: 'utf-8' })
87-
88-
if (spawnResult.error || spawnResult.status !== 0) {
89-
failureReason = 'CouldNotRunJavaCommand'
90-
continue // if cannot get Java version, move on to other projects in workspace
91-
}
92-
const majorVersionIndex = spawnResult.stdout.indexOf('major version: ')
93-
const javaVersion = spawnResult.stdout.slice(majorVersionIndex + 15, majorVersionIndex + 17).trim()
94-
if (javaVersion === CodeWhispererConstants.JDK8VersionNumber) {
95-
transformByQState.setSourceJDKVersionToJDK8()
96-
containsSupportedJava = true
97-
} else if (javaVersion === CodeWhispererConstants.JDK11VersionNumber) {
98-
transformByQState.setSourceJDKVersionToJDK11()
99-
containsSupportedJava = true
100-
} else {
101-
continue
102-
}
103-
const buildFile = await vscode.workspace.findFiles(
104-
new vscode.RelativePattern(folder, '**/pom.xml'), // only supporting projects with a pom.xml for now
105-
'**/node_modules/**',
106-
1
107-
)
108-
if (buildFile.length < 1) {
109-
checkIfGradle(folder)
110-
continue
111-
} else {
112-
containsPomXml = true
113-
}
114-
validModules.push({ label: folder.name, description: folder.uri.fsPath })
68+
openProjects.push({
69+
label: folder.name,
70+
description: folder.uri.fsPath,
71+
})
72+
}
73+
return openProjects
74+
}
75+
76+
/*
77+
* This function searches for a .class file in the selected project. Then it runs javap on the found .class file to get the JDK version
78+
* for the project, and sets the version in the state variable. Only JDK8 and JDK11 are supported. It also ensure a pom.xml file is found,
79+
* since only the Maven build system is supported for now.
80+
*/
81+
export async function validateProjectSelection(project: vscode.QuickPickItem) {
82+
const projectPath = project.description
83+
const compiledJavaFiles = await vscode.workspace.findFiles(
84+
new vscode.RelativePattern(projectPath!, '**/*.class'),
85+
'**/node_modules/**',
86+
1
87+
)
88+
if (compiledJavaFiles.length < 1) {
89+
vscode.window.showErrorMessage(CodeWhispererConstants.noSupportedJavaProjectsFoundMessage, { modal: true })
90+
throw new ToolkitError('No Java projects found', { code: 'NoJavaProjectsAvailable' })
11591
}
116-
if (!containsSupportedJava) {
92+
const classFilePath = compiledJavaFiles[0].fsPath
93+
const baseCommand = 'javap'
94+
const args = ['-v', classFilePath]
95+
const spawnResult = spawnSync(baseCommand, args, { shell: false, encoding: 'utf-8' })
96+
97+
if (spawnResult.error || spawnResult.status !== 0) {
11798
vscode.window.showErrorMessage(CodeWhispererConstants.noSupportedJavaProjectsFoundMessage, { modal: true })
118-
throw new ToolkitError('No Java projects found', { code: failureReason })
99+
throw new ToolkitError('Unable to determine Java version', { code: 'CannotDetermineJavaVersion' })
119100
}
120-
if (!containsPomXml) {
121-
vscode.window.showErrorMessage(CodeWhispererConstants.noPomXmlFoundMessage, { modal: true })
122-
throw new ToolkitError('No build file found', { code: 'CouldNotFindPomXml' })
101+
const majorVersionIndex = spawnResult.stdout.indexOf('major version: ')
102+
const javaVersion = spawnResult.stdout.slice(majorVersionIndex + 15, majorVersionIndex + 17).trim()
103+
if (javaVersion === CodeWhispererConstants.JDK8VersionNumber) {
104+
transformByQState.setSourceJDKVersionToJDK8()
105+
} else if (javaVersion === CodeWhispererConstants.JDK11VersionNumber) {
106+
transformByQState.setSourceJDKVersionToJDK11()
123107
} else {
124-
telemetry.amazonq_codeTransformInvoke.record({
125-
codeTransform_ProjectType: 'maven',
126-
})
108+
vscode.window.showErrorMessage(CodeWhispererConstants.noSupportedJavaProjectsFoundMessage, { modal: true })
109+
throw new ToolkitError('Project selected is not Java 8 or Java 11', { code: 'UnsupportedJavaVersion' })
127110
}
128-
return validModules
111+
const buildFile = await vscode.workspace.findFiles(
112+
new vscode.RelativePattern(projectPath!, '**/pom.xml'),
113+
'**/node_modules/**',
114+
1
115+
)
116+
if (buildFile.length < 1) {
117+
await checkIfGradle(projectPath!)
118+
vscode.window.showErrorMessage(CodeWhispererConstants.noPomXmlFoundMessage, { modal: true })
119+
throw new ToolkitError('No valid Maven build file found', { code: 'CouldNotFindPomXml' })
120+
}
121+
telemetry.amazonq_codeTransformInvoke.record({
122+
codeTransform_ProjectType: 'maven',
123+
})
129124
}
130125

131126
export function getSha256(fileName: string) {
@@ -384,14 +379,14 @@ export async function pollTransformationJob(jobId: string, validStates: string[]
384379
return status
385380
}
386381

387-
async function checkIfGradle(folder: vscode.WorkspaceFolder) {
388-
const gradleBuildFiles = await vscode.workspace.findFiles(
389-
new vscode.RelativePattern(folder, '**/build.gradle'),
382+
async function checkIfGradle(projectPath: string) {
383+
const gradleBuildFile = await vscode.workspace.findFiles(
384+
new vscode.RelativePattern(projectPath, '**/build.gradle'),
390385
'**/node_modules/**',
391386
1
392387
)
393388

394-
if (gradleBuildFiles.length > 1) {
389+
if (gradleBuildFile.length > 0) {
395390
telemetry.amazonq_codeTransformInvoke.record({
396391
codeTransform_ProjectType: 'gradle',
397392
})

src/codewhisperer/service/transformationResultsViewProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export class ProposedTransformationExplorer {
303303
diffModel.parseDiff(
304304
path.join(pathContainingArchive, ExportResultArchiveStructure.PathToDiffPatch),
305305
path.join(pathContainingArchive, ExportResultArchiveStructure.PathToSourceDir),
306-
transformByQState.getModulePath()
306+
transformByQState.getProjectPath()
307307
)
308308

309309
vscode.commands.executeCommand('setContext', 'gumby.reviewState', TransformByQReviewStatus.InReview)

src/test/codewhisperer/commands/transformByQ.test.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import {
1818
convertDateToTimestamp,
1919
convertToTimeString,
2020
throwIfCancelled,
21-
getValidModules,
2221
stopJob,
2322
pollTransformationJob,
23+
validateProjectSelection,
2424
} from '../../../codewhisperer/service/transformByQHandler'
2525

2626
describe('transformByQ', function () {
@@ -68,11 +68,7 @@ describe('transformByQ', function () {
6868
assert.strictEqual(model.transformByQState.getStatus(), 'Cancelled')
6969
})
7070

71-
it('WHEN get valid modules called on valid project THEN correctly extracts JDK8 version', async function () {
72-
sinon
73-
.stub(vscode.workspace, 'workspaceFolders')
74-
.get(() => [{ uri: vscode.Uri.file('/user/sample/project/'), name: 'SampleProject', index: 0 }])
75-
71+
it('WHEN validateProjectSelection called on valid project THEN correctly extracts JDK8 version', async function () {
7672
const findFilesStub = sinon.stub(vscode.workspace, 'findFiles')
7773
findFilesStub.onFirstCall().resolves([vscode.Uri.file('/user/sample/project/ClassFile.class')])
7874
findFilesStub.onSecondCall().resolves([vscode.Uri.file('/user/sample/project/pom.xml')])
@@ -88,26 +84,31 @@ describe('transformByQ', function () {
8884
}
8985
const spawnSyncStub = sinon.stub().returns(spawnSyncResult)
9086

91-
const { getValidModules } = proxyquire('../../../codewhisperer/service/transformByQHandler', {
87+
const { validateProjectSelection } = proxyquire('../../../codewhisperer/service/transformByQHandler', {
9288
child_process: { spawnSync: spawnSyncStub },
9389
})
9490

95-
const validModules = await getValidModules()
96-
assert.strictEqual(validModules![0].label, 'SampleProject')
91+
const dummyQuickPickItem: vscode.QuickPickItem = {
92+
label: 'SampleProject',
93+
description: '/dummy/path/here',
94+
}
95+
await assert.doesNotReject(async () => {
96+
await validateProjectSelection(dummyQuickPickItem)
97+
})
9798
assert.strictEqual(model.transformByQState.getSourceJDKVersion(), '8')
9899
})
99100

100-
it('WHEN get valid modules called on project with no class files THEN throws error', async function () {
101-
sinon
102-
.stub(vscode.workspace, 'workspaceFolders')
103-
.get(() => [{ uri: vscode.Uri.file('/user/sample/project/'), name: 'SampleProject', index: 0 }])
104-
101+
it('WHEN validateProjectSelection called on project with no class files THEN throws error', async function () {
105102
const findFilesStub = sinon.stub(vscode.workspace, 'findFiles')
106103
findFilesStub.onFirstCall().resolves([])
104+
const dummyQuickPickItem: vscode.QuickPickItem = {
105+
label: 'SampleProject',
106+
description: '/dummy/path/here',
107+
}
107108

108109
await assert.rejects(
109110
async () => {
110-
await getValidModules()
111+
await validateProjectSelection(dummyQuickPickItem)
111112
},
112113
{
113114
name: 'Error',

0 commit comments

Comments
 (0)