✨ feat(vscode): 代码阅读的函数和类可以看调用树

This commit is contained in:
cruldra committed 2026-10-11 00:37:24 -04:00
1 parent 8dc572bf94
commit 645542eb5c
17 files changed
+1679 -5

No files matched your search

@@ -0,0 +1,173 @@
import type { CallEdge, CallRange, CallSymbol, CallTreeNode } from './model'
import { describe, expect, it } from 'vitest'
import { buildCallTrees } from './model'
function symbol(name: string, line = 0, extra: Partial<CallSymbol> = {}): CallSymbol {
return {
uri: `file:///${name}`,
line,
character: 0,
name,
kind: 'function',
path: `${name}.py`,
...extra,
}
}
function at(line: number, character = 0): CallRange {
return { line, character, endLine: line, endCharacter: character + 4 }
}
function edge(caller: CallSymbol, callee: CallSymbol, line: number, character = 0): CallEdge {
return { caller, callee, at: at(line, character) }
}
function currents(node: CallTreeNode): CallTreeNode[] {
return [...(node.current ? [node] : []), ...node.children.flatMap(currents)]
}
describe('调用边收成调用树', () => {
const mask = symbol('mask_phone', 27, { path: 'utils/phone.py' })
it('两个没有更上层调用者的根是两棵树,每棵记下当前块出现几次', () => {
const read = symbol('读取用户资料', 0, { path: 'routes/user.py' })
const profile = symbol('get_profile', 10, { path: 'services/user.py' })
const serialize = symbol('serialize_user', 20, { path: 'services/user.py' })
const load = symbol('load_user', 30, { path: 'services/user.py' })
const exp = symbol('导出通讯录', 0, { path: 'routes/export.py' })
const trees = buildCallTrees([
edge(serialize, mask, 40),
edge(load, mask, 50),
edge(profile, serialize, 12),
edge(profile, load, 14),
edge(read, profile, 3),
edge(exp, mask, 8),
], mask)
expect(trees.map(tree => tree.name)).toEqual(['读取用户资料', '导出通讯录'])
expect(trees.map(tree => tree.hits)).toEqual([2, 1])
expect(trees[0].root.detail).toBe('入口')
expect(trees[0].root.where).toBe('routes/user.py:1')
expect(currents(trees[0].root).map(node => node.where)).toEqual(['utils/phone.py:28', 'utils/phone.py:28'])
expect(currents(trees[1].root)).toHaveLength(1)
})
it('同一个调用者的两段不同范围是当前块的两次出现', () => {
const caller = symbol('serialize_user', 20, { path: 'services/user.py' })
const root = symbol('读取用户资料', 0, { path: 'routes/user.py' })
const trees = buildCallTrees([
edge(caller, mask, 10),
edge(caller, mask, 20),
edge(root, caller, 3),
], mask)
expect(trees).toHaveLength(1)
expect(trees[0].hits).toBe(2)
const parent = trees[0].root.children[0]
expect(parent.name).toBe('serialize_user')
expect(parent.children.map(node => node.current)).toEqual([true, true])
expect(parent.otherCalls).toBeUndefined()
})
it('不在通向当前块的路径上的调用收成一行,不把那些符号画出来', () => {
const caller = symbol('serialize_user', 20, { path: 'services/user.py' })
const root = symbol('读取用户资料', 0, { path: 'routes/user.py' })
const trees = buildCallTrees([
edge(caller, mask, 10),
edge(caller, symbol('format_name'), 11),
edge(caller, symbol('format_name'), 12, 2),
edge(root, caller, 3),
edge(root, symbol('sideways'), 4),
edge(root, symbol('leftover'), 5),
], mask)
expect(trees).toHaveLength(1)
expect(trees[0].root.otherCalls).toBe(2)
expect(trees[0].root.children).toHaveLength(1)
expect(trees[0].root.children[0].otherCalls).toBe(2)
expect(trees[0].root.children[0].children.map(node => node.name)).toEqual(['mask_phone'])
const text = JSON.stringify(trees)
expect(text).not.toContain('format_name')
expect(text).not.toContain('sideways')
expect(text).not.toContain('leftover')
})
it('调用边绕回去就停,不把当前块再画到自己上面', () => {
const caller = symbol('get_profile')
const trees = buildCallTrees([
edge(caller, mask, 1),
edge(mask, caller, 2),
], mask)
expect(trees).toHaveLength(1)
expect(trees[0].name).toBe('get_profile')
expect(trees[0].root.children.map(node => node.name)).toEqual(['mask_phone'])
expect(trees[0].root.children[0].current).toBe(true)
expect(trees[0].root.children[0].children).toEqual([])
expect(trees[0].hits).toBe(1)
})
it('往上最多 8 层,再高的调用者不进这棵树', () => {
const above = Array.from({ length: 9 }, (_, index) => symbol(`n${index}`, index))
const edges = [edge(above[0], mask, 1)]
for (let index = 1; index < above.length; index++)
edges.push(edge(above[index], above[index - 1], index + 1))
const trees = buildCallTrees(edges, mask)
expect(trees).toHaveLength(1)
expect(trees[0].name).toBe('n7')
expect(JSON.stringify(trees)).not.toContain('n8')
})
it('重复的同一段调用范围只算一次', () => {
const caller = symbol('serialize_user')
const trees = buildCallTrees([
edge(caller, mask, 10),
edge(caller, mask, 10),
], mask)
expect(trees).toHaveLength(1)
expect(trees[0].hits).toBe(1)
})
it('同一次查询里种类变了仍是同一个符号,不拆成两棵', () => {
const asFunction = symbol('get_profile', 4, { uri: 'file:///services/user.py', path: 'services/user.py', kind: 'function' })
const asClass = symbol('get_profile', 4, { uri: 'file:///services/user.py', path: 'services/user.py', kind: 'class' })
const trees = buildCallTrees([
edge(asFunction, mask, 8),
edge(asClass, mask, 8),
], mask)
expect(trees).toHaveLength(1)
expect(trees[0].hits).toBe(1)
expect(trees[0].root.kind).toBe('function')
})
it('没有任何调用者就没有树', () => {
expect(buildCallTrees([], mask)).toEqual([])
})
it('选中的是类时,直接调用它的那一层是「新建」,类自己标当前,不编造方法', () => {
const order = symbol('OrderService', 13, { uri: 'file:///billing/order.py', path: 'billing/order.py', kind: 'class' })
const create = symbol('create_order', 20, { path: 'billing/order.py', detail: '创建一笔订单' })
const post = symbol('POST /orders', 0, { path: 'routes/order.py' })
const trees = buildCallTrees([
edge(create, order, 39),
edge(post, create, 5),
edge(create, symbol('charge'), 41),
], order, { classTarget: true })
expect(trees).toHaveLength(1)
expect(trees[0].hits).toBe(1)
const created = trees[0].root.children[0]
expect(created.name).toBe('create_order')
expect(created.detail).toBe('创建一笔订单')
expect(created.otherCalls).toBe(1)
const made = created.children[0]
expect(made.name).toBe('新建 OrderService')
expect(made.detail).toBe('进入这个类')
expect(made.where).toBe('billing/order.py:40')
expect(made.current).toBe(false)
expect(made.kind).toBe('class')
expect(made.children).toEqual([{
name: 'OrderService',
kind: 'class',
where: 'billing/order.py:14',
current: true,
children: [],
}])
expect(JSON.stringify(trees)).not.toContain('方法')
})
})