⚡ perf(vscode): FastAPI 规则整份文件的路由对象、接口一份文档只扫一遍,各块共用

This commit is contained in:
cruldra committed 2026-10-13 14:15:25 +11:00
1 parent ded0395d1f
commit 80d9962856
2 files changed
+66 -21

No files matched your search

+24 -1
View File
@@ -1,9 +1,10 @@
import type { BlockView } from '../blockView'
import type { Inline, PropertyRow, RuleContext, Segment } from '../contract'
import type { CodeBlock, Inline, PropertyRow, RuleContext, Segment } from '../contract'
import type { DefinitionStart } from '../nameResolution'
import { describe, expect, it, vi } from 'vitest'
import { toBlockViews } from '../blockView'
import { createPythonAdapter } from '../languages/python'
import { importsIn } from '../libraryRules'
import { DocumentLookups } from '../ruleLookups'
import { flatSegments, inlineOf, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
@@ -526,3 +527,25 @@ def dynamic_route():
expect((await route(source, 'dynamic_route')).callTree).toBeUndefined()
})
})
describe('整份文件的扫描', () => {
it('同一份文档的各块共用一遍扫描(宿主给的 context 是同一个),认出的卡片和每块各扫一遍一样', async () => {
const cards = (context: () => RuleContext, blocks: readonly CodeBlock[]): unknown[] => {
const all = (list: readonly CodeBlock[]): CodeBlock[] => list.flatMap(block => [block, ...all(block.children)])
return all(blocks).map(block => fastapiRule.claim(block, context()))
}
await withBlocks(py, 'python', ROUTES, (blocks) => {
const imports = importsIn(blocks)
let reads = 0
const base = { document: { source: ROUTES, language: py, markers: [] }, translate: () => [], doc: () => undefined, lookup: () => undefined }
const shared: RuleContext = { ...base, get imports() {
reads++
return imports
} }
const once = cards(() => shared, blocks)
expect(reads).toBe(1)
expect(once.filter(Boolean).length).toBeGreaterThan(2)
expect(once).toEqual(cards(() => ({ ...base, imports }), blocks))
})
})
})
+42 -20
View File
@@ -241,8 +241,8 @@ function namesOf(imports: readonly ImportFacts[]): LibraryNames {
}
/** 调用的是 fastapi / starlette 里的哪个名字(`Query(...)`、`fastapi.APIRouter(...)`)。 */
function calledName(node: Node | null | undefined, env: Env): string | undefined {
return node?.type === 'call' ? libraryName(node.childForFieldName('function'), env.names) : undefined
function calledName(node: Node | null | undefined, names: LibraryNames): string | undefined {
return node?.type === 'call' ? libraryName(node.childForFieldName('function'), names) : undefined
}
/** 列表、元组的各项;别的写法当一项。 */
@@ -262,13 +262,13 @@ interface Router {
responses?: Node
}
function routersOf(root: Node, env: Env): Map<string, Router> {
function routersOf(root: Node, names: LibraryNames): Map<string, Router> {
const routers = new Map<string, Router>()
for (const statement of syntaxChildren(root)) {
const [assignment] = statement.type === 'expression_statement' ? syntaxChildren(statement) : []
const target = assignment?.type === 'assignment' ? assignment.childForFieldName('left') : null
const value = assignment?.childForFieldName('right')
const kind = calledName(value, env)
const kind = calledName(value, names)
const call = callOf(value, new Map())
if (target?.type !== 'identifier' || !call || (kind !== 'APIRouter' && kind !== 'FastAPI'))
continue
@@ -311,16 +311,40 @@ interface RouteOn {
name: Node
}
/** 这份文件模块顶层挂在 router 上的接口,照源码顺序;一个函数几个路由装饰器时同接口卡片,取第一个。 */
function routesOn(root: Node, routers: ReadonlyMap<string, Router>, router: Router): RouteOn[] {
return syntaxChildren(root).flatMap((statement) => {
/** 这份文件模块顶层挂在各路由对象上的接口,照源码顺序;一个函数几个路由装饰器时同接口卡片,取第一个。 */
function routesOn(root: Node, routers: ReadonlyMap<string, Router>): Map<Router, RouteOn[]> {
const routes = new Map<Router, RouteOn[]>()
for (const statement of syntaxChildren(root)) {
const definition = statement.type === 'decorated_definition' ? statement.childForFieldName('definition') : null
const name = definition?.type === 'function_definition' ? definition.childForFieldName('name') : null
if (!definition || !name)
return []
continue
const route = syntaxChildren(statement).filter(child => child.type === 'decorator').map(decorator => routeOf(decorator, routers)).find(found => found !== undefined)
return route?.router === router ? [{ route, definition, name }] : []
})
if (route)
routes.set(route.router, [...routes.get(route.router) ?? [], { route, definition, name }])
}
return routes
}
/** 认接口要整份扫一遍才知道的:fastapi / starlette 的名字、模块顶层的路由对象、各路由对象上挂的接口。 */
interface FileScan {
names: LibraryNames
routers: Map<string, Router>
routes: Map<Router, RouteOn[]>
}
/** 宿主每读一份文档给各块同一个 context:整份文件按它扫一遍,这份文档的各块共用。 */
const scans = new WeakMap<RuleContext, FileScan>()
function scanOf(root: Node, context: RuleContext): FileScan {
let scan = scans.get(context)
if (!scan) {
const names = namesOf(context.imports)
const routers = routersOf(root, names)
scan = { names, routers, routes: routesOn(root, routers) }
scans.set(context, scan)
}
return scan
}
/** 函数的参数名 → 定义处那个名字(`*args` 这种不算),路径里 `{参数}` 讲的是它。 */
@@ -428,10 +452,10 @@ function placeOf(parameter: ParameterFacts, inPath: ReadonlySet<string>, env: En
const param = parameter.name
if (param.type !== 'identifier')
return undefined
const annotated = annotatedMarker(parameter.type, call => calledName(call, env))
const annotated = annotatedMarker(parameter.type, call => calledName(call, env.names))
const type = annotated?.type ?? (parameter.type && unwrapType(parameter.type))
const markerCall = MARKER_PLACES[calledName(parameter.default, env) ?? ''] ? parameter.default : annotated?.marker
const marker = markerCall && calledName(markerCall, env)
const markerCall = MARKER_PLACES[calledName(parameter.default, env.names) ?? ''] ? parameter.default : annotated?.marker
const marker = markerCall && calledName(markerCall, env.names)
if (markerCall && marker && MARKER_PLACES[marker] === 'dependency') {
const call = callOf(markerCall, new Map())
return { place: 'dependency', param, dependency: call?.positional[0] ?? call?.keywords.get('dependency') ?? type }
@@ -481,7 +505,7 @@ function dependencyText(node: Node, env: Env): Inline[] {
/** `dependencies=[Depends(x), ...]` 里的各个 x。 */
function listedDependencies(items: readonly Node[], env: Env): Inline[][] {
return items.flatMap((item) => {
const call = MARKER_PLACES[calledName(item, env) ?? ''] === 'dependency' ? callOf(item, new Map()) : undefined
const call = MARKER_PLACES[calledName(item, env.names) ?? ''] === 'dependency' ? callOf(item, new Map()) : undefined
const dependency = call?.positional[0] ?? call?.keywords.get('dependency')
return dependency ? [dependencyText(dependency, env)] : []
})
@@ -812,23 +836,21 @@ export const fastapiRule: LibraryRule = {
languageIds: ['python'],
libraries: ['fastapi'],
claim: (block, context) => {
const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH, names: namesOf(context.imports) }
const root = block.node.tree.rootNode
const { names, routers, routes } = scanOf(block.node.tree.rootNode, context)
const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH, names }
if (block.kind === 'function' && block.facts.decorators.length > 0) {
const routers = routersOf(root, env)
for (const decorator of block.facts.decorators) {
const route = routeOf(decorator, routers)
if (route)
return routeCard(block, route, env, routesOn(root, routers, route.router))
return routeCard(block, route, env, routes.get(route.router) ?? [])
}
}
// 路由对象只认模块顶层的赋值(routersOf),按赋值目标的位置认出是不是这一块
if ((block.kind === 'const' || block.kind === 'variable') && block.facts.declarators.length === 1) {
const routers = routersOf(root, env)
const target = block.facts.declarators[0].name
const router = routers.get(target.text)
if (router?.target.startIndex === target.startIndex)
return routerCard(block, router, routesOn(root, routers, router), env)
return routerCard(block, router, routes.get(router) ?? [], env)
}
return undefined
},