Skip to content

Commit 346aa66

Browse files
🐛 Handle module aliase
1 parent 2621cbd commit 346aa66

6 files changed

Lines changed: 224 additions & 68 deletions

File tree

src/core/analyzer.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import type { Tree } from "web-tree-sitter"
66
import { logError } from "../utils/logger"
77
import {
8-
collectAPIRouterSubclasses,
9-
collectFastAPIAliases,
8+
collectRecognizedNames,
109
collectStringVariables,
1110
decoratorExtractor,
1211
findNodesByType,
@@ -46,11 +45,9 @@ export function analyzeTree(tree: Tree, filePath: string): FileAnalysis {
4645

4746
// Get all router assignments
4847
const assignments = findNodesByType(rootNode, "assignment")
49-
const apiRouterSubclasses = collectAPIRouterSubclasses(rootNode)
50-
const { fastAPIAliases, apiRouterAliases } = collectFastAPIAliases(rootNode)
51-
for (const alias of apiRouterAliases) apiRouterSubclasses.add(alias)
48+
const { fastAPINames, apiRouterNames } = collectRecognizedNames(rootNode)
5249
const routers = assignments
53-
.map((node) => routerExtractor(node, apiRouterSubclasses, fastAPIAliases))
50+
.map((node) => routerExtractor(node, apiRouterNames, fastAPINames))
5451
.filter(notNull)
5552

5653
// Get all include_router and mount calls

src/core/extractors.ts

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,47 +48,66 @@ function stripDocstring(raw: string): string {
4848
const dedented = lines.map((l, i) => (i === 0 ? l : l.slice(minIndent)))
4949
return dedented.join("\n").trim()
5050
}
51-
52-
export function collectAPIRouterSubclasses(rootNode: Node): Set<string> {
53-
const subclasses = new Set<string>()
54-
for (const cls of findNodesByType(rootNode, "class_definition")) {
55-
const nameNode = cls.childForFieldName("name")
56-
const superclassesNode = cls.childForFieldName("superclasses")
57-
if (!nameNode || !superclassesNode) continue
58-
59-
const extendsAPIRouter = superclassesNode.namedChildren.some(
60-
(s) => s.text === "APIRouter" || s.text === "fastapi.APIRouter",
61-
)
62-
if (extendsAPIRouter) {
63-
subclasses.add(nameNode.text)
64-
}
65-
}
66-
return subclasses
67-
}
68-
69-
/**
70-
* Collects aliases for FastAPI and APIRouter from import statements.
71-
* Handles: from fastapi import FastAPI as FA, APIRouter as AR
51+
/*
52+
* Extracts recognized FastAPI and APIRouter names from imports and class definitions
53+
* This allows the rest of the extractors to handle user-defined aliases and subclasses.
54+
*
55+
* For example, if the code has:
56+
* from fastapi import FastAPI as MyApp
57+
* from fastapi import APIRouter as MyRouter
58+
* class CustomRouter(MyRouter): ...
59+
*
60+
* Then this function will return:
61+
* fastAPINames = Set { "FastAPI", "fastapi.FastAPI", "MyApp" }
62+
* apiRouterNames = Set { "APIRouter", "fastapi.APIRouter", "MyRouter", "CustomRouter" }
63+
*
64+
* This allows decoratorExtractor and routerExtractor to recognize routes and routers
65+
* defined using these aliases and subclasses without needing complex logic in those functions.
7266
*/
73-
export function collectFastAPIAliases(rootNode: Node): {
74-
fastAPIAliases: Set<string>
75-
apiRouterAliases: Set<string>
67+
export function collectRecognizedNames(rootNode: Node): {
68+
fastAPINames: Set<string>
69+
apiRouterNames: Set<string>
7670
} {
77-
const fastAPIAliases = new Set<string>()
78-
const apiRouterAliases = new Set<string>()
71+
const fastAPINames = new Set<string>(["FastAPI", "fastapi.FastAPI"])
72+
const apiRouterNames = new Set<string>(["APIRouter", "fastapi.APIRouter"])
7973

74+
// Add aliases from "from fastapi import X as Y" imports
8075
for (const node of findNodesByType(rootNode, "import_from_statement")) {
8176
const info = importExtractor(node)
8277
if (!info || info.modulePath !== "fastapi") continue
78+
for (const named of info.namedImports) {
79+
if (named.alias === null) continue
80+
if (named.name === "FastAPI") fastAPINames.add(named.alias)
81+
else if (named.name === "APIRouter") apiRouterNames.add(named.alias)
82+
}
83+
}
84+
85+
// Add module aliases from "import fastapi as f" → recognizes f.FastAPI, f.APIRouter
86+
for (const node of findNodesByType(rootNode, "import_statement")) {
87+
const info = importExtractor(node)
88+
if (!info) continue
89+
for (const named of info.namedImports) {
90+
if (named.alias === null) continue
91+
if (named.name === "fastapi") {
92+
fastAPINames.add(`${named.alias}.FastAPI`)
93+
apiRouterNames.add(`${named.alias}.APIRouter`)
94+
}
95+
}
96+
}
8397

84-
for (const ni of info.namedImports) {
85-
if (ni.alias === null) continue
86-
if (ni.name === "FastAPI") fastAPIAliases.add(ni.alias)
87-
if (ni.name === "APIRouter") apiRouterAliases.add(ni.alias)
98+
// Add subclasses, checking against the already-accumulated alias sets so
99+
// "class MyRouter(AR)" works when AR is an alias for APIRouter
100+
for (const cls of findNodesByType(rootNode, "class_definition")) {
101+
const nameNode = cls.childForFieldName("name")
102+
const superclassesNode = cls.childForFieldName("superclasses")
103+
if (!nameNode || !superclassesNode) continue
104+
for (const parent of superclassesNode.namedChildren) {
105+
if (apiRouterNames.has(parent.text)) apiRouterNames.add(nameNode.text)
106+
else if (fastAPINames.has(parent.text)) fastAPINames.add(nameNode.text)
88107
}
89108
}
90109

91-
return { fastAPIAliases, apiRouterAliases }
110+
return { fastAPINames, apiRouterNames }
92111
}
93112

94113
function collectNodesByType(node: Node, type: string, results: Node[]): void {
@@ -281,8 +300,8 @@ function extractTags(listNode: Node): string[] {
281300

282301
export function routerExtractor(
283302
node: Node,
284-
apiRouterSubclasses?: Set<string>,
285-
fastAPIAliases?: Set<string>,
303+
apiRouterNames?: Set<string>,
304+
fastAPINames?: Set<string>,
286305
): RouterInfo | null {
287306
if (node.type !== "assignment") {
288307
return null
@@ -299,13 +318,13 @@ export function routerExtractor(
299318
if (
300319
funcName === "APIRouter" ||
301320
funcName === "fastapi.APIRouter" ||
302-
(funcName !== undefined && apiRouterSubclasses?.has(funcName))
321+
(funcName !== undefined && apiRouterNames?.has(funcName))
303322
) {
304323
type = "APIRouter"
305324
} else if (
306325
funcName === "FastAPI" ||
307326
funcName === "fastapi.FastAPI" ||
308-
(funcName !== undefined && fastAPIAliases?.has(funcName))
327+
(funcName !== undefined && fastAPINames?.has(funcName))
309328
) {
310329
type = "FastAPI"
311330
} else {
@@ -380,13 +399,26 @@ export function importExtractor(node: Node): ImportInfo | null {
380399
const namedImports: ImportedName[] = []
381400

382401
if (node.type === "import_statement") {
402+
// Handle aliased imports: "import fastapi as f"
403+
for (const aliased of findNodesByType(node, "aliased_import")) {
404+
const nameNode = aliased.childForFieldName("name")
405+
const aliasNode = aliased.childForFieldName("alias")
406+
if (nameNode) {
407+
const alias = aliasNode?.text ?? null
408+
names.push(alias ?? nameNode.text)
409+
namedImports.push({ name: nameNode.text, alias })
410+
}
411+
}
412+
// Non-aliased: "import fastapi"
383413
const nameNodes = findNodesByType(node, "dotted_name")
384414
for (const nameNode of nameNodes) {
385-
const firstName = nameNode.text.split(".")[0]
386-
names.push(firstName)
387-
namedImports.push({ name: firstName, alias: null })
415+
if (!hasAncestor(nameNode, "aliased_import")) {
416+
const firstName = nameNode.text.split(".")[0]
417+
names.push(firstName)
418+
namedImports.push({ name: firstName, alias: null })
419+
}
388420
}
389-
const modulePath = nameNodes[0]?.text ?? ""
421+
const modulePath = namedImports[0]?.name ?? ""
390422
return {
391423
modulePath,
392424
names,

src/test/core/extractors.test.ts

Lines changed: 109 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as assert from "node:assert"
22
import {
3-
collectAPIRouterSubclasses,
4-
collectFastAPIAliases,
3+
collectRecognizedNames,
54
decoratorExtractor,
65
extractPathFromNode,
76
extractStringValue,
@@ -477,44 +476,50 @@ def list_users():
477476
assert.strictEqual(result, null)
478477
})
479478

480-
test("recognizes custom APIRouter subclass when subclasses set provided", () => {
479+
test("recognizes custom APIRouter subclass", () => {
481480
const code = `
482481
class AdminAPIRouter(APIRouter):
483482
pass
484483
485484
admin_router = AdminAPIRouter(prefix="/admin")
486485
`
487486
const tree = parse(code)
488-
const subclasses = collectAPIRouterSubclasses(tree.rootNode)
489-
assert.ok(subclasses.has("AdminAPIRouter"))
487+
const { apiRouterNames } = collectRecognizedNames(tree.rootNode)
488+
assert.ok(apiRouterNames.has("AdminAPIRouter"))
490489

491490
const assignments = findNodesByType(tree.rootNode, "assignment")
492-
const result = routerExtractor(assignments[0], subclasses)
491+
const result = routerExtractor(assignments[0], apiRouterNames)
493492

494493
assert.ok(result)
495494
assert.strictEqual(result.variableName, "admin_router")
496495
assert.strictEqual(result.type, "APIRouter")
497496
assert.strictEqual(result.prefix, "/admin")
498497
})
499498

500-
test("does not treat FastAPI subclass as APIRouter", () => {
499+
test("recognizes FastAPI subclass", () => {
501500
const code = `
502501
class MyApp(FastAPI):
503502
pass
504503
505504
app = MyApp()
506505
`
507506
const tree = parse(code)
508-
const subclasses = collectAPIRouterSubclasses(tree.rootNode)
509-
assert.ok(
510-
!subclasses.has("MyApp"),
511-
"FastAPI subclass should not be in APIRouter subclasses",
507+
const { fastAPINames, apiRouterNames } = collectRecognizedNames(
508+
tree.rootNode,
512509
)
510+
assert.ok(fastAPINames.has("MyApp"))
511+
assert.ok(!apiRouterNames.has("MyApp"))
513512

514513
const assignments = findNodesByType(tree.rootNode, "assignment")
515-
const result = routerExtractor(assignments[0], subclasses)
514+
const result = routerExtractor(
515+
assignments[0],
516+
apiRouterNames,
517+
fastAPINames,
518+
)
516519

517-
assert.strictEqual(result, null)
520+
assert.ok(result)
521+
assert.strictEqual(result.variableName, "app")
522+
assert.strictEqual(result.type, "FastAPI")
518523
})
519524

520525
test("recognizes aliased FastAPI import (FastAPI as FA)", () => {
@@ -524,11 +529,17 @@ from fastapi import FastAPI as FA
524529
app = FA()
525530
`
526531
const tree = parse(code)
527-
const { fastAPIAliases } = collectFastAPIAliases(tree.rootNode)
528-
assert.ok(fastAPIAliases.has("FA"))
532+
const { fastAPINames, apiRouterNames } = collectRecognizedNames(
533+
tree.rootNode,
534+
)
535+
assert.ok(fastAPINames.has("FA"))
529536

530537
const assignments = findNodesByType(tree.rootNode, "assignment")
531-
const result = routerExtractor(assignments[0], undefined, fastAPIAliases)
538+
const result = routerExtractor(
539+
assignments[0],
540+
apiRouterNames,
541+
fastAPINames,
542+
)
532543

533544
assert.ok(result)
534545
assert.strictEqual(result.variableName, "app")
@@ -542,26 +553,83 @@ from fastapi import APIRouter as AR
542553
router = AR(prefix="/items")
543554
`
544555
const tree = parse(code)
545-
const { apiRouterAliases } = collectFastAPIAliases(tree.rootNode)
546-
assert.ok(apiRouterAliases.has("AR"))
556+
const { apiRouterNames } = collectRecognizedNames(tree.rootNode)
557+
assert.ok(apiRouterNames.has("AR"))
547558

548559
const assignments = findNodesByType(tree.rootNode, "assignment")
549-
const result = routerExtractor(assignments[0], apiRouterAliases)
560+
const result = routerExtractor(assignments[0], apiRouterNames)
550561

551562
assert.ok(result)
552563
assert.strictEqual(result.variableName, "router")
553564
assert.strictEqual(result.type, "APIRouter")
554565
assert.strictEqual(result.prefix, "/items")
555566
})
556567

557-
test("collectFastAPIAliases ignores non-aliased imports", () => {
568+
test("recognizes subclass of aliased APIRouter (class MyRouter(AR))", () => {
569+
const code = `
570+
from fastapi import APIRouter as AR
571+
572+
class MyRouter(AR):
573+
pass
574+
575+
router = MyRouter(prefix="/items")
576+
`
577+
const tree = parse(code)
578+
const { apiRouterNames } = collectRecognizedNames(tree.rootNode)
579+
assert.ok(apiRouterNames.has("AR"))
580+
assert.ok(apiRouterNames.has("MyRouter"))
581+
582+
const assignments = findNodesByType(tree.rootNode, "assignment")
583+
const result = routerExtractor(assignments[0], apiRouterNames)
584+
585+
assert.ok(result)
586+
assert.strictEqual(result.type, "APIRouter")
587+
})
588+
589+
test("collectRecognizedNames ignores non-aliased imports", () => {
558590
const code = "from fastapi import FastAPI, APIRouter"
559591
const tree = parse(code)
560-
const { fastAPIAliases, apiRouterAliases } = collectFastAPIAliases(
592+
const { fastAPINames, apiRouterNames } = collectRecognizedNames(
593+
tree.rootNode,
594+
)
595+
// Only the defaults — no extras from non-aliased imports
596+
assert.strictEqual(fastAPINames.size, 2) // "FastAPI", "fastapi.FastAPI"
597+
assert.strictEqual(apiRouterNames.size, 2) // "APIRouter", "fastapi.APIRouter"
598+
})
599+
600+
test("recognizes module alias (import fastapi as f)", () => {
601+
const code = `
602+
import fastapi as f
603+
604+
app = f.FastAPI()
605+
router = f.APIRouter(prefix="/items")
606+
`
607+
const tree = parse(code)
608+
const { fastAPINames, apiRouterNames } = collectRecognizedNames(
561609
tree.rootNode,
562610
)
563-
assert.strictEqual(fastAPIAliases.size, 0)
564-
assert.strictEqual(apiRouterAliases.size, 0)
611+
assert.ok(fastAPINames.has("f.FastAPI"))
612+
assert.ok(apiRouterNames.has("f.APIRouter"))
613+
614+
const assignments = findNodesByType(tree.rootNode, "assignment")
615+
const appResult = routerExtractor(
616+
assignments[0],
617+
apiRouterNames,
618+
fastAPINames,
619+
)
620+
assert.ok(appResult)
621+
assert.strictEqual(appResult.variableName, "app")
622+
assert.strictEqual(appResult.type, "FastAPI")
623+
624+
const routerResult = routerExtractor(
625+
assignments[1],
626+
apiRouterNames,
627+
fastAPINames,
628+
)
629+
assert.ok(routerResult)
630+
assert.strictEqual(routerResult.variableName, "router")
631+
assert.strictEqual(routerResult.type, "APIRouter")
632+
assert.strictEqual(routerResult.prefix, "/items")
565633
})
566634
})
567635

@@ -575,6 +643,24 @@ router = AR(prefix="/items")
575643
assert.ok(result)
576644
assert.strictEqual(result.modulePath, "fastapi")
577645
assert.deepStrictEqual(result.names, ["fastapi"])
646+
assert.deepStrictEqual(result.namedImports, [
647+
{ name: "fastapi", alias: null },
648+
])
649+
assert.strictEqual(result.isRelative, false)
650+
})
651+
652+
test("extracts aliased module import (import fastapi as f)", () => {
653+
const code = "import fastapi as f"
654+
const tree = parse(code)
655+
const imports = findNodesByType(tree.rootNode, "import_statement")
656+
const result = importExtractor(imports[0])
657+
658+
assert.ok(result)
659+
assert.strictEqual(result.modulePath, "fastapi")
660+
assert.deepStrictEqual(result.names, ["f"])
661+
assert.deepStrictEqual(result.namedImports, [
662+
{ name: "fastapi", alias: "f" },
663+
])
578664
assert.strictEqual(result.isRelative, false)
579665
})
580666

0 commit comments

Comments
 (0)