-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathanalyzer.ts
More file actions
115 lines (103 loc) · 3.55 KB
/
analyzer.ts
File metadata and controls
115 lines (103 loc) · 3.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* Analyzer module to extract FastAPI-related information from syntax trees.
*/
import type { Tree } from "web-tree-sitter"
import { logError } from "../utils/logger"
import {
collectRecognizedNames,
collectStringVariables,
decoratorExtractor,
factoryCallExtractor,
getNodesByType,
importExtractor,
includeRouterExtractor,
mountExtractor,
routerExtractor,
} from "./extractors.js"
import type { FileSystem } from "./filesystem"
import type { FileAnalysis } from "./internal"
import type { Parser } from "./parser.js"
function notNull<T>(value: T | null): value is T {
return value !== null
}
function resolveVariables(
path: string,
variables: Map<string, string>,
): string {
// Match sentinel-wrapped names produced by extractPathFromNode for identifiers.
// Using \uE000 (Unicode private use) as sentinel ensures FastAPI path parameters
// like {id} are never substituted — only actual identifier references are resolved.
return path.replace(
/\uE000([^\uE000]+)\uE000/g,
(_, name) => variables.get(name) ?? `{${name}}`,
)
}
/** Analyze a syntax tree and extract FastAPI-related information */
export function analyzeTree(tree: Tree, filePath: string): FileAnalysis {
const nodesByType = getNodesByType(tree.rootNode)
// Get all decorated definitions (functions and classes with decorators)
const decoratedDefs = nodesByType.get("decorated_definition") ?? []
const routes = decoratedDefs.flatMap(decoratorExtractor)
// Get all router assignments
const assignments = nodesByType.get("assignment") ?? []
const { fastAPINames, apiRouterNames } = collectRecognizedNames(nodesByType)
const knownConstructors = new Set([...fastAPINames, ...apiRouterNames])
const factoryCalls = assignments
.map((node) => factoryCallExtractor(node, knownConstructors))
.filter(notNull)
const routers = assignments
.map((node) => routerExtractor(node, apiRouterNames, fastAPINames))
.filter(notNull)
// Get all include_router and mount calls
const callNodes = nodesByType.get("call") ?? []
const includeRouters = callNodes.map(includeRouterExtractor).filter(notNull)
const mounts = callNodes.map(mountExtractor).filter(notNull)
// Get all import statements
const importNodes = nodesByType.get("import_statement") ?? []
const importFromNodes = nodesByType.get("import_from_statement") ?? []
const imports = [...importNodes, ...importFromNodes]
.map(importExtractor)
.filter(notNull)
const stringVariables = collectStringVariables(nodesByType)
for (const route of routes) {
route.path = resolveVariables(route.path, stringVariables)
}
for (const router of routers) {
router.prefix = resolveVariables(router.prefix, stringVariables)
}
for (const ir of includeRouters) {
ir.prefix = resolveVariables(ir.prefix, stringVariables)
}
for (const mount of mounts) {
mount.path = resolveVariables(mount.path, stringVariables)
}
return {
filePath,
routes,
routers,
includeRouters,
mounts,
imports,
factoryCalls,
}
}
/** Analyze a file given its URI string and a parser instance */
export async function analyzeFile(
fileUri: string,
parser: Parser,
fs: FileSystem,
): Promise<FileAnalysis | null> {
try {
const content = await fs.readFile(fileUri)
const code = new TextDecoder().decode(content)
const tree = parser.parse(code)
if (!tree) {
logError(`Failed to parse file: "${fileUri}"`)
return null
}
return analyzeTree(tree, fileUri)
} catch (error) {
logError(`Error reading file: "${fileUri}"`, error)
return null
}
}