Skip to content

Commit

Permalink
Utility class to detect Typescript functions that can possibly be use…
Browse files Browse the repository at this point in the history
…d as Lambda Function Handlers. (#142)

* Utility class to detect Typescript functions that can possibly be used as Lambda Function Handlers.
  • Loading branch information
awschristou authored Oct 29, 2018
1 parent f695ade commit ce746c6
Show file tree
Hide file tree
Showing 10 changed files with 2,329 additions and 1,704 deletions.
3,405 changes: 1,702 additions & 1,703 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@
"tslint": "^5.11.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-no-circular-imports": "^0.6.1",
"typescript": "^3.0.1",
"vsce": "^1.51.1",
"vscode": "^1.1.21",
"vscode-nls-dev": "^3.2.2"
Expand All @@ -233,6 +232,7 @@
"npm": "^6.1.0",
"opn": "^5.4.0",
"request": "^2.88.0",
"typescript": "^3.1.3",
"vscode-nls": "^3.2.4",
"vue": "^2.5.16",
"xml2js": "^0.4.19"
Expand Down
23 changes: 23 additions & 0 deletions src/shared/lambdaHandlerSearch.ts
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[]>

}
324 changes: 324 additions & 0 deletions src/shared/typescriptLambdaHandlerSearch.ts
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
}
}
22 changes: 22 additions & 0 deletions src/test/samples/javascript/sampleClasses.js
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
Loading

0 comments on commit ce746c6

Please sign in to comment.