-
Notifications
You must be signed in to change notification settings - Fork 472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Utility class to detect Typescript functions that can possibly be used as Lambda Function Handlers. #142
Merged
Merged
Utility class to detect Typescript functions that can possibly be used as Lambda Function Handlers. #142
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
cea91c0
Utility class to detect Typescript functions that can possibly be use…
awschristou c816599
Feedback Changes
awschristou 0a52fcd
Feedback and build fixes
awschristou 77cfffe
Updated processing to find eligible TS (export) and JS (exports) hand…
awschristou 1e7811e
feedback changes
awschristou fee3445
Feedback changes
awschristou 70d439c
Added support for `export { foo }` style statements
awschristou 75eddd3
Added position information to detected candidate lambda handlers
awschristou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | ||
mpiroc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* @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 { | ||
mpiroc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to clarify, does |
||
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() { } | ||
mpiroc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
class ExportedClass { | ||
publicMethod() { } | ||
|
||
static publicStaticMethod() { } | ||
} | ||
exports.ExportedClass = ExportedClass | ||
|
||
function functionWithNoArgs() { } | ||
|
||
function exportedFunctionWithNoArgs() { } | ||
exports.exportedFunctionWithNoArgs = exportedFunctionWithNoArgs |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Promise<LambdaHandlerCandidate | undefined>[]
(or aPromise<Promise<LambdaHandlerCandidate | undefined>[]>
) instead? It would be awesome if we could update the view for each function independently.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I've seen of CodeLens, extensions are given the file as a whole, not for a viewport.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've put the performance investigation into #145