-
Notifications
You must be signed in to change notification settings - Fork 793
Expand file tree
/
Copy pathjavaCodeLensProvider.ts
More file actions
201 lines (177 loc) · 8.31 KB
/
javaCodeLensProvider.ts
File metadata and controls
201 lines (177 loc) · 8.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'
import { LambdaHandlerCandidate } from '../lambdaHandlerSearch'
import { findParentProjectFile } from '../utilities/workspaceUtils'
export const javaLanguage = 'java'
export const javaAllfiles: vscode.DocumentFilter[] = [
{
scheme: 'file',
language: javaLanguage,
},
]
export const gradleBasePattern = '**/*.gradle{,.kts}'
export const mavenBasePattern = '**/pom.xml'
const regexpReservedWordPublic = /\bpublic \b/
const regexpReservedWordAbstract = /\b abstract \b/
const regexpParameters = /\(.*\)/
export interface JavaLambdaHandlerComponents {
package: string
class: string
method: string
// Range of the function representing the Lambda Handler
handlerRange: vscode.Range
}
export async function getLambdaHandlerCandidates(document: vscode.TextDocument): Promise<LambdaHandlerCandidate[]> {
const rootUri =
(await findParentProjectFile(document.uri, /^.*pom.xml$/)) ??
(await findParentProjectFile(document.uri, /^.*\.gradle(\.kts)?$/))
if (!rootUri) {
return []
}
const symbols: vscode.DocumentSymbol[] =
(await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri)) || []
return getLambdaHandlerComponents(document, symbols).map<LambdaHandlerCandidate>((lambdaHandlerComponents) => {
const handlerName = generateJavaLambdaHandler(lambdaHandlerComponents)
return {
filename: document.uri.fsPath,
handlerName,
rootUri: rootUri,
range: lambdaHandlerComponents.handlerRange,
}
})
}
export function getLambdaHandlerComponents(
document: vscode.TextDocument,
symbols: vscode.DocumentSymbol[]
): JavaLambdaHandlerComponents[] {
const packageSymbols = symbols.filter((symbol) => symbol.kind === vscode.SymbolKind.Package)
if (packageSymbols.length !== 1) {
return []
}
const packageName = packageSymbols[0].name
return (
symbols
.filter((symbol) => symbol.kind === vscode.SymbolKind.Class)
.filter((classSymbol) => isValidClassSymbol(document, classSymbol))
// Find relevant methods within each class
.reduce<JavaLambdaHandlerComponents[]>((accumulator, lambdaHandlerComponent) => {
accumulator.push(
...lambdaHandlerComponent.children
.filter((classChildSymbol) => classChildSymbol.kind === vscode.SymbolKind.Method)
.filter((methodSymbol) => isValidLambdaHandler(document, methodSymbol))
.map((methodSymbol) => {
return {
package: packageName,
class: document.getText(lambdaHandlerComponent.selectionRange),
method: document.getText(methodSymbol.selectionRange),
handlerRange: methodSymbol.range,
}
})
)
return accumulator
}, [])
)
}
export function isValidClassSymbol(
document: Pick<vscode.TextDocument, 'getText'>,
symbol: vscode.DocumentSymbol
): boolean {
if (symbol.kind === vscode.SymbolKind.Class) {
// from "public abstract class Processor" pull "public abstract class "
const classDeclarationBeforeNameRange = new vscode.Range(symbol.range.start, symbol.selectionRange.start)
const classDeclarationBeforeName: string = document.getText(classDeclarationBeforeNameRange)
return (
regexpReservedWordPublic.test(classDeclarationBeforeName) &&
!regexpReservedWordAbstract.test(classDeclarationBeforeName)
)
}
return false
}
/**
* Returns whether or not a method is a valid Lambda handler
* @param document VS Code document
* @param symbol VS Code DocumentSymbol to evaluate
*/
export function isValidLambdaHandler(
document: Pick<vscode.TextDocument, 'getText'>,
symbol: vscode.DocumentSymbol
): boolean {
if (symbol.kind === vscode.SymbolKind.Method) {
// from "public async Task<Response> foo()" pull "public async Task<Response> "
const signatureBeforeMethodNameRange = new vscode.Range(symbol.range.start, symbol.selectionRange.start)
const signatureBeforeMethodName: string = document.getText(signatureBeforeMethodNameRange)
if (regexpReservedWordPublic.test(signatureBeforeMethodName)) {
return isValidMethodSignature(symbol)
}
}
return false
}
/**
* Returns whether or not a VS Code DocumentSymbol is a method that could be a Lambda handler
* * has one parameter
* * has two parameters where the first params are an InputStream and OutputStream OR the last param is a Context
* * TODO?: Notably, we are not checking specifically for a `com.amazonaws.services.lambda.runtime.Context`, or `java.io` streams
* * has three parameters where both conditions from two parameters are met
* @param symbol VS Code DocumentSymbol to analyze
*/
export function isValidMethodSignature(symbol: vscode.DocumentSymbol): boolean {
if (symbol.kind === vscode.SymbolKind.Method) {
// The `redhat.java` extension appears to strip a fair amount from this signature:
// from source function `public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context)`
// redhat extension returns: symbol.name = `'handleRequest(APIGatewayProxyRequestEvent, Context)'`
const parametersArr = regexpParameters.exec(symbol.name)
// reject if there are no parameters
if (!parametersArr) {
return false
}
// remove generics from parameter string so we can do a predictable split on comma
const strippedStr = stripGenericsFromParams(parametersArr[0])
const individualParams = strippedStr.split(',')
switch (individualParams.length) {
case 1:
return individualParams[0] === '()' ? false : true
case 2:
return lastParamIsContext(individualParams) || firstTwoParamsAreStreams(individualParams)
case 3:
return lastParamIsContext(individualParams) && firstTwoParamsAreStreams(individualParams)
default:
return false
}
}
return false
}
function lastParamIsContext(paramArr: string[]): boolean {
// TODO: handle different kinds of imported context objects in case user is importing a non-Lambda context
const lambdaContextType = /[\.\b]{0,1}Context\b/
return lambdaContextType.test(paramArr[paramArr.length - 1].valueOf().trimStart())
}
function firstTwoParamsAreStreams(paramArr: string[]): boolean {
const inputStreamType = /[\.\b]{0,1}InputStream\b/
const outputStreamType = /[\.\b]{0,1}OutputStream\b/
return inputStreamType.test(paramArr[0].valueOf().trim()) && outputStreamType.test(paramArr[1].valueOf().trim())
}
/**
* Strips any generics from a string in order to ensure predictable commas for a string of parameters.
* e.g.: `'(Foo<Bar, Baz> x, Context y)' -> '(Foo x, Context y)'`
* Implements a fairly rough English-centric approximation of the Java identifier spec:
* * isJavaIdentifierStart(firstCharacter) is true: https://docs.oracle.com/javase/7/docs/api/java/lang/Character.html#isJavaIdentifierStart(char)
* * all other characters are true for isJavaIdentifierPart: https://docs.oracle.com/javase/7/docs/api/java/lang/Character.html#isJavaIdentifierPart(char)
* * For now, cover this from an English-centric point of view (a-zA-Z) and add unicode ranges if necessary
*
* @param input String to remove generics from
*/
function stripGenericsFromParams(input: string): string {
const javaGenericIdentifierRegex = /<\s*(?:[a-zA-Z_$]{1}[a-zA-Z0-9_$]*?[\s,]*?)+>/g
return input.replace(javaGenericIdentifierRegex, '')
}
/**
*
* @param components Components to generate handler from
* @returns String representation of the Lambda Java handler. Always provides method (even if it corrrectly implements a Java Lambda interface)
*/
export function generateJavaLambdaHandler(components: JavaLambdaHandlerComponents): string {
return `${components.package}.${components.class}::${components.method}`
}