diff --git a/vscode/src/codeReader/rules/fastapi.test.ts b/vscode/src/codeReader/rules/fastapi.test.ts index f6be1b8..93bdb82 100644 --- a/vscode/src/codeReader/rules/fastapi.test.ts +++ b/vscode/src/codeReader/rules/fastapi.test.ts @@ -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)) + }) + }) +}) diff --git a/vscode/src/codeReader/rules/fastapi.ts b/vscode/src/codeReader/rules/fastapi.ts index 4c9545c..7c9045a 100644 --- a/vscode/src/codeReader/rules/fastapi.ts +++ b/vscode/src/codeReader/rules/fastapi.ts @@ -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 { +function routersOf(root: Node, names: LibraryNames): Map { const routers = new Map() 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, router: Router): RouteOn[] { - return syntaxChildren(root).flatMap((statement) => { +/** 这份文件模块顶层挂在各路由对象上的接口,照源码顺序;一个函数几个路由装饰器时同接口卡片,取第一个。 */ +function routesOn(root: Node, routers: ReadonlyMap): Map { + const routes = new Map() + 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 + routes: Map +} + +/** 宿主每读一份文档给各块同一个 context:整份文件按它扫一遍,这份文档的各块共用。 */ +const scans = new WeakMap() + +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, 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 },