✨ feat(vscode): 代码阅读把字典、对象字面量的值展开成键值树

This commit is contained in:
cruldra committed 2026-09-25 20:57:27 +08:00
1 parent 51f2ef7bfa
commit 4930dc0d42
20 files changed
+578 -103

No files matched your search

@@ -236,6 +236,35 @@ describe('python 属性表信息(facts)', () => {
expect(found[9]).toMatchObject({ name: '_helper', access: 'private' })
expect(found[10]).not.toHaveProperty('access')
})
it('字典字面量的各项:字符串键、名字键、嵌套字典连它的各项、**展开;空字典是空数组,别的值没有;类属性也有', async () => {
const source = [
'_KEYS = {',
' "published": "contests:publish",',
' key: 1,',
' # 注释不算一项',
' "nested": {"a": {}, "b": [1]},',
' **other,',
'}',
'EMPTY = {}',
'ITEMS = [1]',
'class K:',
' labels = {"x": 1}',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private', declarators: [{ name: '_KEYS', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}, "b": [1]}', entries: [
{ kind: 'pair', key: '"a"', value: '{}', entries: [] },
{ kind: 'pair', key: '"b"', value: '[1]' },
] },
{ kind: 'spread', value: 'other' },
] }] })
expect(found[8]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}', entries: [] }] })
expect(found[9]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[11]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}', entries: [{ kind: 'pair', key: '"x"', value: '1' }] }] })
})
})
describe('python 类', () => {
+20 -2
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, ClassFacts, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
@@ -230,16 +230,34 @@ function expressionBlock(node: Node, scope: Scope, docstring: boolean): CodeBloc
const type = expression.childForFieldName('type')
if (scope === 'class' && type)
return leafBlock('field', node)
const value = expression.childForFieldName('right')
return {
kind: scope === 'module' && isConstantTarget(left) ? 'const' : 'variable',
...leafShape(node),
facts: {
access: scopeAccess(left, scope),
declarators: [{ name: left, type: type ?? undefined, value: expression.childForFieldName('right') ?? undefined }],
declarators: [{ name: left, type: type ?? undefined, value: value ?? undefined, entries: dictionaryEntries(value) }],
},
}
}
/** 字典字面量的各项:`键: 值`(值又是字典时连它的各项)、`**other`;值不是字典字面量时没有。 */
function dictionaryEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'dictionary')
return undefined
return syntaxChildren(value).flatMap((item): EntryFacts[] => {
if (item.type === 'dictionary_splat') {
const [spread] = syntaxChildren(item)
return spread ? [{ kind: 'spread', value: spread }] : []
}
const key = item.childForFieldName('key')
const itemValue = item.childForFieldName('value')
if (item.type !== 'pair' || !key || !itemValue)
return []
return [{ kind: 'pair', key, value: itemValue, entries: dictionaryEntries(itemValue) }]
})
}
/** elif / else 展平成紧跟 if 的兄弟块。 */
function ifBlocks(node: Node): CodeBlock[] {
const blocks = [bodyBlock('if', node, bodyOf(node), 'other')]
@@ -325,7 +325,58 @@ describe('typeScript 属性表信息(facts)', () => {
})
expect(env[24]).toEqual({ declarators: [{ name: 'folder', value: 'workspace.workspaceFolders?.find(f => f.uri.fsPath === workspaceRoot)' }] })
expect(env[38]).toMatchObject({ name: 'chmodAll', access: 'private', async: true })
expect(env[40]).toEqual({ declarators: [{ name: 'result', type: 'EnvLockResult', value: '{ total: files.length, ok: [], failed: [] }' }] })
expect(env[40]).toEqual({ declarators: [{
name: 'result',
type: 'EnvLockResult',
value: '{ total: files.length, ok: [], failed: [] }',
entries: [
{ kind: 'pair', key: 'total', value: 'files.length' },
{ kind: 'pair', key: 'ok', value: '[]' },
{ kind: 'pair', key: 'failed', value: '[]' },
],
}] })
})
it('对象字面量的各项:各种键、简写(键和值是同一个节点)、展开、方法(头部到主体之前)、嵌套对象连它的各项;空对象是空数组,别的值没有', async () => {
const source = [
'const o = {',
' a: 1,',
' \'b-c\': x,',
' [k]: 2,',
' s,',
' ...rest,',
' // 注释不算一项',
' async load(id: string): Promise<void> {',
' await run(id)',
' },',
' get size() { return 1 },',
' n: { d: 4, e: {} },',
'}',
'const empty = {}',
'const list = [1]',
].join('\n')
const at = (text: string): number => source.indexOf(text)
const found = await facts(source)
expect(found[1]).toMatchObject({ declarators: [{ entries: [
{ kind: 'pair', key: 'a', value: '1' },
{ kind: 'pair', key: '\'b-c\'', value: 'x' },
{ kind: 'pair', key: '[k]', value: '2' },
{ kind: 'pair', key: 's', value: 's' },
{ kind: 'spread', value: 'rest' },
{
kind: 'method',
name: 'load',
node: 'async load(id: string): Promise<void> {\n await run(id)\n }',
header: { startIndex: at('async load'), endIndex: at(' {\n await'), startRow: 7, endRow: 7 },
},
{ kind: 'method', name: 'size', header: { startIndex: at('get size'), endIndex: at(' { return 1'), startRow: 10, endRow: 10 } },
{ kind: 'pair', key: 'n', value: '{ d: 4, e: {} }', entries: [
{ kind: 'pair', key: 'd', value: '4' },
{ kind: 'pair', key: 'e', value: '{}', entries: [] },
] },
] }] })
expect(found[14]).toEqual({ access: 'private', declarators: [{ name: 'empty', value: '{}', entries: [] }] })
expect(found[15]).toEqual({ access: 'private', declarators: [{ name: 'list', value: '[1]' }] })
})
it('导入:默认导入、* as、仅类型、副作用导入、require', async () => {
+33 -3
View File
@@ -1,7 +1,7 @@
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, extendHeader, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren } from './shared'
const GRAMMARS: Record<string, string> = {
typescript: 'tree-sitter-typescript.wasm',
@@ -205,13 +205,43 @@ function parameterFacts(param: Node): ParameterFacts {
}
function declaratorFacts(declarator: Node): DeclaratorFacts {
const value = declarator.childForFieldName('value')
return {
name: declarator.childForFieldName('name') ?? declarator,
type: annotatedType(declarator.childForFieldName('type')),
value: declarator.childForFieldName('value') ?? undefined,
value: value ?? undefined,
entries: objectEntries(value),
}
}
/** 对象字面量的各项:`键: 值`(值又是对象时连它的各项)、简写 `{ a }`、`...other`、方法;值不是对象字面量时没有。 */
function objectEntries(value: Node | null): EntryFacts[] | undefined {
if (value?.type !== 'object')
return undefined
return syntaxChildren(value).flatMap((member): EntryFacts[] => {
switch (member.type) {
case 'pair': {
const key = member.childForFieldName('key')
const memberValue = member.childForFieldName('value')
return key && memberValue ? [{ kind: 'pair', key, value: memberValue, entries: objectEntries(memberValue) }] : []
}
case 'shorthand_property_identifier':
return [{ kind: 'pair', key: member, value: member }]
case 'spread_element': {
const [spread] = syntaxChildren(member)
return spread ? [{ kind: 'spread', value: spread }] : []
}
case 'method_definition': {
const name = member.childForFieldName('name')
const body = member.childForFieldName('body')
return name ? [{ kind: 'method', name, node: member, header: body ? headerSpan(member, body.startIndex) : nodeSpan(member) }] : []
}
default:
return []
}
})
}
/** import 语句:默认导入、`* as x`、`{ a as b }`、`import x = require('m')`;`import type` 只导入类型。 */
function importFacts(node: Node): ImportFacts {
const require = named(node).find(child => child.type === 'import_require_clause')