diff --git a/vscode/src/codeReader/callTree/walk.test.ts b/vscode/src/codeReader/callTree/walk.test.ts index e784ae5..cca01b3 100644 --- a/vscode/src/codeReader/callTree/walk.test.ts +++ b/vscode/src/codeReader/callTree/walk.test.ts @@ -10,6 +10,10 @@ function range(line: number, character = 0): CallRange { return { line, character, endLine: line, endCharacter: character + 3 } } +function symbolIn(path: string, name: string): CallSymbol { + return { ...symbol(name), path } +} + describe('顺着调用者往上收集调用边', () => { it('有环就停,不再问已经走过的那个', async () => { const target = symbol('mask_phone') @@ -85,6 +89,73 @@ describe('顺着调用者往上收集调用边', () => { ]) }) + it('业务代码的调用者里有测试函数:测试函数不当调用者,也不再往上问', async () => { + const target = symbolIn('app/sync.py', 'sync_users') + const job = symbolIn('app/jobs.py', 'run_jobs') + const inTestsDir = symbolIn('tests/test_sync.py', 'test_sync_runs') + const byFileName = symbolIn('test_x.py', 'test_x') + const asked: string[] = [] + const outgoingAsked: string[] = [] + const edges = await collectCallEdges(target, async (node) => { + asked.push(node.name) + if (node.name === 'sync_users') { + return [ + { from: inTestsDir, ranges: [range(3)] }, + { from: byFileName, ranges: [range(5)] }, + { from: job, ranges: [range(7)] }, + ] + } + return [] + }, async (node) => { + outgoingAsked.push(node.name) + return [] + }) + expect(asked).toEqual(['sync_users', 'run_jobs']) + expect(outgoingAsked).toEqual(['run_jobs']) + expect(edges).toEqual([{ caller: job, callee: target, at: range(7) }]) + }) + + it('往下问到的被调用方在测试文件里,这条边不收', async () => { + const target = symbolIn('app/sync.py', 'sync_users') + const job = symbolIn('app/jobs.py', 'run_jobs') + const fixture = symbolIn('tests/conftest.py', 'make_user') + const side = symbolIn('app/log.py', 'log_event') + const edges = await collectCallEdges(target, async (node) => { + if (node.name === 'sync_users') + return [{ from: job, ranges: [range(4)] }] + return [] + }, async () => [ + { to: target, ranges: [range(4)] }, + { to: fixture, ranges: [range(6)] }, + { to: side, ranges: [range(8)] }, + ]) + expect(edges).toEqual([ + { caller: job, callee: target, at: range(4) }, + { caller: job, callee: side, at: range(8) }, + ]) + }) + + it('当前块本身在测试文件里:测试函数照样当调用者,往下问到的测试代码也收', async () => { + const target = symbolIn('tests/conftest.py', 'make_user') + const caller = symbolIn('tests/test_sync.py', 'test_sync_runs') + const helper = symbolIn('tests/helpers.py', 'fake_db') + const asked: string[] = [] + const edges = await collectCallEdges(target, async (node) => { + asked.push(node.name) + if (node.name === 'make_user') + return [{ from: caller, ranges: [range(2)] }] + return [] + }, async () => [ + { to: target, ranges: [range(2)] }, + { to: helper, ranges: [range(5)] }, + ]) + expect(asked).toEqual(['make_user', 'test_sync_runs']) + expect(edges).toEqual([ + { caller, callee: target, at: range(2) }, + { caller, callee: helper, at: range(5) }, + ]) + }) + it('取消之后不再问下一个人', async () => { const target = symbol('mask_phone') const caller = symbol('get_profile') diff --git a/vscode/src/codeReader/callTree/walk.ts b/vscode/src/codeReader/callTree/walk.ts index 9a015fe..d09bd0c 100644 --- a/vscode/src/codeReader/callTree/walk.ts +++ b/vscode/src/codeReader/callTree/walk.ts @@ -4,6 +4,7 @@ */ import type { CallEdge, CallRange, CallSymbol } from './model' +import { isTestFile } from '../uncommitted/testFiles' import { CALL_TREE_MAX_DEPTH, symbolKey } from './model' /** 一个调用者,以及它里面每一处调用的范围。 */ @@ -22,6 +23,8 @@ export interface OutgoingHit { * 收集通向 start 的调用边,再加上这些调用者通向别处的边。 * 有环就停。往上最多 8 层。cancelled 变成真就停,已经收到的也不要了。 * start 自己不再往下问:它调用了谁不画在这棵树上。 + * start 不在测试文件里时,测试文件(见 isTestFile)里的符号不算:不当调用者,不再往上问,往下问到的被调用方在测试文件里也不收。 + * start 本身在测试文件里,说明正在读测试代码,就照常全收。 */ export async function collectCallEdges( start: CallSymbol, @@ -36,6 +39,8 @@ export async function collectCallEdges( const ancestors = new Map() const walked = new Map() const startKey = symbolKey(start) + const readingTests = isTestFile(start.path) + const counted = (symbol: CallSymbol) => readingTests || !isTestFile(symbol.path) const add = (caller: CallSymbol, callee: CallSymbol, at: CallRange) => { const edge: CallEdge = { caller, callee, at } @@ -57,7 +62,7 @@ export async function collectCallEdges( const next = new Map() for (const call of calls) { const key = symbolKey(call.from) - if (here.has(key)) + if (here.has(key) || !counted(call.from)) continue for (const at of uniqueRanges(call.ranges)) add(call.from, node, at) @@ -87,6 +92,8 @@ export async function collectCallEdges( if (cancelled()) return [] for (const call of calls) { + if (!counted(call.to)) + continue for (const at of uniqueRanges(call.ranges)) add(ancestor, call.to, at) } diff --git a/vscode/src/codeReader/uncommitted/testFiles.ts b/vscode/src/codeReader/uncommitted/testFiles.ts index 4d8af83..69e7d81 100644 --- a/vscode/src/codeReader/uncommitted/testFiles.ts +++ b/vscode/src/codeReader/uncommitted/testFiles.ts @@ -17,7 +17,7 @@ const TEST_FILE_NAMES: readonly RegExp[] = [ /** * 是不是测试文件:路径上有一层目录是测试目录,或者文件名照测试文件的约定起。 - * 「未提交的更改」和审查隐藏测试文件时按它剔除。 + * 「未提交的更改」和审查隐藏测试文件、调用树不算测试代码,都按它认。 * @param path 相对仓库根,`/` 分隔 */ export function isTestFile(path: string): boolean {