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+ }
0 commit comments