✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新

This commit is contained in:
cruldra committed 2026-09-27 22:08:16 +08:00
1 parent ebf228cdc7
commit ecc415d763
30 files changed
+1033 -157

No files matched your search

+26 -1
View File
@@ -1,7 +1,7 @@
import type { IterationFacts, Node } from '../contract'
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { factsByLine, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
import { factsByLine, nodeFacts, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
import { createPythonAdapter } from './python'
const adapter = createPythonAdapter(WASM_DIR)
@@ -488,3 +488,28 @@ describe('python 写在表达式里的遍历(推导式)', () => {
expect(await iterations('x = [1, 2]\ny = {"a": 1}\nz = (a, b)\nfor x in y:\n pass')).toEqual([])
})
})
describe('python 调用', () => {
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.call?.(node))
it('实参照源码顺序:按位置、按名字、*x、**x;新建对象和调用写法一样,不给 construct;点分名字取最后一节', async () => {
const source = 'user_service.update_user(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)'
expect(await calls(source)).toEqual([{
callee: 'user_service.update_user',
name: 'update_user',
argumentList: '(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)',
arguments: [
{ kind: 'positional', value: 'db' },
{ kind: 'keyword', name: 'user_id', value: 'user_id' },
{ kind: 'spread', value: 'rest' },
{ kind: 'keywordSpread', value: 'options' },
],
}])
})
it('唯一的实参是生成器表达式时就是它;被调用的不是名字时没有 name;装饰器、父类列表不是调用', async () => {
expect(await calls('sum(x for x in xs)')).toEqual([{ callee: 'sum', name: 'sum', argumentList: '(x for x in xs)', arguments: [{ kind: 'positional', value: '(x for x in xs)' }] }])
expect(await calls('handlers[k]()')).toEqual([{ callee: 'handlers[k]', argumentList: '()', arguments: [] }])
expect(await calls('@cache\nclass A(Base):\n pass')).toEqual([])
})
})
+38 -1
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { pythonNameReader } from './pythonNames'
@@ -363,6 +363,42 @@ function pythonIteration(node: Node): IterationFacts | undefined {
}
/** elif / else 展平成紧跟 if 的兄弟块。 */
/** 调用的实参:name=value 按名字传,*x、**x 展开,别的按位置传;唯一的实参是生成器表达式(f(x for x in y))时就是它。 */
function callArgument(arg: Node): CallArgument | undefined {
switch (arg.type) {
case 'keyword_argument': {
const name = arg.childForFieldName('name')
const value = arg.childForFieldName('value')
return name && value ? { kind: 'keyword', name, value } : undefined
}
case 'list_splat':
case 'dictionary_splat': {
const [value] = syntaxChildren(arg)
if (!value)
return undefined
return { kind: arg.type === 'list_splat' ? 'spread' : 'keywordSpread', value }
}
default:
return { kind: 'positional', value: arg }
}
}
/** 调用的结构:新建对象和调用函数写法一样,不给 construct。被调用的是名字或点分名字时 name 是最后那个名字。 */
function pythonCall(node: Node): CallFacts | undefined {
const callee = node.type === 'call' ? node.childForFieldName('function') : null
const list = node.childForFieldName('arguments')
if (!callee || !list)
return undefined
const name = callee.type === 'identifier' ? callee : callee.type === 'attribute' ? callee.childForFieldName('attribute') : null
const args = list.type === 'generator_expression' ? [list] : syntaxChildren(list)
return {
callee,
...(name && { name }),
argumentList: list,
arguments: args.map(callArgument).filter(arg => arg !== undefined),
}
}
function ifBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('if', node, bodyOf(node), 'other')]
for (const alternative of node.childrenForFieldName('alternative')) {
@@ -603,6 +639,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
toBlocks: root => withExceptions(statements(named(root), 'module', -1), PYTHON_EXCEPTIONS),
tokenRole: pythonTokenRole,
iteration: pythonIteration,
call: pythonCall,
displayNames: pythonNameReader,
}
}
@@ -1,6 +1,6 @@
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { factsByLine, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { factsByLine, nodeFacts, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { createTypeScriptAdapter } from './typescript'
const adapter = createTypeScriptAdapter(WASM_DIR)
@@ -612,3 +612,23 @@ describe('typeScript 枚举', () => {
expect(byLine[1]).toMatchObject({ members: [{ name: 'A', docs: [] }, { name: 'B', docs: ['/** 第二 */'] }, { name: 'C', docs: ['// 第三'] }] })
})
})
describe('typeScript 调用', () => {
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.call?.(node))
it('new X(…) 是新建,被调用的不含 new;别的调用不是新建;...x 是展开;点分名字取最后一节', async () => {
expect(await calls('new vscode.Uri(a, ...rest)')).toEqual([{
callee: 'vscode.Uri',
name: 'Uri',
construct: true,
argumentList: '(a, ...rest)',
arguments: [{ kind: 'positional', value: 'a' }, { kind: 'spread', value: 'rest' }],
}])
expect(await calls('run<T>({ a: 1 })')).toEqual([{ callee: 'run', name: 'run', construct: false, argumentList: '({ a: 1 })', arguments: [{ kind: 'positional', value: '{ a: 1 }' }] }])
})
it('带标签的模板字符串、不带括号的 new X 不是调用', async () => {
expect(await calls('sql`select 1`')).toEqual([])
expect(await calls('new Date')).toEqual([])
})
})
+22 -1
View File
@@ -1,4 +1,4 @@
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
@@ -542,6 +542,26 @@ const LITERAL_KEYWORDS = new Set(['true', 'false', 'null', 'undefined', 'this',
const STRING_NODES = new Set(['string', 'template_string', 'regex'])
const FUNCTION_NAMED = new Set(['function_declaration', 'generator_function_declaration', 'function_expression', 'function', 'generator_function', 'method_definition', 'method_signature', 'abstract_method_signature', 'function_signature'])
/** 调用的结构:`new X(…)` 是新建对象,别的调用不是;带标签的模板字符串(sql`…`)、不带括号的 `new X` 不算。 */
function typescriptCall(node: Node): CallFacts | undefined {
const construct = node.type === 'new_expression'
const callee = construct ? node.childForFieldName('constructor') : node.type === 'call_expression' ? node.childForFieldName('function') : null
const list = node.childForFieldName('arguments')
if (!callee || list?.type !== 'arguments')
return undefined
const name = callee.type === 'identifier' ? callee : callee.type === 'member_expression' ? callee.childForFieldName('property') : null
return {
callee,
...(name && { name }),
construct,
argumentList: list,
arguments: syntaxChildren(list).map((arg): CallArgument => {
const spread = arg.type === 'spread_element' ? syntaxChildren(arg)[0] : undefined
return spread ? { kind: 'spread', value: spread } : { kind: 'positional', value: arg }
}),
}
}
function insideString(leaf: Node): boolean {
for (let node = leaf.parent; node; node = node.parent) {
if (node.type === 'template_substitution')
@@ -620,6 +640,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
},
toBlocks: (root, _source, languageId) => withExceptions(blockReader(TYPED_LANGUAGES.has(languageId))(root), TYPESCRIPT_EXCEPTIONS),
tokenRole: typescriptTokenRole,
call: typescriptCall,
displayNames: typescriptNameReader,
}
}