✨ feat(vscode): 代码阅读的调用与构造:跨多行写的调用排成「新建 / 调用 名字 · N 个参数」一行,实参摊成参数 / 值表;Python 查被调用名字的定义分新建和调用,查到后刷新
This commit is contained in:
1 parent
ebf228cdc7
commit
ecc415d763
30 files changed
+1033
-157
No files matched your search
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user