⚡ perf(vscode): FastAPI 规则整份文件的路由对象、接口一份文档只扫一遍,各块共用
This commit is contained in:
1 parent
ded0395d1f
commit
80d9962856
2 files changed
+66
-21
No files matched your search
@@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user