python.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /**
  2. * Python Framework Resolver
  3. *
  4. * Handles Django, Flask, and FastAPI patterns.
  5. */
  6. import { Node } from '../../types';
  7. import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
  8. export const djangoResolver: FrameworkResolver = {
  9. name: 'django',
  10. detect(context: ResolutionContext): boolean {
  11. // Check for Django in requirements.txt or setup.py
  12. const requirements = context.readFile('requirements.txt');
  13. if (requirements && requirements.includes('django')) {
  14. return true;
  15. }
  16. const setup = context.readFile('setup.py');
  17. if (setup && setup.includes('django')) {
  18. return true;
  19. }
  20. const pyproject = context.readFile('pyproject.toml');
  21. if (pyproject && pyproject.includes('django')) {
  22. return true;
  23. }
  24. // Check for manage.py (Django signature)
  25. return context.fileExists('manage.py');
  26. },
  27. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  28. // Pattern 1: Model references
  29. if (ref.referenceName.endsWith('Model') || /^[A-Z][a-z]+$/.test(ref.referenceName)) {
  30. const result = resolveModel(ref.referenceName, context);
  31. if (result) {
  32. return {
  33. original: ref,
  34. targetNodeId: result,
  35. confidence: 0.8,
  36. resolvedBy: 'framework',
  37. };
  38. }
  39. }
  40. // Pattern 2: View references
  41. if (ref.referenceName.endsWith('View') || ref.referenceName.endsWith('ViewSet')) {
  42. const result = resolveView(ref.referenceName, context);
  43. if (result) {
  44. return {
  45. original: ref,
  46. targetNodeId: result,
  47. confidence: 0.8,
  48. resolvedBy: 'framework',
  49. };
  50. }
  51. }
  52. // Pattern 3: Form references
  53. if (ref.referenceName.endsWith('Form')) {
  54. const result = resolveForm(ref.referenceName, context);
  55. if (result) {
  56. return {
  57. original: ref,
  58. targetNodeId: result,
  59. confidence: 0.8,
  60. resolvedBy: 'framework',
  61. };
  62. }
  63. }
  64. return null;
  65. },
  66. extractNodes(filePath: string, content: string): Node[] {
  67. const nodes: Node[] = [];
  68. const now = Date.now();
  69. // Extract URL patterns
  70. // path('route/', view, name='name')
  71. const urlPatterns = [
  72. /path\s*\(\s*['"]([^'"]+)['"],\s*(\w+)/g,
  73. /url\s*\(\s*r?['"]([^'"]+)['"],\s*(\w+)/g,
  74. ];
  75. for (const pattern of urlPatterns) {
  76. let match;
  77. while ((match = pattern.exec(content)) !== null) {
  78. const [, urlPath] = match;
  79. const line = content.slice(0, match.index).split('\n').length;
  80. nodes.push({
  81. id: `route:${filePath}:${urlPath}:${line}`,
  82. kind: 'route',
  83. name: urlPath!,
  84. qualifiedName: `${filePath}::route:${urlPath}`,
  85. filePath,
  86. startLine: line,
  87. endLine: line,
  88. startColumn: 0,
  89. endColumn: match[0].length,
  90. language: 'python',
  91. updatedAt: now,
  92. });
  93. }
  94. }
  95. return nodes;
  96. },
  97. };
  98. export const flaskResolver: FrameworkResolver = {
  99. name: 'flask',
  100. detect(context: ResolutionContext): boolean {
  101. const requirements = context.readFile('requirements.txt');
  102. if (requirements && (requirements.includes('flask') || requirements.includes('Flask'))) {
  103. return true;
  104. }
  105. const pyproject = context.readFile('pyproject.toml');
  106. if (pyproject && pyproject.includes('flask')) {
  107. return true;
  108. }
  109. // Check for Flask app pattern in common files
  110. const appFiles = ['app.py', 'application.py', 'main.py', '__init__.py'];
  111. for (const file of appFiles) {
  112. const content = context.readFile(file);
  113. if (content && content.includes('Flask(__name__)')) {
  114. return true;
  115. }
  116. }
  117. return false;
  118. },
  119. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  120. // Pattern 1: Blueprint references
  121. if (ref.referenceName.endsWith('_bp') || ref.referenceName.endsWith('_blueprint')) {
  122. const result = resolveBlueprint(ref.referenceName, context);
  123. if (result) {
  124. return {
  125. original: ref,
  126. targetNodeId: result,
  127. confidence: 0.8,
  128. resolvedBy: 'framework',
  129. };
  130. }
  131. }
  132. return null;
  133. },
  134. extractNodes(filePath: string, content: string): Node[] {
  135. const nodes: Node[] = [];
  136. const now = Date.now();
  137. // Extract Flask route decorators
  138. // @app.route('/path') or @blueprint.route('/path')
  139. const routePattern = /@(\w+)\.route\s*\(\s*['"]([^'"]+)['"]/g;
  140. let match;
  141. while ((match = routePattern.exec(content)) !== null) {
  142. const [, _appOrBp, routePath] = match;
  143. const line = content.slice(0, match.index).split('\n').length;
  144. nodes.push({
  145. id: `route:${filePath}:${routePath}:${line}`,
  146. kind: 'route',
  147. name: `${routePath}`,
  148. qualifiedName: `${filePath}::route:${routePath}`,
  149. filePath,
  150. startLine: line,
  151. endLine: line,
  152. startColumn: 0,
  153. endColumn: match[0].length,
  154. language: 'python',
  155. updatedAt: now,
  156. });
  157. }
  158. return nodes;
  159. },
  160. };
  161. export const fastapiResolver: FrameworkResolver = {
  162. name: 'fastapi',
  163. detect(context: ResolutionContext): boolean {
  164. const requirements = context.readFile('requirements.txt');
  165. if (requirements && requirements.includes('fastapi')) {
  166. return true;
  167. }
  168. const pyproject = context.readFile('pyproject.toml');
  169. if (pyproject && pyproject.includes('fastapi')) {
  170. return true;
  171. }
  172. // Check for FastAPI app pattern
  173. const appFiles = ['app.py', 'main.py', 'api.py'];
  174. for (const file of appFiles) {
  175. const content = context.readFile(file);
  176. if (content && content.includes('FastAPI()')) {
  177. return true;
  178. }
  179. }
  180. return false;
  181. },
  182. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  183. // Pattern 1: Router references
  184. if (ref.referenceName.endsWith('_router') || ref.referenceName === 'router') {
  185. const result = resolveRouter(ref.referenceName, context);
  186. if (result) {
  187. return {
  188. original: ref,
  189. targetNodeId: result,
  190. confidence: 0.8,
  191. resolvedBy: 'framework',
  192. };
  193. }
  194. }
  195. // Pattern 2: Dependency references
  196. if (ref.referenceName.startsWith('get_') || ref.referenceName.startsWith('Depends')) {
  197. const result = resolveDependency(ref.referenceName, context);
  198. if (result) {
  199. return {
  200. original: ref,
  201. targetNodeId: result,
  202. confidence: 0.75,
  203. resolvedBy: 'framework',
  204. };
  205. }
  206. }
  207. return null;
  208. },
  209. extractNodes(filePath: string, content: string): Node[] {
  210. const nodes: Node[] = [];
  211. const now = Date.now();
  212. // Extract FastAPI route decorators
  213. // @app.get('/path') or @router.post('/path')
  214. const routePattern = /@(\w+)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"]([^'"]+)['"]/g;
  215. let match;
  216. while ((match = routePattern.exec(content)) !== null) {
  217. const [, _appOrRouter, method, routePath] = match;
  218. const line = content.slice(0, match.index).split('\n').length;
  219. nodes.push({
  220. id: `route:${filePath}:${method!.toUpperCase()}:${routePath}:${line}`,
  221. kind: 'route',
  222. name: `${method!.toUpperCase()} ${routePath}`,
  223. qualifiedName: `${filePath}::${method!.toUpperCase()}:${routePath}`,
  224. filePath,
  225. startLine: line,
  226. endLine: line,
  227. startColumn: 0,
  228. endColumn: match[0].length,
  229. language: 'python',
  230. updatedAt: now,
  231. });
  232. }
  233. return nodes;
  234. },
  235. };
  236. // Helper functions
  237. function resolveModel(name: string, context: ResolutionContext): string | null {
  238. const modelDirs = ['models', 'app/models', 'src/models'];
  239. for (const dir of modelDirs) {
  240. const allFiles = context.getAllFiles();
  241. for (const file of allFiles) {
  242. if (file.startsWith(dir) && file.endsWith('.py')) {
  243. const nodes = context.getNodesInFile(file);
  244. const modelNode = nodes.find(
  245. (n) => n.kind === 'class' && n.name === name
  246. );
  247. if (modelNode) {
  248. return modelNode.id;
  249. }
  250. }
  251. }
  252. }
  253. return null;
  254. }
  255. function resolveView(name: string, context: ResolutionContext): string | null {
  256. const viewDirs = ['views', 'app/views', 'src/views', 'api/views'];
  257. for (const dir of viewDirs) {
  258. const allFiles = context.getAllFiles();
  259. for (const file of allFiles) {
  260. if (file.startsWith(dir) && file.endsWith('.py')) {
  261. const nodes = context.getNodesInFile(file);
  262. const viewNode = nodes.find(
  263. (n) => (n.kind === 'class' || n.kind === 'function') && n.name === name
  264. );
  265. if (viewNode) {
  266. return viewNode.id;
  267. }
  268. }
  269. }
  270. }
  271. return null;
  272. }
  273. function resolveForm(name: string, context: ResolutionContext): string | null {
  274. const formDirs = ['forms', 'app/forms', 'src/forms'];
  275. for (const dir of formDirs) {
  276. const allFiles = context.getAllFiles();
  277. for (const file of allFiles) {
  278. if (file.startsWith(dir) && file.endsWith('.py')) {
  279. const nodes = context.getNodesInFile(file);
  280. const formNode = nodes.find(
  281. (n) => n.kind === 'class' && n.name === name
  282. );
  283. if (formNode) {
  284. return formNode.id;
  285. }
  286. }
  287. }
  288. }
  289. return null;
  290. }
  291. function resolveBlueprint(name: string, context: ResolutionContext): string | null {
  292. const allFiles = context.getAllFiles();
  293. for (const file of allFiles) {
  294. if (file.endsWith('.py')) {
  295. const nodes = context.getNodesInFile(file);
  296. const bpNode = nodes.find(
  297. (n) => n.kind === 'variable' && n.name === name
  298. );
  299. if (bpNode) {
  300. return bpNode.id;
  301. }
  302. }
  303. }
  304. return null;
  305. }
  306. function resolveRouter(name: string, context: ResolutionContext): string | null {
  307. const routerDirs = ['routers', 'api', 'routes', 'endpoints'];
  308. for (const dir of routerDirs) {
  309. const allFiles = context.getAllFiles();
  310. for (const file of allFiles) {
  311. if ((file.startsWith(dir) || file.includes('/routers/')) && file.endsWith('.py')) {
  312. const nodes = context.getNodesInFile(file);
  313. const routerNode = nodes.find(
  314. (n) => n.kind === 'variable' && n.name === name
  315. );
  316. if (routerNode) {
  317. return routerNode.id;
  318. }
  319. }
  320. }
  321. }
  322. return null;
  323. }
  324. function resolveDependency(name: string, context: ResolutionContext): string | null {
  325. const depDirs = ['dependencies', 'deps', 'core'];
  326. for (const dir of depDirs) {
  327. const allFiles = context.getAllFiles();
  328. for (const file of allFiles) {
  329. if ((file.startsWith(dir) || file.includes('/dependencies/')) && file.endsWith('.py')) {
  330. const nodes = context.getNodesInFile(file);
  331. const depNode = nodes.find(
  332. (n) => n.kind === 'function' && n.name === name
  333. );
  334. if (depNode) {
  335. return depNode.id;
  336. }
  337. }
  338. }
  339. }
  340. return null;
  341. }