From 13f65b9ff41e5e8e9217b7a3813e6a06d7608e76 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Tue, 3 Mar 2026 14:36:38 -0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20app=20factory=20patter?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/routerResolver.ts | 18 +++++++++++++++ src/test/core/routerResolver.test.ts | 32 ++++++++++++++++++++++++++ src/test/fixtures/factory-func/app.py | 5 ++++ src/test/fixtures/factory-func/main.py | 13 +++++++++++ src/test/testUtils.ts | 4 ++++ 5 files changed, 72 insertions(+) create mode 100644 src/test/fixtures/factory-func/app.py create mode 100644 src/test/fixtures/factory-func/main.py diff --git a/src/core/routerResolver.ts b/src/core/routerResolver.ts index 55107a4..6899e0d 100644 --- a/src/core/routerResolver.ts +++ b/src/core/routerResolver.ts @@ -113,6 +113,24 @@ async function buildRouterGraphInternal( } } + // If still no router found but a targetVariable is specified and routes use it + // as their owner (e.g. `app = get_fastapi_app()` with `@app.get("/...")`), + // infer the variable is a FastAPI app so routes are not silently dropped. + if ( + !appRouter && + targetVariable && + analysis.routes.some((r) => r.owner === targetVariable) + ) { + appRouter = { + variableName: targetVariable, + type: "FastAPI", + prefix: "", + tags: [], + line: 0, + column: 0, + } + } + if (!appRouter || !analysis) { return null } diff --git a/src/test/core/routerResolver.test.ts b/src/test/core/routerResolver.test.ts index 41b1a63..b37d4c0 100644 --- a/src/test/core/routerResolver.test.ts +++ b/src/test/core/routerResolver.test.ts @@ -538,5 +538,37 @@ suite("routerResolver", () => { assert.strictEqual(result.children[0].router.prefix, "/users") assert.ok(result.children[0].router.routes.length >= 2) }) + + test("infers FastAPI app when assigned via factory function (app = get_fastapi_app())", async () => { + const result = await buildRouterGraph( + fixtures.factoryFunc.mainPy, + parser, + fixtures.factoryFunc.root, + nodeFileSystem, + "app", + ) + + assert.ok( + result, + "Should find app even when assigned via factory function", + ) + assert.strictEqual(result.type, "FastAPI") + assert.strictEqual(result.variableName, "app") + assert.strictEqual(result.routes.length, 2) + const paths = result.routes.map((r) => r.path) + assert.ok(paths.includes("/1")) + assert.ok(paths.includes("/2")) + }) + + test("returns null without targetVariable when app is a factory function", async () => { + const result = await buildRouterGraph( + fixtures.factoryFunc.mainPy, + parser, + fixtures.factoryFunc.root, + nodeFileSystem, + ) + + assert.strictEqual(result, null) + }) }) }) diff --git a/src/test/fixtures/factory-func/app.py b/src/test/fixtures/factory-func/app.py new file mode 100644 index 0000000..a7f51c6 --- /dev/null +++ b/src/test/fixtures/factory-func/app.py @@ -0,0 +1,5 @@ +from fastapi import FastAPI + + +def get_fastapi_app() -> FastAPI: + return FastAPI() diff --git a/src/test/fixtures/factory-func/main.py b/src/test/fixtures/factory-func/main.py new file mode 100644 index 0000000..6266c69 --- /dev/null +++ b/src/test/fixtures/factory-func/main.py @@ -0,0 +1,13 @@ +from app import get_fastapi_app + +app = get_fastapi_app() + + +@app.get("/1") +def one(): + return "Route one" + + +@app.get("/2") +def two(): + return "Route two" diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index 6608a7d..02751c2 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -83,6 +83,10 @@ export const fixtures = { projectRoot: uri(join(fixturesPath, "monorepo", "service")), mainPy: uri(join(fixturesPath, "monorepo", "service", "myapp", "main.py")), }, + factoryFunc: { + root: uri(join(fixturesPath, "factory-func")), + mainPy: uri(join(fixturesPath, "factory-func", "main.py")), + }, } /** From af45180c889ab9de780a1e333d36f8cb60295458 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Tue, 3 Mar 2026 14:38:32 -0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=92=A1=20Update=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/routerResolver.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/routerResolver.ts b/src/core/routerResolver.ts index 6899e0d..2c2f7a6 100644 --- a/src/core/routerResolver.ts +++ b/src/core/routerResolver.ts @@ -113,9 +113,11 @@ async function buildRouterGraphInternal( } } - // If still no router found but a targetVariable is specified and routes use it - // as their owner (e.g. `app = get_fastapi_app()` with `@app.get("/...")`), - // infer the variable is a FastAPI app so routes are not silently dropped. + // App factory pattern: if the entrypoint variable (e.g. "app" from "main:app") + // is assigned via a factory function (`app = create_app()`) rather than a direct + // FastAPI() constructor call, static analysis can't confirm the type. But if + // routes are decorated with @app.get(...) etc., we know it must be a FastAPI + // instance, so infer it rather than silently returning no results. if ( !appRouter && targetVariable && From 7521bd4832ed57ecd49680fb7c1d353d77be9598 Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Tue, 3 Mar 2026 14:41:04 -0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=92=A1=20Update=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/routerResolver.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/routerResolver.ts b/src/core/routerResolver.ts index 2c2f7a6..6b46899 100644 --- a/src/core/routerResolver.ts +++ b/src/core/routerResolver.ts @@ -113,11 +113,10 @@ async function buildRouterGraphInternal( } } - // App factory pattern: if the entrypoint variable (e.g. "app" from "main:app") + // Factory function: if the entrypoint variable (e.g. "app" from "main:app") // is assigned via a factory function (`app = create_app()`) rather than a direct - // FastAPI() constructor call, static analysis can't confirm the type. But if - // routes are decorated with @app.get(...) etc., we know it must be a FastAPI - // instance, so infer it rather than silently returning no results. + // FastAPI() constructor call, static analysis can't determine the type. If routes + // are decorated with @app.get(...) etc. though, we know it must be a FastAPI instance. if ( !appRouter && targetVariable &&