-
Notifications
You must be signed in to change notification settings - Fork 469
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Utility class to detect Typescript functions that can possibly be use…
…d as Lambda Function Handlers. (#142) * Utility class to detect Typescript functions that can possibly be used as Lambda Function Handlers.
- Loading branch information
1 parent
f695ade
commit ce746c6
Showing
10 changed files
with
2,329 additions
and
1,704 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/*! | ||
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
export interface LambdaHandlerCandidate { | ||
handlerName: string, | ||
filename: string, | ||
positionStart: number, | ||
positionEnd: number, | ||
} | ||
|
||
export interface LambdaHandlerSearch { | ||
|
||
/** | ||
* @description Looks for functions that appear to be valid Lambda Function Handlers. | ||
* @returns A collection of information for each detected candidate. | ||
*/ | ||
findCandidateLambdaHandlers(): Promise<LambdaHandlerCandidate[]> | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,324 @@ | ||
/*! | ||
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
import * as path from 'path' | ||
import * as ts from 'typescript' | ||
import * as vscode from 'vscode' | ||
import * as filesystem from './filesystem' | ||
import { LambdaHandlerCandidate, LambdaHandlerSearch } from './lambdaHandlerSearch' | ||
|
||
/** | ||
* Detects functions that could possibly be used as Lambda Function Handlers from a Typescript file. | ||
*/ | ||
export class TypescriptLambdaHandlerSearch implements LambdaHandlerSearch { | ||
|
||
public static readonly MAXIMUM_FUNCTION_PARAMETERS: number = 3 | ||
|
||
private readonly _uri!: vscode.Uri | ||
private readonly _filename: string | ||
private readonly _baseFilename: string | ||
// _candidateDeclaredFunctionNames - names of functions that could be lambda handlers | ||
private readonly _candidateDeclaredFunctionNames: Set<string> = new Set() | ||
// _candidateModuleExportsExpressions - all statements like "exports.handler = ..." | ||
private _candidateModuleExportsExpressions: ts.ExpressionStatement[] = [] | ||
// _candidateExportDeclarations - all "export { xyz }" | ||
private _candidateExportDeclarations: ts.ExportDeclaration[] = [] | ||
// _candidateExportNodes - all "export function Xyz()" / "export const Xyz = () => {}" | ||
private _candidateExportNodes: ts.Node[] = [] | ||
|
||
public constructor(uri: vscode.Uri) { | ||
this._uri = uri | ||
this._filename = this._uri.fsPath | ||
this._baseFilename = path.parse(this._filename).name | ||
} | ||
|
||
/** | ||
* @description Looks for functions that appear to be valid Lambda Function Handlers. | ||
* @returns A collection of information for each detected candidate. | ||
*/ | ||
public async findCandidateLambdaHandlers(): Promise<LambdaHandlerCandidate[]> { | ||
this._candidateDeclaredFunctionNames.clear() | ||
this._candidateModuleExportsExpressions = [] | ||
this._candidateExportDeclarations = [] | ||
this._candidateExportNodes = [] | ||
|
||
return await this.getCandidateHandlers() | ||
} | ||
|
||
private async getCandidateHandlers(): Promise<LambdaHandlerCandidate[]> { | ||
const fileContents = await filesystem.readFileAsyncAsString(this._filename) | ||
|
||
const sourceFile = ts.createSourceFile(this._filename, fileContents, ts.ScriptTarget.Latest, true) | ||
|
||
const handlers: LambdaHandlerCandidate[] = this.processSourceFile(sourceFile) | ||
|
||
return handlers | ||
} | ||
|
||
/** | ||
* @description looks for Lambda Handler candidates in the given source file | ||
* Lambda Handler candidates are top level exported methods/functions. | ||
* | ||
* @param sourceFile SourceFile child node to process | ||
* @returns Collection of candidate Lambda handler information, empty array otherwise | ||
*/ | ||
private processSourceFile(sourceFile: ts.SourceFile): LambdaHandlerCandidate[] { | ||
this.scanSourceFile(sourceFile) | ||
|
||
const handlers: LambdaHandlerCandidate[] = [] | ||
|
||
handlers.push(...this.findCandidateHandlersInModuleExports()) | ||
handlers.push(...this.findCandidateHandlersInExportedFunctions()) | ||
handlers.push(...this.findCandidateHandlersInExportDeclarations()) | ||
|
||
return handlers | ||
} | ||
|
||
/** | ||
* @description looks through a file's nodes, looking for data to support finding handler candidates | ||
*/ | ||
private scanSourceFile(sourceFile: ts.SourceFile): void { | ||
sourceFile.forEachChild((node: ts.Node) => { | ||
|
||
// Function declarations | ||
if (ts.isFunctionLike(node)) { | ||
if (TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate(node)) { | ||
this._candidateDeclaredFunctionNames.add(node.name!.getText()) | ||
} | ||
} | ||
|
||
// Arrow Function declarations "const foo = (arg) => { }" | ||
if (ts.isVariableStatement(node)) { | ||
node.declarationList.forEachChild(declaration => { | ||
if (ts.isVariableDeclaration(declaration)) { | ||
const declarationName: string = declaration.name.getText() | ||
|
||
if (declarationName.length > 0 | ||
&& declaration.initializer | ||
&& ts.isFunctionLike(declaration.initializer) | ||
&& TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate( | ||
declaration.initializer, | ||
false // initializers do not have a name value, it is up in declaration.name | ||
) | ||
) { | ||
this._candidateDeclaredFunctionNames.add(declarationName) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
// export function xxx / "export const xxx = () => {}" | ||
// We grab all of these and filter them later on in order to better deal with the VariableStatement entries | ||
if (TypescriptLambdaHandlerSearch.isNodeExported(node)) { | ||
this._candidateExportNodes.push(node) | ||
} | ||
|
||
// Things like "exports.handler = ..." | ||
// Grab all, cull after we've found all valid functions that can be referenced on rhs | ||
if (ts.isExpressionStatement(node)) { | ||
if (TypescriptLambdaHandlerSearch.isModuleExportsAssignment(node)) { | ||
this._candidateModuleExportsExpressions.push(node) | ||
} | ||
} | ||
|
||
// Things like "export { xxx }" | ||
// Grab all, cull after we've found all valid functions that can be referenced in brackets | ||
if (ts.isExportDeclaration(node)) { | ||
this._candidateExportDeclarations.push(node) | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* @description Looks at module.exports assignments to find candidate Lamdba handlers | ||
*/ | ||
private findCandidateHandlersInModuleExports(): LambdaHandlerCandidate[] { | ||
return this._candidateModuleExportsExpressions | ||
.filter(expression => { | ||
return TypescriptLambdaHandlerSearch.isEligibleLambdaHandlerAssignment( | ||
expression, | ||
this._candidateDeclaredFunctionNames | ||
) | ||
}).map(candidate => { | ||
// 'module.exports.xyz' => ['module', 'exports', 'xyz'] | ||
const lhsComponents: string[] = (candidate.expression as ts.BinaryExpression) | ||
.left.getText().split('.').map(x => x.trim()) | ||
|
||
const exportsTarget: string = lhsComponents[0] === 'exports' ? lhsComponents[1] : lhsComponents[2] | ||
|
||
return { | ||
filename: this._filename, | ||
handlerName: `${this._baseFilename}.${exportsTarget}`, | ||
positionStart: candidate.pos, | ||
positionEnd: candidate.end, | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* @description Looks at "export { xyz }" statements to find candidate Lambda handlers | ||
*/ | ||
private findCandidateHandlersInExportDeclarations(): LambdaHandlerCandidate[] { | ||
const handlers: LambdaHandlerCandidate[] = [] | ||
|
||
this._candidateExportDeclarations.forEach(exportDeclaration => { | ||
if (exportDeclaration.exportClause) { | ||
exportDeclaration.exportClause.forEachChild(clause => { | ||
if (ts.isExportSpecifier(clause)) { | ||
const exportedFunction: string = clause.name.getText() | ||
|
||
if (this._candidateDeclaredFunctionNames.has(exportedFunction)) { | ||
handlers.push({ | ||
filename: this._filename, | ||
handlerName: `${this._baseFilename}.${exportedFunction}`, | ||
positionStart: clause.pos, | ||
positionEnd: clause.end, | ||
}) | ||
} | ||
} | ||
}) | ||
} | ||
}) | ||
|
||
return handlers | ||
} | ||
|
||
/** | ||
* @description Looks at export function declarations to find candidate Lamdba handlers | ||
*/ | ||
private findCandidateHandlersInExportedFunctions(): LambdaHandlerCandidate[] { | ||
const handlers: LambdaHandlerCandidate[] = [] | ||
|
||
this._candidateExportNodes.forEach(exportNode => { | ||
if (ts.isFunctionLike(exportNode) | ||
&& (TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate(exportNode)) | ||
&& !!exportNode.name | ||
) { | ||
handlers.push({ | ||
filename: this._filename, | ||
handlerName: `${this._baseFilename}.${exportNode.name.getText()}`, | ||
positionStart: exportNode.pos, | ||
positionEnd: exportNode.end, | ||
}) | ||
} else if (ts.isVariableStatement(exportNode)) { | ||
exportNode.declarationList.forEachChild(declaration => { | ||
if (ts.isVariableDeclaration(declaration) | ||
&& !!declaration.initializer | ||
&& ts.isFunctionLike(declaration.initializer) | ||
&& TypescriptLambdaHandlerSearch.isFunctionLambdaHandlerCandidate( | ||
declaration.initializer, | ||
false | ||
) | ||
) { | ||
handlers.push({ | ||
filename: this._filename, | ||
handlerName: `${this._baseFilename}.${declaration.name.getText()}`, | ||
positionStart: declaration.pos, | ||
positionEnd: declaration.end, | ||
}) | ||
} | ||
}) | ||
} | ||
}) | ||
|
||
return handlers | ||
} | ||
|
||
/** | ||
* @description Whether or not the given expression is attempting to assign to '[module.]exports.foo' | ||
* @param expressionStatement Expression node to evaluate | ||
*/ | ||
private static isModuleExportsAssignment(expressionStatement: ts.ExpressionStatement): boolean { | ||
if (ts.isBinaryExpression(expressionStatement.expression)) { | ||
const lhsComponents: string[] = expressionStatement.expression.left.getText().split('.').map(x => x.trim()) | ||
|
||
return (lhsComponents.length === 3 && lhsComponents[0] === 'module' && lhsComponents[1] === 'exports') | ||
|| (lhsComponents.length === 2 && lhsComponents[0] === 'exports') | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* @description Whether or not the given expression appears to be assigning a candidate Lambda Handler | ||
* Expression could be one of: | ||
* [module.]exports.foo = alreadyDeclaredFunction | ||
* [module.]exports.foo = (event, context) => { ... } | ||
* @param expressionStatement Expression node to evaluate | ||
* @param functionHandlerNames Names of declared functions considered to be Handler Candidates | ||
*/ | ||
private static isEligibleLambdaHandlerAssignment( | ||
expressionStatement: ts.ExpressionStatement, | ||
functionHandlerNames: Set<string> | ||
): boolean { | ||
if (ts.isBinaryExpression(expressionStatement.expression)) { | ||
return this.isTargetFunctionReference(expressionStatement.expression.right, functionHandlerNames) | ||
|| this.isValidFunctionAssignment(expressionStatement.expression.right) | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* @description Whether or not the given expression appears to contain a function of interest on the right hand side | ||
* | ||
* Example expression: | ||
* something = alreadyDeclaredFunction | ||
* @param expression Expression node to evaluate | ||
* @param targetFunctionNames Names of functions of interest | ||
*/ | ||
private static isTargetFunctionReference(expression: ts.Expression, targetFunctionNames: Set<string>): boolean { | ||
if (ts.isIdentifier(expression)) { | ||
return (targetFunctionNames.has(expression.text)) | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* @description Whether or not the given expression appears to have a function that could be a valid Lambda handler | ||
* on the right hand side. | ||
* | ||
* Example expression: | ||
* something = (event, context) => { } | ||
* @param expression Expression node to evaluate | ||
* @param targetFunctionNames Names of functions of interest | ||
*/ | ||
private static isValidFunctionAssignment(expression: ts.Expression): boolean { | ||
if (ts.isFunctionLike(expression)) { | ||
return expression.parameters.length <= TypescriptLambdaHandlerSearch.MAXIMUM_FUNCTION_PARAMETERS | ||
} | ||
|
||
return false | ||
} | ||
|
||
/** | ||
* @description Indicates whether or not a node is marked as visible outside this file | ||
* @param node Node to check | ||
* @returns true if node is exported, false otherwise | ||
*/ | ||
private static isNodeExported(node: ts.Node): boolean { | ||
const flags: ts.ModifierFlags = ts.getCombinedModifierFlags(node as ts.Declaration) | ||
|
||
// tslint:disable-next-line:no-bitwise | ||
return ((flags & ts.ModifierFlags.Export) === ts.ModifierFlags.Export) | ||
} | ||
|
||
/** | ||
* @description Indicates whether or not a function/method could be a Lambda Handler | ||
* @param node Signature Declaration Node to check | ||
* @param validateName whether or not to check the name property | ||
*/ | ||
private static isFunctionLambdaHandlerCandidate( | ||
node: ts.SignatureDeclaration, | ||
validateName: boolean = true | ||
): boolean { | ||
const nameIsValid: boolean = (!validateName || !!node.name) | ||
|
||
return node.parameters.length <= TypescriptLambdaHandlerSearch.MAXIMUM_FUNCTION_PARAMETERS && nameIsValid | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/*! | ||
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
'use strict' | ||
|
||
class NonExportedClass { | ||
publicMethod() { } | ||
} | ||
|
||
class ExportedClass { | ||
publicMethod() { } | ||
|
||
static publicStaticMethod() { } | ||
} | ||
exports.ExportedClass = ExportedClass | ||
|
||
function functionWithNoArgs() { } | ||
|
||
function exportedFunctionWithNoArgs() { } | ||
exports.exportedFunctionWithNoArgs = exportedFunctionWithNoArgs |
Oops, something went wrong.