Skip to content

Commit ce746c6

Browse files
authored
Utility class to detect Typescript functions that can possibly be used as Lambda Function Handlers. (#142)
* Utility class to detect Typescript functions that can possibly be used as Lambda Function Handlers.
1 parent f695ade commit ce746c6

10 files changed

+2329
-1704
lines changed

package-lock.json

Lines changed: 1702 additions & 1703 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@
214214
"tslint": "^5.11.0",
215215
"tslint-eslint-rules": "^5.4.0",
216216
"tslint-no-circular-imports": "^0.6.1",
217-
"typescript": "^3.0.1",
218217
"vsce": "^1.51.1",
219218
"vscode": "^1.1.21",
220219
"vscode-nls-dev": "^3.2.2"
@@ -233,6 +232,7 @@
233232
"npm": "^6.1.0",
234233
"opn": "^5.4.0",
235234
"request": "^2.88.0",
235+
"typescript": "^3.1.3",
236236
"vscode-nls": "^3.2.4",
237237
"vue": "^2.5.16",
238238
"xml2js": "^0.4.19"

src/shared/lambdaHandlerSearch.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*!
2+
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
export interface LambdaHandlerCandidate {
9+
handlerName: string,
10+
filename: string,
11+
positionStart: number,
12+
positionEnd: number,
13+
}
14+
15+
export interface LambdaHandlerSearch {
16+
17+
/**
18+
* @description Looks for functions that appear to be valid Lambda Function Handlers.
19+
* @returns A collection of information for each detected candidate.
20+
*/
21+
findCandidateLambdaHandlers(): Promise<LambdaHandlerCandidate[]>
22+
23+
}
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
/*!
2+
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
import * as path from 'path'
9+
import * as ts from 'typescript'
10+
import * as vscode from 'vscode'
11+
import * as filesystem from './filesystem'
12+
import { LambdaHandlerCandidate, LambdaHandlerSearch } from './lambdaHandlerSearch'
13+
14+
/**
15+
* Detects functions that could possibly be used as Lambda Function Handlers from a Typescript file.
16+
*/
17+
export class TypescriptLambdaHandlerSearch implements LambdaHandlerSearch {
18+
19+
public static readonly MAXIMUM_FUNCTION_PARAMETERS: number = 3
20+
21+
private readonly _uri!: vscode.Uri
22+
private readonly _filename: string
23+
private readonly _baseFilename: string
24+
// _candidateDeclaredFunctionNames - names of functions that could be lambda handlers
25+
private readonly _candidateDeclaredFunctionNames: Set<string> = new Set()
26+
// _candidateModuleExportsExpressions - all statements like "exports.handler = ..."
27+
private _candidateModuleExportsExpressions: ts.ExpressionStatement[] = []
28+
// _candidateExportDeclarations - all "export { xyz }"
29+
private _candidateExportDeclarations: ts.ExportDeclaration[] = []
30+
// _candidateExportNodes - all "export function Xyz()" / "export const Xyz = () => {}"
31+
private _candidateExportNodes: ts.Node[] = []
32+
33+
public constructor(uri: vscode.Uri) {
34+
this._uri = uri
35+
this._filename = this._uri.fsPath
36+
this._baseFilename = path.parse(this._filename).name
37+
}
38+
39+
/**
40+
* @description Looks for functions that appear to be valid Lambda Function Handlers.
41+
* @returns A collection of information for each detected candidate.
42+
*/
43+
public async findCandidateLambdaHandlers(): Promise<LambdaHandlerCandidate[]> {
44+
this._candidateDeclaredFunctionNames.clear()
45+
this._candidateModuleExportsExpressions = []
46+
this._candidateExportDeclarations = []
47+
this._candidateExportNodes = []
48+
49+
return await this.getCandidateHandlers()
50+
}
51+
52+
private async getCandidateHandlers(): Promise<LambdaHandlerCandidate[]> {
53+
const fileContents = await filesystem.readFileAsyncAsString(this._filename)
54+
55+
const sourceFile = ts.createSourceFile(this._filename, fileContents, ts.ScriptTarget.Latest, true)
56+
57+
const handlers: LambdaHandlerCandidate[] = this.processSourceFile(sourceFile)
58+
59+
return handlers
60+
}
61+
62+
/**
63+
* @description looks for Lambda Handler candidates in the given source file
64+
* Lambda Handler candidates are top level exported methods/functions.
65+
*
66+
* @param sourceFile SourceFile child node to process
67+
* @returns Collection of candidate Lambda handler information, empty array otherwise
68+
*/
69+
private processSourceFile(sourceFile: ts.SourceFile): LambdaHandlerCandidate[] {
70+
this.scanSourceFile(sourceFile)
71+
72+
const handlers: LambdaHandlerCandidate[] = []
73+
74+
handlers.push(...this.findCandidateHandlersInModuleExports())
75+
handlers.push(...this.findCandidateHandlersInExportedFunctions())
76+
handlers.push(...this.findCandidateHandlersInExportDeclarations())
77+
78+
return handlers
79+
}
80+
81+
/**
82+
* @description looks through a file's nodes, looking for data to support finding handler candidates
83+
*/
84+
private scanSourceFile(sourceFile: ts.SourceFile): void {
85+
sourceFile.forEachChild((node: ts.Node) => {
86+
87+
// Function declarations
88+
if (ts.isFunctionLike(node)) {
89+
if (TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate(node)) {
90+
this._candidateDeclaredFunctionNames.add(node.name!.getText())
91+
}
92+
}
93+
94+
// Arrow Function declarations "const foo = (arg) => { }"
95+
if (ts.isVariableStatement(node)) {
96+
node.declarationList.forEachChild(declaration => {
97+
if (ts.isVariableDeclaration(declaration)) {
98+
const declarationName: string = declaration.name.getText()
99+
100+
if (declarationName.length > 0
101+
&& declaration.initializer
102+
&& ts.isFunctionLike(declaration.initializer)
103+
&& TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate(
104+
declaration.initializer,
105+
false // initializers do not have a name value, it is up in declaration.name
106+
)
107+
) {
108+
this._candidateDeclaredFunctionNames.add(declarationName)
109+
}
110+
}
111+
})
112+
}
113+
114+
// export function xxx / "export const xxx = () => {}"
115+
// We grab all of these and filter them later on in order to better deal with the VariableStatement entries
116+
if (TypescriptLambdaHandlerSearch.isNodeExported(node)) {
117+
this._candidateExportNodes.push(node)
118+
}
119+
120+
// Things like "exports.handler = ..."
121+
// Grab all, cull after we've found all valid functions that can be referenced on rhs
122+
if (ts.isExpressionStatement(node)) {
123+
if (TypescriptLambdaHandlerSearch.isModuleExportsAssignment(node)) {
124+
this._candidateModuleExportsExpressions.push(node)
125+
}
126+
}
127+
128+
// Things like "export { xxx }"
129+
// Grab all, cull after we've found all valid functions that can be referenced in brackets
130+
if (ts.isExportDeclaration(node)) {
131+
this._candidateExportDeclarations.push(node)
132+
}
133+
})
134+
}
135+
136+
/**
137+
* @description Looks at module.exports assignments to find candidate Lamdba handlers
138+
*/
139+
private findCandidateHandlersInModuleExports(): LambdaHandlerCandidate[] {
140+
return this._candidateModuleExportsExpressions
141+
.filter(expression => {
142+
return TypescriptLambdaHandlerSearch.isEligibleLambdaHandlerAssignment(
143+
expression,
144+
this._candidateDeclaredFunctionNames
145+
)
146+
}).map(candidate => {
147+
// 'module.exports.xyz' => ['module', 'exports', 'xyz']
148+
const lhsComponents: string[] = (candidate.expression as ts.BinaryExpression)
149+
.left.getText().split('.').map(x => x.trim())
150+
151+
const exportsTarget: string = lhsComponents[0] === 'exports' ? lhsComponents[1] : lhsComponents[2]
152+
153+
return {
154+
filename: this._filename,
155+
handlerName: `${this._baseFilename}.${exportsTarget}`,
156+
positionStart: candidate.pos,
157+
positionEnd: candidate.end,
158+
}
159+
})
160+
}
161+
162+
/**
163+
* @description Looks at "export { xyz }" statements to find candidate Lambda handlers
164+
*/
165+
private findCandidateHandlersInExportDeclarations(): LambdaHandlerCandidate[] {
166+
const handlers: LambdaHandlerCandidate[] = []
167+
168+
this._candidateExportDeclarations.forEach(exportDeclaration => {
169+
if (exportDeclaration.exportClause) {
170+
exportDeclaration.exportClause.forEachChild(clause => {
171+
if (ts.isExportSpecifier(clause)) {
172+
const exportedFunction: string = clause.name.getText()
173+
174+
if (this._candidateDeclaredFunctionNames.has(exportedFunction)) {
175+
handlers.push({
176+
filename: this._filename,
177+
handlerName: `${this._baseFilename}.${exportedFunction}`,
178+
positionStart: clause.pos,
179+
positionEnd: clause.end,
180+
})
181+
}
182+
}
183+
})
184+
}
185+
})
186+
187+
return handlers
188+
}
189+
190+
/**
191+
* @description Looks at export function declarations to find candidate Lamdba handlers
192+
*/
193+
private findCandidateHandlersInExportedFunctions(): LambdaHandlerCandidate[] {
194+
const handlers: LambdaHandlerCandidate[] = []
195+
196+
this._candidateExportNodes.forEach(exportNode => {
197+
if (ts.isFunctionLike(exportNode)
198+
&& (TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate(exportNode))
199+
&& !!exportNode.name
200+
) {
201+
handlers.push({
202+
filename: this._filename,
203+
handlerName: `${this._baseFilename}.${exportNode.name.getText()}`,
204+
positionStart: exportNode.pos,
205+
positionEnd: exportNode.end,
206+
})
207+
} else if (ts.isVariableStatement(exportNode)) {
208+
exportNode.declarationList.forEachChild(declaration => {
209+
if (ts.isVariableDeclaration(declaration)
210+
&& !!declaration.initializer
211+
&& ts.isFunctionLike(declaration.initializer)
212+
&& TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate(
213+
declaration.initializer,
214+
false
215+
)
216+
) {
217+
handlers.push({
218+
filename: this._filename,
219+
handlerName: `${this._baseFilename}.${declaration.name.getText()}`,
220+
positionStart: declaration.pos,
221+
positionEnd: declaration.end,
222+
})
223+
}
224+
})
225+
}
226+
})
227+
228+
return handlers
229+
}
230+
231+
/**
232+
* @description Whether or not the given expression is attempting to assign to '[module.]exports.foo'
233+
* @param expressionStatement Expression node to evaluate
234+
*/
235+
private static isModuleExportsAssignment(expressionStatement: ts.ExpressionStatement): boolean {
236+
if (ts.isBinaryExpression(expressionStatement.expression)) {
237+
const lhsComponents: string[] = expressionStatement.expression.left.getText().split('.').map(x => x.trim())
238+
239+
return (lhsComponents.length === 3 && lhsComponents[0] === 'module' && lhsComponents[1] === 'exports')
240+
|| (lhsComponents.length === 2 && lhsComponents[0] === 'exports')
241+
}
242+
243+
return false
244+
}
245+
246+
/**
247+
* @description Whether or not the given expression appears to be assigning a candidate Lambda Handler
248+
* Expression could be one of:
249+
* [module.]exports.foo = alreadyDeclaredFunction
250+
* [module.]exports.foo = (event, context) => { ... }
251+
* @param expressionStatement Expression node to evaluate
252+
* @param functionHandlerNames Names of declared functions considered to be Handler Candidates
253+
*/
254+
private static isEligibleLambdaHandlerAssignment(
255+
expressionStatement: ts.ExpressionStatement,
256+
functionHandlerNames: Set<string>
257+
): boolean {
258+
if (ts.isBinaryExpression(expressionStatement.expression)) {
259+
return this.isTargetFunctionReference(expressionStatement.expression.right, functionHandlerNames)
260+
|| this.isValidFunctionAssignment(expressionStatement.expression.right)
261+
}
262+
263+
return false
264+
}
265+
266+
/**
267+
* @description Whether or not the given expression appears to contain a function of interest on the right hand side
268+
*
269+
* Example expression:
270+
* something = alreadyDeclaredFunction
271+
* @param expression Expression node to evaluate
272+
* @param targetFunctionNames Names of functions of interest
273+
*/
274+
private static isTargetFunctionReference(expression: ts.Expression, targetFunctionNames: Set<string>): boolean {
275+
if (ts.isIdentifier(expression)) {
276+
return (targetFunctionNames.has(expression.text))
277+
}
278+
279+
return false
280+
}
281+
282+
/**
283+
* @description Whether or not the given expression appears to have a function that could be a valid Lambda handler
284+
* on the right hand side.
285+
*
286+
* Example expression:
287+
* something = (event, context) => { }
288+
* @param expression Expression node to evaluate
289+
* @param targetFunctionNames Names of functions of interest
290+
*/
291+
private static isValidFunctionAssignment(expression: ts.Expression): boolean {
292+
if (ts.isFunctionLike(expression)) {
293+
return expression.parameters.length <= TypescriptLambdaHandlerSearch.MAXIMUM_FUNCTION_PARAMETERS
294+
}
295+
296+
return false
297+
}
298+
299+
/**
300+
* @description Indicates whether or not a node is marked as visible outside this file
301+
* @param node Node to check
302+
* @returns true if node is exported, false otherwise
303+
*/
304+
private static isNodeExported(node: ts.Node): boolean {
305+
const flags: ts.ModifierFlags = ts.getCombinedModifierFlags(node as ts.Declaration)
306+
307+
// tslint:disable-next-line:no-bitwise
308+
return ((flags & ts.ModifierFlags.Export) === ts.ModifierFlags.Export)
309+
}
310+
311+
/**
312+
* @description Indicates whether or not a function/method could be a Lambda Handler
313+
* @param node Signature Declaration Node to check
314+
* @param validateName whether or not to check the name property
315+
*/
316+
private static isFunctionLambdaHandlerCandidate(
317+
node: ts.SignatureDeclaration,
318+
validateName: boolean = true
319+
): boolean {
320+
const nameIsValid: boolean = (!validateName || !!node.name)
321+
322+
return node.parameters.length <= TypescriptLambdaHandlerSearch.MAXIMUM_FUNCTION_PARAMETERS && nameIsValid
323+
}
324+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*!
2+
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
class NonExportedClass {
9+
publicMethod() { }
10+
}
11+
12+
class ExportedClass {
13+
publicMethod() { }
14+
15+
static publicStaticMethod() { }
16+
}
17+
exports.ExportedClass = ExportedClass
18+
19+
function functionWithNoArgs() { }
20+
21+
function exportedFunctionWithNoArgs() { }
22+
exports.exportedFunctionWithNoArgs = exportedFunctionWithNoArgs

0 commit comments

Comments
 (0)