Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 104 additions & 8 deletions src/mappers/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const fastApiRouteDecoratorStartPattern = new RegExp(
"u",
);
const fastApiRouteDecoratorPattern = new RegExp(
`^\\s*@${fastApiRouteTargetPattern}\\.(${fastApiRouteMethods})\\((.*)\\)\\s*(?:#.*)?$`,
`^\\s*@(${fastApiRouteTargetPattern})\\.(${fastApiRouteMethods})\\((.*)\\)\\s*(?:#.*)?$`,
"u",
);
const projectMetadataFiles = [
Expand Down Expand Up @@ -980,7 +980,8 @@ function sourceLooksFastApi(source: string): boolean {

function parseFastApiRoutes(filePath: string, source: string): FastApiRoute[] {
const routes: FastApiRoute[] = [];
let pending: Array<{ routePath: string; methods: string[] }> = [];
const prefixes = parseFastApiRouterPrefixes(source);
let pending: Array<{ target: string; routePath: string; methods: string[] }> = [];
let decoratorSource: string | null = null;
let decoratorDepth = 0;
for (const line of source.split("\n")) {
Expand Down Expand Up @@ -1016,7 +1017,14 @@ function parseFastApiRoutes(filePath: string, source: string): FastApiRoute[] {
const functionName = /^\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/u.exec(line)?.[1];
if (functionName !== undefined && pending.length > 0) {
for (const item of pending) {
routes.push({ filePath, functionName, ...item });
const prefix = prefixes.get(item.target) ?? "";
const combinedPath = combineFastApiPaths(prefix, item.routePath);
routes.push({
filePath,
functionName,
routePath: combinedPath,
methods: item.methods,
});
}
pending = [];
continue;
Expand All @@ -1038,11 +1046,12 @@ function startsFastApiRouteDecorator(line: string): boolean {
return fastApiRouteDecoratorStartPattern.test(line);
}

function parseFastApiRouteDecorator(line: string): { routePath: string; methods: string[] } | null {
function parseFastApiRouteDecorator(line: string): { target: string; routePath: string; methods: string[] } | null {
const match = fastApiRouteDecoratorPattern.exec(line);
const method = match?.[1];
const args = match?.[2];
if (method === undefined || args === undefined) {
const target = match?.[1];
const method = match?.[2];
const args = match?.[3];
if (target === undefined || method === undefined || args === undefined) {
return null;
}
const routePath = parseFastApiPath(args);
Expand All @@ -1053,7 +1062,94 @@ function parseFastApiRouteDecorator(line: string): { routePath: string; methods:
if (methods === null) {
return null;
}
return { routePath, methods };
return { target, routePath, methods };
}

function parseFastApiRouterPrefixes(source: string): Map<string, string> {
const prefixes = new Map<string, string>();
let index = 0;
while (true) {
const apiRouterIndex = source.indexOf("APIRouter(", index);
if (apiRouterIndex === -1) {
break;
}
index = apiRouterIndex + 10;
const prefixSegment = source.slice(0, apiRouterIndex);
const varMatch = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:[A-Za-z_][A-Za-z0-9_]*\.)?$/u.exec(prefixSegment.trimEnd());
if (varMatch === null) {
continue;
}
const varName = varMatch[1];
if (varName === undefined) {
continue;
}
const openParenIndex = apiRouterIndex + 9;
const closeParenIndex = findBalancedParenthesis(source, openParenIndex + 1);
if (closeParenIndex === -1) {
continue;
}
const argsStr = source.slice(openParenIndex + 1, closeParenIndex);
const args = splitTopLevelPythonArgs(argsStr);
for (const arg of args) {
const match = /^\s*prefix\s*=\s*(.*)$/u.exec(arg);
if (match?.[1] !== undefined) {
const val = pythonStringLiteralValue(match[1]);
if (val !== null) {
prefixes.set(varName, val);
break;
}
}
}
}
return prefixes;
}

function findBalancedParenthesis(source: string, start: number): number {
let depth = 1;
let quote: string | null = null;
let escaped = false;
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (char === undefined) {
break;
}
if (quote !== null) {
if (escaped) {
escaped = false;
} else if (char === "\\") {
escaped = true;
} else if (char === quote) {
quote = null;
}
continue;
}
if (char === '"' || char === "'") {
quote = char;
} else if (char === "(") {
depth += 1;
} else if (char === ")") {
depth -= 1;
if (depth === 0) {
return index;
}
}
}
return -1;
}

function combineFastApiPaths(prefix: string, routePath: string): string {
const cleanPrefix = prefix.trim().replace(/^\/+/u, "").replace(/\/+$/u, "");
const cleanPath = routePath.trim().replace(/^\/+/u, "").replace(/\/+$/u, "");
if (cleanPrefix.length === 0 && cleanPath.length === 0) {
return "/";
}
if (cleanPrefix.length === 0) {
return `/${cleanPath}`;
}
if (cleanPath.length === 0) {
return `/${cleanPrefix}`;
}
return `/${cleanPrefix}/${cleanPath}`;
}

function parseFastApiPath(args: string): string | null {
Expand Down