Skip to content

Commit 1f152fc

Browse files
authored
Merge pull request #239 from digma-ai/feature/flask_client_side_discovery
Feature/flask client side discovery
2 parents bc89bb1 + 0bd5a6f commit 1f152fc

File tree

9 files changed

+128
-13
lines changed

9 files changed

+128
-13
lines changed

src/analyticsCodeLens.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,11 @@ class CodelensProvider implements vscode.CodeLensProvider<vscode.CodeLens>
239239
const lenses = await this.getLensForCodeLocationObject(methodInfo,
240240
uniqueEndpoints,documentInfo.usageData.getAll(),documentInfo.insights.all.filter(x=>x.scope=="EntrySpan"|| x.scope=="Span"),
241241
);
242+
243+
const uniqueLenses =[...new Map(lenses.map(item =>
244+
[item.command!.title, item])).values()];
242245

243-
for (const lens of lenses){
246+
for (const lens of uniqueLenses){
244247
codelens.push(lens);
245248
}
246249

src/services/codeInspector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class CodeInspector {
6767
return;
6868
}
6969

70-
private async getType(
70+
public async getType(
7171
usageDocument: vscode.TextDocument,
7272
usagePosition: vscode.Position,
7373
): Promise<Definition | undefined> {

src/services/documentInfoProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class DocumentInfoProvider implements vscode.Disposable
183183
const symbolTrees = await this.symbolProvider.getSymbolTree(doc);
184184
const tokens = await this.symbolProvider.getTokens(doc);
185185
const symbolInfos = await this.symbolProvider.getMethods(doc, tokens, symbolTrees);
186-
const endpoints = await this.symbolProvider.getEndpoints(doc, symbolInfos, tokens, symbolTrees, this);
186+
const endpoints = await this.symbolProvider.getEndpoints(doc, symbolInfos, tokens, symbolTrees );
187187
const spans = await this.symbolProvider.getSpans(doc, symbolInfos, tokens);
188188
const paramsExtractor = await this.symbolProvider.getParametersExtractor(doc);
189189
const symbolAliasExtractor = await this.symbolProvider.getSymbolAliasExtractor(doc);

src/services/languages/csharp/AspNetCoreMvcEndpointExtractor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22
import { DocumentInfoProvider } from './../../documentInfoProvider';
33
import { CodeInspector } from '../../codeInspector';
4-
import { SymbolTree } from './../symbolProvider';
4+
import { SymbolProvider, SymbolTree } from './../symbolProvider';
55
import { Token, TokenType } from '../tokens';
66
import { EndpointInfo, IEndpointExtractor, SymbolInfo } from '../extractors';
77
import { convertRange } from '../../utils';
@@ -16,7 +16,7 @@ export class AspNetCoreMvcEndpointExtractor implements IEndpointExtractor {
1616
symbolInfo: SymbolInfo[],
1717
tokens: Token[],
1818
symbolTrees: SymbolTree[] | undefined,
19-
documentInfoProvider: DocumentInfoProvider,
19+
symbolProvider: SymbolProvider
2020
): Promise<EndpointInfo[]> {
2121
const results: EndpointInfo[] = [];
2222

@@ -28,7 +28,7 @@ export class AspNetCoreMvcEndpointExtractor implements IEndpointExtractor {
2828
const derivesFromControllerBase = await this._codeInspector.derivesFrom(
2929
classDefinition,
3030
'ControllerBase',
31-
documentInfoProvider.symbolProvider,
31+
symbolProvider,
3232
this.findParentToken,
3333
);
3434
if(!derivesFromControllerBase) {

src/services/languages/extractors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export interface IEndpointExtractor {
100100
symbolInfos: SymbolInfo[],
101101
tokens: Token[],
102102
symbolTrees: SymbolTree[] | undefined,
103-
documentInfoProvider: DocumentInfoProvider,
103+
symbolProvider: SymbolProvider
104104
): Promise<EndpointInfo[]>;
105105
}
106106

src/services/languages/languageExtractor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IMethodExtractor, IParametersExtractor, IEndpointExtractor, ISpanExtrac
55
import { BasicParametersExtractor } from './defaultImpls';
66
import { IModulePathToUriConverter } from './modulePathToUriConverters';
77
import { ICodeObjectIdParser, CommonCodeObjectIdParser } from '../codeObject';
8+
import { FlaskEndpointExtractor } from './python/flaskEndpointExtractor';
89

910
export interface ILanguageExtractor {
1011
requiredExtensionLoaded: boolean;
@@ -44,7 +45,7 @@ export abstract class LanguageExtractor implements ILanguageExtractor {
4445
}
4546

4647
public getEndpointExtractors(codeInspector: CodeInspector): IEndpointExtractor[] {
47-
return [];
48+
return [new FlaskEndpointExtractor(codeInspector)];
4849
}
4950

5051
public abstract getSpanExtractors(codeInspector: CodeInspector): ISpanExtractor[];

src/services/languages/python/fastapiEndpointExtractor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode';
22
import { DocumentInfoProvider } from './../../documentInfoProvider';
3-
import { SymbolTree } from './../symbolProvider';
3+
import { SymbolProvider, SymbolTree } from './../symbolProvider';
44
import { Token, TokenType } from '../tokens';
55
import { EndpointInfo, IEndpointExtractor, SymbolInfo } from '../extractors';
66
import { EndpointSchema } from '../../analyticsProvider';
@@ -12,7 +12,7 @@ export class FastapiEndpointExtractor implements IEndpointExtractor
1212
symbolInfo: SymbolInfo[],
1313
tokens: Token[],
1414
symbolTrees: SymbolTree[] | undefined,
15-
documentInfoProvider: DocumentInfoProvider,
15+
symbolProvider: SymbolProvider,
1616
): Promise<EndpointInfo[]> {
1717
// Ensure fastapi module was imported
1818
if(!tokens.any(t => t.text == 'fastapi' && t.type == TokenType.module))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as vscode from 'vscode';
2+
import { DocumentInfoProvider } from './../../documentInfoProvider';
3+
import { SymbolProvider, SymbolTree } from './../symbolProvider';
4+
import { Token, TokenType } from '../tokens';
5+
import { EndpointInfo, IEndpointExtractor, SymbolInfo } from '../extractors';
6+
import { EndpointSchema } from '../../analyticsProvider';
7+
import { CodeInspector } from '../../codeInspector';
8+
9+
export class FlaskEndpointExtractor implements IEndpointExtractor
10+
{
11+
readonly routeMethods = ['route', 'get','post', 'patch'];
12+
readonly genericRouteMethod = 'route';
13+
readonly flaskAppType = 'Flask';
14+
15+
16+
constructor(private _codeInspector: CodeInspector) {}
17+
18+
async extractEndpoints(
19+
document: vscode.TextDocument,
20+
symbolInfo: SymbolInfo[],
21+
tokens: Token[],
22+
symbolTrees: SymbolTree[] | undefined,
23+
symbolProvider: SymbolProvider
24+
): Promise<EndpointInfo[]> {
25+
26+
// Ensure flask module was imported
27+
if(!tokens.any(t => t.text == 'flask' && t.type == TokenType.module))
28+
return [];
29+
30+
// Search for "@app.get" decorators
31+
const results: EndpointInfo[] = [];
32+
for(let i=0; i<tokens.length-1; i++)
33+
{
34+
const appToken = tokens[i];
35+
const methodToken = tokens[i+1];
36+
let route = '';
37+
let method = '';
38+
39+
//Check if this is a router variable
40+
if ( appToken.type === TokenType.variable
41+
&& methodToken.type === TokenType.method && this.routeMethods.includes( methodToken.text) ){
42+
43+
const routerLine = document.getText(new vscode.Range(
44+
appToken.range.start,
45+
new vscode.Position(methodToken.range.end.line, 1000)));
46+
47+
//extract the prefix
48+
let match = new RegExp(`^(?:${appToken.text}.(route|get|post|update|patch))\\s*\\((?:'|")(.*?)(?:'|")(?:.*[\\s,]*\\bmethods\\b\\s*=\\s*\\[(.*?)\\])?`).exec(routerLine);
49+
50+
if (!match || match.length<3){
51+
return [];
52+
}
53+
const tracerDefTypeDef = await this._codeInspector.getTypeFromSymbolProvider(document, appToken.range.start,symbolProvider,x=>true);
54+
55+
if (!tracerDefTypeDef || tracerDefTypeDef!=this.flaskAppType){
56+
return [];
57+
}
58+
59+
60+
route = match[2];
61+
method = match[1];
62+
63+
if (method==this.genericRouteMethod){
64+
65+
//Handle form of @app.route('/', methods: ["POST", "GET"])
66+
//In this scenario the regex would capture the methods
67+
//As the fourth group
68+
if (match.length==4 && match[3]){
69+
const methods = match[3].replaceAll('"','');
70+
method=methods;
71+
}
72+
//Otherwise the default for the "route" form is GET
73+
else{
74+
method='GET';
75+
76+
}
77+
}
78+
79+
80+
}
81+
else{
82+
continue;
83+
}
84+
85+
const relevantFunc = symbolInfo.firstOrDefault(s => s.range.contains(methodToken.range));
86+
if (!relevantFunc){
87+
continue;
88+
}
89+
90+
let folder = vscode.workspace.getWorkspaceFolder(document.uri);
91+
let folderPrefix = folder?.uri.path.replaceAll('\\',"/").split('/').slice(0,-1).join('/');
92+
let relevantPath = document.uri.path.substring(folderPrefix!.length);
93+
let pathParts = relevantPath.split('/').filter(x=>x);
94+
const methods = method.split(",").map(x=>x.toUpperCase().trim());
95+
96+
for (var apiMethod of methods){
97+
for (let j=0;j<pathParts.length-1;j++){
98+
let possibleRoot = pathParts[j];
99+
results.push(new EndpointInfo(
100+
possibleRoot + '$_$' + EndpointSchema.HTTP + "HTTP " + apiMethod + ' ' + route,
101+
apiMethod,
102+
route,
103+
relevantFunc.range,
104+
document.uri));
105+
}
106+
}
107+
108+
109+
}
110+
return results;
111+
}
112+
}

src/services/languages/symbolProvider.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ export class SymbolProvider
103103
document: vscode.TextDocument,
104104
symbolInfos: SymbolInfo[],
105105
tokens: Token[],
106-
symbolTrees: SymbolTree[] | undefined,
107-
documentInfoProvider: DocumentInfoProvider,
106+
symbolTrees: SymbolTree[] | undefined
108107
): Promise<EndpointInfo[]> {
109108
const supportedLanguage = await this.getSupportedLanguageExtractor(document);
110109
if(!supportedLanguage) {
@@ -113,7 +112,7 @@ export class SymbolProvider
113112

114113
const endpointExtractors = supportedLanguage.getEndpointExtractors(this._codeInspector);
115114
const extractedEndpoints = await Promise.all(
116-
endpointExtractors.map(async (x) => await x.extractEndpoints(document, symbolInfos, tokens, symbolTrees, documentInfoProvider))
115+
endpointExtractors.map(async (x) => await x.extractEndpoints(document, symbolInfos, tokens, symbolTrees, this))
117116
);
118117
const endpoints = extractedEndpoints.flat();
119118
return endpoints;

0 commit comments

Comments
 (0)