Skip to content

Commit 18fe3db

Browse files
authored
fix(appbuilder): improve open handler logic
## Problem open handler version may search and open the wrong handler. ## Solution Improve the logic to identify handler in ruby/python Improve the logic search in limited location in java/node/dotnet
1 parent f030e35 commit 18fe3db

File tree

3 files changed

+226
-30
lines changed

3 files changed

+226
-30
lines changed

packages/core/src/awsService/appBuilder/utils.ts

Lines changed: 126 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { samDeployUrl } from '../../shared/constants'
1717
import path from 'path'
1818
import fs from '../../shared/fs/fs'
1919
import { getLogger } from '../../shared/logger/logger'
20+
import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime'
21+
import { showMessage } from '../../shared/utilities/messages'
2022
const localize = nls.loadMessageBundle()
2123

2224
export async function runOpenTemplate(arg?: TreeNode) {
@@ -28,36 +30,134 @@ export async function runOpenTemplate(arg?: TreeNode) {
2830
await vscode.window.showTextDocument(document)
2931
}
3032

31-
export async function runOpenHandler(arg: ResourceNode) {
33+
/**
34+
* Find and open the lambda handler with given ResoruceNode
35+
* If not found, a NoHandlerFound error will be raised
36+
* @param arg ResourceNode
37+
*/
38+
export async function runOpenHandler(arg: ResourceNode): Promise<void> {
3239
const folderUri = path.dirname(arg.resource.location.fsPath)
33-
let handler: string | undefined
34-
let extension = '*'
35-
if (arg.resource.resource.Runtime?.includes('java')) {
36-
handler = arg.resource.resource.Handler?.split('::')[0]
37-
if (handler?.includes('.')) {
38-
handler = handler.split('.')[1]
39-
}
40-
extension = 'java'
41-
} else if (arg.resource.resource.Runtime?.includes('dotnet')) {
42-
handler = arg.resource.resource.Handler?.split('::')[1]
43-
if (handler?.includes('.')) {
44-
handler = handler.split('.')[1]
45-
}
46-
extension = 'cs'
47-
} else {
48-
handler = arg.resource.resource.Handler?.split('.')[0]
40+
if (!arg.resource.resource.CodeUri) {
41+
throw new ToolkitError('No CodeUri provided in template, cannot open handler', { code: 'NoCodeUriProvided' })
4942
}
50-
const handlerFile = (
51-
await vscode.workspace.findFiles(
52-
new vscode.RelativePattern(folderUri, `**/${handler}.${extension}`),
53-
new vscode.RelativePattern(folderUri, '.aws-sam')
54-
)
55-
)[0]
43+
44+
if (!arg.resource.resource.Handler) {
45+
throw new ToolkitError('No Handler provided in template, cannot open handler', { code: 'NoHandlerProvided' })
46+
}
47+
48+
if (!arg.resource.resource.Runtime) {
49+
throw new ToolkitError('No Runtime provided in template, cannot open handler', { code: 'NoRuntimeProvided' })
50+
}
51+
52+
const handlerFile = await getLambdaHandlerFile(
53+
vscode.Uri.file(folderUri),
54+
arg.resource.resource.CodeUri,
55+
arg.resource.resource.Handler,
56+
arg.resource.resource.Runtime
57+
)
5658
if (!handlerFile) {
57-
throw new ToolkitError(`No handler file found with name "${handler}"`, { code: 'NoHandlerFound' })
59+
throw new ToolkitError(`No handler file found with name "${arg.resource.resource.Handler}"`, {
60+
code: 'NoHandlerFound',
61+
})
5862
}
59-
const document = await vscode.workspace.openTextDocument(handlerFile)
60-
await vscode.window.showTextDocument(document)
63+
await vscode.workspace.openTextDocument(handlerFile).then(async (doc) => await vscode.window.showTextDocument(doc))
64+
}
65+
66+
// create a set to store all supported runtime in the following function
67+
const supportedRuntimeForHandler = new Set<RuntimeFamily>([
68+
RuntimeFamily.Ruby,
69+
RuntimeFamily.Python,
70+
RuntimeFamily.NodeJS,
71+
RuntimeFamily.DotNet,
72+
RuntimeFamily.Java,
73+
])
74+
75+
/**
76+
* Get the actual Lambda handler file, in vscode.Uri format, from the template
77+
* file and handler name. If not found, return undefined.
78+
*
79+
* @param folderUri The root folder for sam project
80+
* @param codeUri codeUri prop in sam template
81+
* @param handler handler prop in sam template
82+
* @param runtime runtime prop in sam template
83+
* @returns
84+
*/
85+
export async function getLambdaHandlerFile(
86+
folderUri: vscode.Uri,
87+
codeUri: string,
88+
handler: string,
89+
runtime: string
90+
): Promise<vscode.Uri | undefined> {
91+
const family = getFamily(runtime)
92+
if (!supportedRuntimeForHandler.has(family)) {
93+
throw new ToolkitError(`Runtime ${runtime} is not supported for open handler button`, {
94+
code: 'RuntimeNotSupported',
95+
})
96+
}
97+
98+
const handlerParts = handler.split('.')
99+
// sample: app.lambda_handler -> app.rb
100+
if (family === RuntimeFamily.Ruby) {
101+
// Ruby supports namespace/class handlers as well, but the path is
102+
// guaranteed to be slash-delimited so we can assume the first part is
103+
// the path
104+
return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.rb')
105+
}
106+
107+
// sample:app.lambda_handler -> app.py
108+
if (family === RuntimeFamily.Python) {
109+
// Otherwise (currently Node.js and Python) handle dot-delimited paths
110+
return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.py')
111+
}
112+
113+
// sample: app.handler -> app.mjs/app.js
114+
// More likely to be mjs if NODEJS version>=18, now searching for both
115+
if (family === RuntimeFamily.NodeJS) {
116+
const handlerName = handlerParts.slice(0, handlerParts.length - 1).join('/')
117+
const handlerPath = path.dirname(handlerName)
118+
const handlerFile = path.basename(handlerName)
119+
const pattern = new vscode.RelativePattern(
120+
vscode.Uri.joinPath(folderUri, codeUri, handlerPath),
121+
`${handlerFile}.{js,mjs}`
122+
)
123+
return searchHandlerFile(folderUri, pattern)
124+
}
125+
// search directly under Code uri for Dotnet and java
126+
// sample: ImageResize::ImageResize.Function::FunctionHandler -> Function.cs
127+
if (family === RuntimeFamily.DotNet) {
128+
const handlerName = path.basename(handler.split('::')[1].replaceAll('.', '/'))
129+
const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `${handlerName}.cs`)
130+
return searchHandlerFile(folderUri, pattern)
131+
}
132+
133+
// sample: resizer.App::handleRequest -> App.java
134+
if (family === RuntimeFamily.Java) {
135+
const handlerName = handler.split('::')[0].replaceAll('.', '/')
136+
const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `**/${handlerName}.java`)
137+
return searchHandlerFile(folderUri, pattern)
138+
}
139+
}
140+
141+
/**
142+
Searches for a handler file in the given pattern and returns the first match.
143+
If no match is found, returns undefined.
144+
*/
145+
export async function searchHandlerFile(
146+
folderUri: vscode.Uri,
147+
pattern: vscode.RelativePattern
148+
): Promise<vscode.Uri | undefined> {
149+
const handlerFile = await vscode.workspace.findFiles(pattern, new vscode.RelativePattern(folderUri, '.aws-sam'))
150+
if (handlerFile.length === 0) {
151+
return undefined
152+
}
153+
if (handlerFile.length > 1) {
154+
getLogger().warn(`Multiple handler files found with name "${path.basename(handlerFile[0].fsPath)}"`)
155+
void showMessage('warn', `Multiple handler files found with name "${path.basename(handlerFile[0].fsPath)}"`)
156+
}
157+
if (await fs.exists(handlerFile[0])) {
158+
return handlerFile[0]
159+
}
160+
return undefined
61161
}
62162

63163
async function promptUserForTemplate() {

packages/core/src/lambda/models/samLambdaRuntime.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum RuntimeFamily {
2222
DotNet,
2323
Go,
2424
Java,
25+
Ruby,
2526
}
2627

2728
export type RuntimePackageType = 'Image' | 'Zip'
@@ -65,6 +66,7 @@ export const javaRuntimes: ImmutableSet<Runtime> = ImmutableSet<Runtime>([
6566
'java21',
6667
])
6768
export const dotNetRuntimes: ImmutableSet<Runtime> = ImmutableSet<Runtime>(['dotnet6', 'dotnet8'])
69+
export const rubyRuntimes: ImmutableSet<Runtime> = ImmutableSet<Runtime>(['ruby3.2', 'ruby3.3'])
6870

6971
/**
7072
* Deprecated runtimes can be found at https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
@@ -83,13 +85,16 @@ export const deprecatedRuntimes: ImmutableSet<Runtime> = ImmutableSet<Runtime>([
8385
'nodejs8.10',
8486
'nodejs10.x',
8587
'nodejs12.x',
88+
'ruby2.5',
89+
'ruby2.7',
8690
])
8791
const defaultRuntimes = ImmutableMap<RuntimeFamily, Runtime>([
8892
[RuntimeFamily.NodeJS, 'nodejs20.x'],
8993
[RuntimeFamily.Python, 'python3.12'],
9094
[RuntimeFamily.DotNet, 'dotnet6'],
9195
[RuntimeFamily.Go, 'go1.x'],
9296
[RuntimeFamily.Java, 'java17'],
97+
[RuntimeFamily.Ruby, 'ruby3.3'],
9398
])
9499

95100
export const samZipLambdaRuntimes: ImmutableSet<Runtime> = ImmutableSet.union([
@@ -163,6 +168,8 @@ export function getFamily(runtime: string): RuntimeFamily {
163168
return RuntimeFamily.Go
164169
} else if (javaRuntimes.has(runtime)) {
165170
return RuntimeFamily.Java
171+
} else if (rubyRuntimes.has(runtime)) {
172+
return RuntimeFamily.Ruby
166173
}
167174
return RuntimeFamily.Unknown
168175
}
@@ -212,6 +219,10 @@ export function getRuntimeFamily(langId: string): RuntimeFamily {
212219
return RuntimeFamily.Python
213220
case 'go':
214221
return RuntimeFamily.Go
222+
case 'java':
223+
return RuntimeFamily.Java
224+
case 'ruby':
225+
return RuntimeFamily.Ruby
215226
default:
216227
return RuntimeFamily.Unknown
217228
}

packages/core/src/test/awsService/appBuilder/utils.test.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,81 @@ const scenarios: TestScenario[] = [
3333
handler: 'resizer.App::handleRequest',
3434
codeUri: 'ResizerFunction',
3535
fileLocation: 'ResizerFunction/src/main/java/resizer/App.java',
36-
fileInfo: 'test',
36+
fileInfo: 'testjava',
3737
regex: /App.java/g,
3838
},
3939
{
4040
runtime: 'dotnet6',
4141
handler: 'ImageResize::ImageResize.Function::FunctionHandler',
4242
codeUri: 'ImageResize/',
4343
fileLocation: 'ImageResize/Function.cs',
44-
fileInfo: 'test',
44+
fileInfo: 'testdotnet',
4545
regex: /Function.cs/g,
4646
},
4747
{
4848
runtime: 'python3.9',
4949
handler: 'app.lambda_handler',
5050
codeUri: 'hello_world/',
5151
fileLocation: 'hello_world/app.py',
52-
fileInfo: 'test',
52+
fileInfo: 'testpython',
5353
regex: /app.py/g,
5454
},
5555
{
5656
runtime: 'nodejs18.x',
5757
handler: 'app.handler',
5858
codeUri: 'src/',
5959
fileLocation: 'src/app.js',
60-
fileInfo: 'test',
60+
fileInfo: 'testnode',
6161
regex: /app.js/g,
6262
},
63+
{
64+
runtime: 'ruby3.2',
65+
handler: 'app.lambda_handler',
66+
codeUri: 'hello_world/',
67+
fileLocation: 'hello_world/app.rb',
68+
fileInfo: 'testruby',
69+
regex: /app.rb/g,
70+
},
71+
{
72+
runtime: 'java21',
73+
handler: 'resizer.App::handleRequest',
74+
codeUri: 'ResizerFunction',
75+
fileLocation: 'ResizerFunction/src/foo/bar/main/java/resizer/App.java',
76+
fileInfo: 'testjava2',
77+
regex: /App.java/g,
78+
},
79+
{
80+
runtime: 'dotnet8',
81+
handler: 'ImageResize::ImageResize.Function::FunctionHandler',
82+
codeUri: 'ImageResize/src/test',
83+
fileLocation: 'ImageResize/src/test/Function.cs',
84+
fileInfo: 'testdotnet2',
85+
regex: /Function.cs/g,
86+
},
87+
{
88+
runtime: 'python3.12',
89+
handler: 'app.foo.bar.lambda_handler',
90+
codeUri: 'hello_world/test123',
91+
fileLocation: 'hello_world/test123/app/foo/bar.py',
92+
fileInfo: 'testpython2',
93+
regex: /bar.py/g,
94+
},
95+
{
96+
runtime: 'nodejs20.x',
97+
handler: 'app.foo.bar.handler',
98+
codeUri: 'src/test123',
99+
fileLocation: 'src/test123/app/foo/bar.js',
100+
fileInfo: 'testnode2',
101+
regex: /bar.js/g,
102+
},
103+
{
104+
runtime: 'ruby3.3',
105+
handler: 'app/foo/bar.lambda_handler',
106+
codeUri: 'hello_world/test456',
107+
fileLocation: 'hello_world/test456/app/foo/bar.rb',
108+
fileInfo: 'testruby2',
109+
regex: /bar.rb/g,
110+
},
63111
]
64112

65113
describe('AppBuilder Utils', function () {
@@ -165,6 +213,43 @@ describe('AppBuilder Utils', function () {
165213
await assertTextEditorContains(scenario.fileInfo)
166214
})
167215
}
216+
217+
it(`should warn for multiple java handler found`, async function () {
218+
const rNode = new ResourceNode(
219+
{
220+
samTemplateUri: vscode.Uri.file(path.join(tempFolder, 'template.yaml')),
221+
workspaceFolder: workspace,
222+
projectRoot: vscode.Uri.file(tempFolder),
223+
},
224+
{
225+
Id: 'MyFunction',
226+
Type: SERVERLESS_FUNCTION_TYPE,
227+
Runtime: 'java21',
228+
Handler: 'resizer.App::handleRequest',
229+
CodeUri: 'ResizerFunction',
230+
}
231+
)
232+
// When 2 java handler with right name under code URI
233+
await fs.mkdir(
234+
path.join(tempFolder, ...path.dirname('ResizerFunction/src/main/java/resizer/App.java').split('/'))
235+
)
236+
await fs.writeFile(
237+
path.join(tempFolder, ...'ResizerFunction/src/main/java/resizer/App.java'.split('/')),
238+
'testjava'
239+
)
240+
await fs.mkdir(
241+
path.join(tempFolder, ...path.dirname('ResizerFunction/src/main/java/resizer2/App.java').split('/'))
242+
)
243+
await fs.writeFile(
244+
path.join(tempFolder, ...'ResizerFunction/src/main/java/resizer2/App.java'.split('/')),
245+
'testjava'
246+
)
247+
// Then should warn
248+
getTestWindow().onDidShowMessage((msg) =>
249+
assert(msg.assertWarn('Multiple handler files found with name App.java"'))
250+
)
251+
await runOpenHandler(rNode)
252+
})
168253
}),
169254
describe('open template', function () {
170255
let sandbox: sinon.SinonSandbox

0 commit comments

Comments
 (0)