✨ feat(vscode): 代码阅读函数、导入、常量、变量块预览改成属性表

This commit is contained in:
cruldra committed 2026-09-25 04:56:22 +08:00
1 parent 0c4c9b0fba
commit dd9b39ac5d
19 files changed
+1458 -213

No files matched your search

+114 -1
View File
@@ -1,6 +1,6 @@
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { factsByLine, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { createPythonAdapter } from './python'
const adapter = createPythonAdapter(WASM_DIR)
@@ -9,6 +9,10 @@ function tree(source: string): Promise<Outline[]> {
return withBlocks(adapter, 'python', source, outline)
}
function facts(source: string): Promise<Record<number, unknown>> {
return withBlocks(adapter, 'python', source, factsByLine)
}
// fixtures/sample.py:行号 1 起
const SAMPLE: Outline[] = [
{ at: 1, kind: 'comment' },
@@ -86,3 +90,112 @@ describe('python 代码块树', () => {
])
})
})
describe('python 属性表信息(facts)', () => {
it('sample.py:import / from import、模块和类里的赋值、函数和方法', async () => {
const sample = await facts(readFixture('sample.py'))
expect(sample[3]).toEqual({ sources: ['os'], items: [{ kind: 'module', module: 'os' }] })
expect(sample[4]).toEqual({ sources: ['pathlib'], items: [{ kind: 'named', name: 'Path', alias: 'P' }] })
expect(sample[6]).toEqual({ access: 'public', declarators: [{ name: 'MAX_DEPTH', value: '8' }] })
expect(sample[10]).toEqual({
name: 'find_env_files',
access: 'public',
async: false,
decorators: [],
parameters: [{ name: 'root', optional: false, type: 'str' }],
returnType: 'list[str]',
})
expect(sample[17]).toEqual({ declarators: [{ name: 'found', value: 'found[:MAX_DEPTH]' }] })
expect(sample[37]).toEqual({ access: 'public', declarators: [{ name: 'label', value: '\'env\'' }] })
expect(sample[39]).toMatchObject({
name: 'summary',
access: 'public',
parameters: [{ name: 'self', optional: false }, { name: 'verbose', optional: false, type: 'bool', default: 'False' }],
returnType: 'str',
})
})
it('routes.py:future 导入、多行括号导入、带装饰器的 async def、await 赋值', async () => {
const routes = await facts(readFixture('routes.py'))
expect(routes[6]).toEqual({ sources: ['__future__'], items: [{ kind: 'named', name: 'annotations' }] })
expect(routes[19]).toEqual({
sources: ['.schemas'],
items: [
{ kind: 'named', name: 'UserAvatarUploadResponse' },
{ kind: 'named', name: 'UserDetailResponse' },
{ kind: 'named', name: 'UserUpdateRequest' },
],
})
expect(routes[29]).toEqual({
name: 'get_user',
access: 'public',
async: true,
decorators: ['@router.get("/{user_id}", response_model=UserDetailResponse)'],
parameters: [
{ name: 'user_id', optional: false, type: 'Int4Path' },
{ name: 'db', optional: false, type: 'AsyncSession', default: 'Depends(get_db)' },
{ name: 'current_user', optional: false, type: 'User', default: 'Depends(get_current_user)' },
],
returnType: 'UserDetailResponse',
})
expect(routes[118]).toEqual({ declarators: [{ name: 'payload', value: 'await file.read()' }] })
})
it('导入:import a.b as c, d 每个模块一个来源;from . import、from m import *', async () => {
const source = [
'import os.path as osp, sys',
'from . import x',
'from m import *',
].join('\n')
expect(await facts(source)).toEqual({
1: { sources: ['os.path', 'sys'], items: [{ kind: 'module', module: 'os.path', alias: 'osp' }, { kind: 'module', module: 'sys' }] },
2: { sources: ['.'], items: [{ kind: 'named', name: 'x' }] },
3: { sources: ['m'], items: [{ kind: 'namespace' }] },
})
})
it('形参:*args / **kwargs 带前缀,* 和 / 分隔符不算参数', async () => {
const found = await facts('async def f(a, /, b: int, *args: int, c=1, d: str = \'x\', **kw) -> None:\n pass')
expect(found[1]).toEqual({
name: 'f',
access: 'public',
async: true,
decorators: [],
parameters: [
{ name: 'a', optional: false },
{ name: 'b', optional: false, type: 'int' },
{ name: '*args', optional: false, type: 'int' },
{ name: 'c', optional: false, default: '1' },
{ name: 'd', optional: false, type: 'str', default: '\'x\'' },
{ name: '**kw', optional: false },
],
returnType: 'None',
})
})
it('访问级别:单下划线开头私有、__dunder__ 公共;解构全是私有名才私有;函数体里的没有', async () => {
const source = [
'_cache = {}',
'__all__ = [\'f\']',
'a, _b = 1, 2',
'_c, _d = 1, 2',
'total: int',
'class K:',
' def __init__(self):',
' self.x = 1',
' def _helper(self):',
' def inner():',
' pass',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private' })
expect(found[2]).toMatchObject({ access: 'public' })
expect(found[3]).toEqual({ access: 'public', declarators: [{ name: 'a, _b', value: '1, 2' }] })
expect(found[4]).toMatchObject({ access: 'private' })
expect(found[5]).toEqual({ access: 'public', declarators: [{ name: 'total', type: 'int' }] })
expect(found[7]).toMatchObject({ name: '__init__', access: 'public' })
expect(found[8]).toEqual({ declarators: [{ name: 'self.x', value: '1' }] })
expect(found[9]).toMatchObject({ name: '_helper', access: 'private' })
expect(found[10]).not.toHaveProperty('access')
})
})
+127 -23
View File
@@ -1,11 +1,13 @@
import type { BlockKind, CodeBlock, LanguageAdapter, Node, TokenRole } from '../contract'
import type { Access, BlockShape, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import * as path from 'node:path'
import { compoundBlock, leafBlock, listBlocks, named, sameNode } from './shared'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
/** 常量、类字段只认直接写在模块 / 类主体里的赋值。 */
/** 常量、类字段只认直接写在模块 / 类主体里的赋值;这两处的声明也才有访问级别。 */
type Scope = 'module' | 'class' | 'other'
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
const PARAMETER_SEPARATORS = new Set(['keyword_separator', 'positional_separator'])
const PATTERNS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern'])
function statements(items: Node[], scope: Scope, afterRow: number, docstring: boolean): CodeBlock[] {
return listBlocks(items, afterRow, (item, first) => statementBlocks(item, scope, docstring && first), () => true)
@@ -20,10 +22,14 @@ function bodyOf(node: Node): Node | null {
return blocks.at(-1) ?? null
}
function bodyBlock(kind: BlockKind, outer: Node, body: Node | null, scope: Scope, docstring = false): CodeBlock {
function bodyShape(outer: Node, body: Node | null, scope: Scope, docstring = false): BlockShape {
if (!body)
return leafBlock(kind, outer)
return compoundBlock(kind, outer, body.startIndex, named(body), (list, afterRow) => statements(list, scope, afterRow, docstring))
return leafShape(outer)
return compoundShape(outer, body.startIndex, named(body), (list, afterRow) => statements(list, scope, afterRow, docstring))
}
function bodyBlock(kind: TextKind, outer: Node, body: Node | null, scope: Scope): CodeBlock {
return { kind, ...bodyShape(outer, body, scope) }
}
function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBlock[] {
@@ -31,13 +37,13 @@ function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBloc
case 'import_statement':
case 'import_from_statement':
case 'future_import_statement':
return [leafBlock('import', node)]
return [{ kind: 'import', ...leafShape(node), facts: importFacts(node) }]
case 'function_definition':
case 'class_definition':
return [definitionBlock(node, node)]
return [definitionBlock(node, node, scope)]
case 'decorated_definition': {
const definition = node.childForFieldName('definition')
return [definition ? definitionBlock(definition, node) : leafBlock('statement', node)]
return [definition ? definitionBlock(definition, node, scope) : leafBlock('statement', node)]
}
case 'if_statement':
return ifBlocks(node)
@@ -55,16 +61,106 @@ function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBloc
case 'return_statement':
return [leafBlock('return', node)]
case 'expression_statement':
return [leafBlock(expressionKind(node, scope, docstring), node)]
return [expressionBlock(node, scope, docstring)]
default:
return [leafBlock('statement', node)]
}
}
/** function_definition / class_definition;outer 是带装饰器的整体时头部从装饰器开始。 */
function definitionBlock(definition: Node, outer: Node): CodeBlock {
const isClass = definition.type === 'class_definition'
return bodyBlock(isClass ? 'class' : 'function', outer, definition.childForFieldName('body'), isClass ? 'class' : 'other', true)
function definitionBlock(definition: Node, outer: Node, scope: Scope): CodeBlock {
if (definition.type === 'class_definition')
return { kind: 'class', ...bodyShape(outer, definition.childForFieldName('body'), 'class', true) }
return {
kind: 'function',
...bodyShape(outer, definition.childForFieldName('body'), 'other', true),
facts: functionFacts(definition, outer, scope),
}
}
/** 单下划线开头的名字是私有(`_x`、`__x`),前后双下划线的特殊方法(`__init__`)是公共。 */
function nameAccess(name: string): Access {
const dunder = name.length > 4 && name.startsWith('__') && name.endsWith('__')
return name.startsWith('_') && !dunder ? 'private' : 'public'
}
/** 赋值目标里的名字;目标是属性、下标(`self.x = …`、`d[k] = …`)时没有。 */
function targetNames(target: Node): Node[] | undefined {
if (target.type === 'identifier')
return [target]
if (!PATTERNS.has(target.type) && target.type !== 'list_splat_pattern')
return undefined
const names: Node[] = []
for (const part of syntaxChildren(target)) {
const found = targetNames(part)
if (!found)
return undefined
names.push(...found)
}
return names
}
/** 模块、类主体里的声明才有访问级别;解构赋值里的名字全是私有才算私有。 */
function scopeAccess(target: Node, scope: Scope): Access | undefined {
const names = scope === 'other' ? undefined : targetNames(target)
if (!names?.length)
return undefined
return names.every(name => nameAccess(name.text) === 'private') ? 'private' : 'public'
}
function functionFacts(definition: Node, outer: Node, scope: Scope): FunctionFacts {
const name = definition.childForFieldName('name')
return {
name: name ?? undefined,
access: name ? scopeAccess(name, scope) : undefined,
async: children(definition).some(child => child.type === 'async'),
decorators: named(outer).filter(child => child.type === 'decorator'),
parameters: syntaxChildren(definition.childForFieldName('parameters'))
.filter(param => !PARAMETER_SEPARATORS.has(param.type))
.map(parameterFacts),
returnType: definition.childForFieldName('return_type') ?? undefined,
}
}
/** 带注解的参数(typed_parameter)语法没给名字起字段名,名字是第一个具名子节点(可能是 `*args`)。 */
function parameterFacts(param: Node): ParameterFacts {
switch (param.type) {
case 'typed_parameter':
return { name: syntaxChildren(param)[0] ?? param, optional: false, type: param.childForFieldName('type') ?? undefined }
case 'default_parameter':
case 'typed_default_parameter':
return {
name: param.childForFieldName('name') ?? param,
optional: false,
type: param.childForFieldName('type') ?? undefined,
default: param.childForFieldName('value') ?? undefined,
}
default:
return { name: param, optional: false }
}
}
/**
* import a.b as c, d:每个模块一个来源,导入项是整个模块;
* from m import a as b / from m import *:来源是 m(可以是相对的 `.models`),future 导入的来源是 `__future__` 这个词本身。
*/
function importFacts(node: Node): ImportFacts {
const names = node.childrenForFieldName('name').filter((name): name is Node => name !== null)
const alias = (name: Node): Node | undefined => name.type === 'aliased_import' ? name.childForFieldName('alias') ?? undefined : undefined
const original = (name: Node): Node => name.type === 'aliased_import' ? name.childForFieldName('name') ?? name : name
if (node.type === 'import_statement') {
return {
sources: names.map(original),
items: names.map(name => ({ kind: 'module', module: original(name), alias: alias(name) })),
}
}
const source = node.type === 'future_import_statement'
? children(node).find(child => child.type === '__future__')
: node.childForFieldName('module_name')
const items: ImportItem[] = names.map(name => ({ kind: 'named', name: original(name), alias: alias(name) }))
if (named(node).some(child => child.type === 'wildcard_import'))
items.push({ kind: 'namespace' })
return { sources: source ? [source] : [], items }
}
function isConstantTarget(target: Node | null): boolean {
@@ -79,17 +175,25 @@ function isConstantTarget(target: Node | null): boolean {
return false
}
function expressionKind(node: Node, scope: Scope, docstring: boolean): BlockKind {
/** 赋值 → 字段 / 常量 / 变量;主体开头的字符串是 docstring;其余表达式是语句。 */
function expressionBlock(node: Node, scope: Scope, docstring: boolean): CodeBlock {
const [expression, ...rest] = named(node)
if (docstring && rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string'))
return 'comment'
if (expression?.type !== 'assignment')
return 'statement'
if (scope === 'class' && expression.childForFieldName('type'))
return 'field'
if (scope === 'module' && isConstantTarget(expression.childForFieldName('left')))
return 'const'
return 'variable'
return leafBlock('comment', node)
const left = expression?.type === 'assignment' ? expression.childForFieldName('left') : null
if (!expression || !left)
return leafBlock('statement', node)
const type = expression.childForFieldName('type')
if (scope === 'class' && type)
return leafBlock('field', node)
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 }],
},
}
}
/** elif / else 展平成紧跟 if 的兄弟块。 */
@@ -111,7 +215,7 @@ function loopBlocks(node: Node): CodeBlock[] {
return blocks
}
const TRY_CLAUSES: Record<string, BlockKind> = {
const TRY_CLAUSES: Record<string, TextKind> = {
except_clause: 'catch',
except_group_clause: 'catch',
else_clause: 'else',
+16 -7
View File
@@ -1,4 +1,4 @@
import type { BlockKind, CodeBlock, Node, SourceSpan } from '../contract'
import type { BlockShape, CodeBlock, Node, SourceSpan, TextKind } from '../contract'
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
@@ -10,6 +10,11 @@ export function children(node: Node): Node[] {
return node.children.filter((child): child is Node => child !== null)
}
/** 具名子节点,去掉注释这类语法之外的节点。 */
export function syntaxChildren(node: Node | null | undefined): Node[] {
return named(node).filter(child => !child.isExtra)
}
export function nodeSpan(node: Node): SourceSpan {
return {
startIndex: node.startIndex,
@@ -19,12 +24,16 @@ export function nodeSpan(node: Node): SourceSpan {
}
}
export function leafBlock(kind: BlockKind, node: Node): CodeBlock {
return { kind, node, header: nodeSpan(node), children: [] }
export function leafShape(node: Node): BlockShape {
return { node, header: nodeSpan(node), children: [] }
}
export function leafBlock(kind: TextKind, node: Node): CodeBlock {
return { kind, ...leafShape(node) }
}
/** 头部往前扩到 from(装饰器)。 */
export function extendHeader(block: CodeBlock, from: Node): CodeBlock {
export function extendHeader<B extends CodeBlock>(block: B, from: Node): B {
return { ...block, header: { ...block.header, startIndex: from.startIndex, startRow: from.startPosition.row } }
}
@@ -67,13 +76,13 @@ function commentsIn(node: Node, from: number, to: number): Node[] {
export type ListBlocks = (items: Node[], afterRow: number) => CodeBlock[]
/**
* 复合语句块:头部到 bodyStart 之前,子块来自 items。
* 复合语句块(不含种类):头部到 bodyStart 之前,子块来自 items。
* 头部和主体之间的注释(例如 Python 冒号后、主体前的注释)也归子块。
*/
export function compoundBlock(kind: BlockKind, outer: Node, bodyStart: number, items: Node[], list: ListBlocks): CodeBlock {
export function compoundShape(outer: Node, bodyStart: number, items: Node[], list: ListBlocks): BlockShape {
const header = headerSpan(outer, bodyStart)
const between = commentsIn(outer, header.endIndex, bodyStart)
return { kind, node: outer, header, children: list([...between, ...items], header.endRow) }
return { node: outer, header, children: list([...between, ...items], header.endRow) }
}
/**
@@ -1,6 +1,6 @@
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { factsByLine, outline, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { createTypeScriptAdapter } from './typescript'
const adapter = createTypeScriptAdapter(WASM_DIR)
@@ -9,6 +9,10 @@ function tree(source: string, languageId = 'typescript'): Promise<Outline[]> {
return withBlocks(adapter, languageId, source, outline)
}
function facts(source: string, languageId = 'typescript'): Promise<Record<number, unknown>> {
return withBlocks(adapter, languageId, source, factsByLine)
}
// 与原型 docs/prototype/code-reader/figma-plugin/code.js 的 CODE_TREE 一致
const ENV_LOCK: Outline[] = [
{ at: [1, 10], kind: 'comment' },
@@ -225,3 +229,119 @@ describe('typeScript 代码块树', () => {
])
})
})
describe('typeScript 属性表信息(facts)', () => {
it('envLock.ts:导入、常量、函数、局部变量', async () => {
const env = await facts(readFixture('envLock.ts.txt'))
expect(env[12]).toEqual({ sources: ['\'node:fs\''], typeOnly: false, items: [{ kind: 'named', name: 'promises', alias: 'fsp' }] })
expect(env[13]).toEqual({ sources: ['\'vscode\''], typeOnly: false, items: [{ kind: 'named', name: 'RelativePattern' }, { kind: 'named', name: 'workspace' }] })
expect(env[15]).toEqual({ access: 'private', declarators: [{ name: 'ENV_PATTERN', value: '\'**/.env*\'' }] })
expect(env[23]).toEqual({
name: 'findEnvFiles',
access: 'public',
async: true,
decorators: [],
parameters: [{ name: 'workspaceRoot', optional: false, type: 'string' }],
returnType: 'Promise<string[]>',
})
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: [] }' }] })
})
it('导入:默认导入、* as、仅类型、副作用导入、require', async () => {
const source = [
'import type { A } from \'x\'',
'import d, { b as c, type E } from \'y\'',
'import * as ns from \'z\'',
'import \'./side\'',
'import fs = require(\'fs\')',
].join('\n')
expect(await facts(source)).toEqual({
1: { sources: ['\'x\''], typeOnly: true, items: [{ kind: 'named', name: 'A' }] },
2: { sources: ['\'y\''], typeOnly: false, items: [
{ kind: 'default', alias: 'd' },
{ kind: 'named', name: 'b', alias: 'c' },
{ kind: 'named', name: 'E' },
] },
3: { sources: ['\'z\''], typeOnly: false, items: [{ kind: 'namespace', alias: 'ns' }] },
4: { sources: ['\'./side\''], typeOnly: false, items: [] },
5: { sources: ['\'fs\''], typeOnly: false, items: [{ kind: 'module', module: '\'fs\'', alias: 'fs' }] },
})
})
it('函数:各种形参、箭头函数、匿名默认导出;嵌套函数没有访问级别', async () => {
const source = [
'export default async function () {}',
'function f(this: Foo, a: string, b?: number, c = 1, ...rest: string[]): Promise<void> {}',
'const g = async (x: number = 2, { y }: O): Promise<string> => {',
' const h = x => {',
' return x',
' }',
'}',
].join('\n')
const found = await facts(source)
expect(found[1]).toEqual({ access: 'public', async: true, decorators: [], parameters: [] })
expect(found[2]).toEqual({
name: 'f',
access: 'private',
async: false,
decorators: [],
parameters: [
{ name: 'this', optional: false, type: 'Foo' },
{ name: 'a', optional: false, type: 'string' },
{ name: 'b', optional: true, type: 'number' },
{ name: 'c', optional: false, default: '1' },
{ name: '...rest', optional: false, type: 'string[]' },
],
returnType: 'Promise<void>',
})
expect(found[3]).toMatchObject({
name: 'g',
access: 'private',
async: true,
parameters: [{ name: 'x', optional: false, type: 'number', default: '2' }, { name: '{ y }', optional: false, type: 'O' }],
returnType: 'Promise<string>',
})
expect(found[4]).toEqual({ name: 'h', async: false, decorators: [], parameters: [{ name: 'x', optional: false }] })
})
it('javaScript 的默认值参数是 assignment_pattern', async () => {
const found = await facts('function f(a, b = 1, ...c) {}', 'javascript')
expect(found[1]).toMatchObject({
parameters: [{ name: 'a', optional: false }, { name: 'b', optional: false, default: '1' }, { name: '...c', optional: false }],
})
})
it('类成员:访问修饰符、#私有名、装饰器、箭头函数字段', async () => {
const source = [
'class K {',
' @dec() @other',
' private async m(a: T): Promise<void> {}',
' #priv() {}',
' protected handler = async (e: E) => {}',
' static s(): x is Foo {}',
'}',
].join('\n')
const found = await facts(source)
expect(found[2]).toMatchObject({ name: 'm', access: 'private', async: true, decorators: ['@dec()', '@other'], returnType: 'Promise<void>' })
expect(found[4]).toMatchObject({ name: '#priv', access: 'private', async: false })
expect(found[5]).toMatchObject({ name: 'handler', access: 'protected', async: true, parameters: [{ name: 'e', optional: false, type: 'E' }] })
expect(found[6]).toMatchObject({ name: 's', access: 'public', returnType: 'x is Foo' })
})
it('变量:解构写整个模式,一条语句多个变量,export 的是公共、顶层其余私有、嵌套的没有', async () => {
const source = [
'export const { p, q } = obj',
'let i = 0, n: number',
'if (ready) {',
' const local = 1',
'}',
].join('\n')
expect(await facts(source)).toEqual({
1: { access: 'public', declarators: [{ name: '{ p, q }', value: 'obj' }] },
2: { access: 'private', declarators: [{ name: 'i', value: '0' }, { name: 'n', type: 'number' }] },
4: { declarators: [{ name: 'local', value: '1' }] },
})
})
})
+146 -36
View File
@@ -1,6 +1,6 @@
import type { BlockKind, CodeBlock, LanguageAdapter, Node, TokenRole } from '../contract'
import type { Access, BlockShape, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import * as path from 'node:path'
import { compoundBlock, extendHeader, leafBlock, listBlocks, named, sameNode } from './shared'
import { children, compoundShape, extendHeader, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
const GRAMMARS: Record<string, string> = {
typescript: 'tree-sitter-typescript.wasm',
@@ -43,17 +43,21 @@ function statements(items: Node[], top: boolean, afterRow: number): CodeBlock[]
}
/** 主体是语句块时子块来自它的语句,否则主体本身就是唯一的子语句(`if (x) return`)。 */
function bodyBlock(kind: BlockKind, outer: Node, body: Node | null, top = false): CodeBlock {
function bodyShape(outer: Node, body: Node | null): BlockShape {
if (!body)
return leafBlock(kind, outer)
return leafShape(outer)
const items = body.type === 'statement_block' ? named(body) : [body]
return compoundBlock(kind, outer, body.startIndex, items, (list, afterRow) => statements(list, top, afterRow))
return compoundShape(outer, body.startIndex, items, (list, afterRow) => statements(list, false, afterRow))
}
function bodyBlock(kind: TextKind, outer: Node, body: Node | null): CodeBlock {
return { kind, ...bodyShape(outer, body) }
}
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
switch (node.type) {
case 'import_statement':
return [leafBlock('import', node)]
return [{ kind: 'import', ...leafShape(node), facts: importFacts(node) }]
case 'export_statement':
return exportBlocks(node, top)
case 'if_statement':
@@ -92,17 +96,17 @@ function exportBlocks(node: Node, top: boolean): CodeBlock[] {
const value = node.childForFieldName('value')
if (value && CLASSES.has(value.type))
return [classBlock(value, node)]
const body = functionBody(value)
return [body ? bodyBlock('function', node, body) : leafBlock('statement', node)]
if (value && functionBody(value))
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
return [leafBlock('statement', node)]
}
function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBlock[] {
switch (declaration.type) {
case 'function_declaration':
case 'generator_function_declaration':
return [bodyBlock('function', outer, declaration.childForFieldName('body'))]
case 'function_signature':
return [leafBlock('function', outer)]
return [functionBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
case 'lexical_declaration':
case 'variable_declaration':
return [variableBlock(declaration, outer, top)]
@@ -123,12 +127,110 @@ function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBl
/** 唯一声明值是带语句块主体的函数 → function;否则模块顶层的 const → const,其余 → variable。 */
function variableBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
const declarators = named(declaration).filter(child => child.type === 'variable_declarator')
const access = declarationAccess(outer, top)
const value = declarators.length === 1 ? declarators[0].childForFieldName('value') : null
const body = functionBody(value)
if (body)
return bodyBlock('function', outer, body)
if (value && functionBody(value))
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
return bodyBlock(top && isConst ? 'const' : 'variable', outer, callbackBody(value))
return {
kind: top && isConst ? 'const' : 'variable',
...bodyShape(outer, callbackBody(value)),
facts: { access, declarators: declarators.map(declaratorFacts) },
}
}
/** 函数块:fn 是声明、方法或函数值,带语句块主体时子块来自主体;name 是它在源码里的名字(变量名、方法名)。 */
function functionBlock(outer: Node, fn: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
return {
kind: 'function',
...bodyShape(outer, fn.childForFieldName('body')),
facts: functionFacts(fn, name, access, decorators),
}
}
/** 模块里的声明:export 了是公共,顶层没 export 是私有,嵌套在语句块里的没有访问级别。 */
function declarationAccess(outer: Node, top: boolean): Access | undefined {
if (outer.type === 'export_statement')
return 'public'
return top ? 'private' : undefined
}
/** 类成员:写了 public / private / protected 按写的,#name 是私有,默认公共。 */
function memberAccess(member: Node): Access {
const modifier = named(member).find(child => child.type === 'accessibility_modifier')
if (modifier)
return modifier.text as Access
return member.childForFieldName('name')?.type === 'private_property_identifier' ? 'private' : 'public'
}
/** 类型标注 `: T` 里的 T(也可能是 `x is T` 这类谓词)。 */
function annotatedType(annotation: Node | null): Node | undefined {
return syntaxChildren(annotation)[0]
}
function functionFacts(fn: Node, name: Node | null, access: Access | undefined, decorators: Node[]): FunctionFacts {
// 不带括号的单参数箭头函数 `x => …`
const single = fn.childForFieldName('parameter')
return {
name: name ?? undefined,
access,
async: children(fn).some(child => child.type === 'async'),
decorators,
parameters: single ? [{ name: single, optional: false }] : syntaxChildren(fn.childForFieldName('parameters')).map(parameterFacts),
returnType: annotatedType(fn.childForFieldName('return_type')),
}
}
/** TypeScript 的形参带类型和默认值;JavaScript 的默认值是 assignment_pattern,其余形参本身就是参数名。 */
function parameterFacts(param: Node): ParameterFacts {
switch (param.type) {
case 'required_parameter':
case 'optional_parameter':
return {
name: param.childForFieldName('pattern') ?? param,
optional: param.type === 'optional_parameter',
type: annotatedType(param.childForFieldName('type')),
default: param.childForFieldName('value') ?? undefined,
}
case 'assignment_pattern':
return { name: param.childForFieldName('left') ?? param, optional: false, default: param.childForFieldName('right') ?? undefined }
default:
return { name: param, optional: false }
}
}
function declaratorFacts(declarator: Node): DeclaratorFacts {
return {
name: declarator.childForFieldName('name') ?? declarator,
type: annotatedType(declarator.childForFieldName('type')),
value: declarator.childForFieldName('value') ?? undefined,
}
}
/** 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')
const source = (require ?? node).childForFieldName('source')
const items: ImportItem[] = []
const requireAlias = named(require).find(child => child.type === 'identifier')
if (source && requireAlias)
items.push({ kind: 'module', module: source, alias: requireAlias })
for (const part of named(named(node).find(child => child.type === 'import_clause'))) {
if (part.type === 'identifier') {
items.push({ kind: 'default', alias: part })
}
else if (part.type === 'namespace_import') {
items.push({ kind: 'namespace', alias: named(part).find(child => child.type === 'identifier') })
}
else if (part.type === 'named_imports') {
for (const specifier of named(part)) {
const name = specifier.type === 'import_specifier' ? specifier.childForFieldName('name') : null
if (name)
items.push({ kind: 'named', name, alias: specifier.childForFieldName('alias') ?? undefined })
}
}
}
return { sources: source ? [source] : [], typeOnly: children(node).some(child => child.type === 'type'), items }
}
/** if 的 else / else if 展平成紧跟其后的兄弟块。 */
@@ -160,7 +262,7 @@ function switchBlock(node: Node): CodeBlock {
const body = node.childForFieldName('body')
if (!body)
return leafBlock('if', node)
return compoundBlock('if', node, body.startIndex, named(body), (list, afterRow) => statements(list, false, afterRow))
return { kind: 'if', ...compoundShape(node, body.startIndex, named(body), (list, afterRow) => statements(list, false, afterRow)) }
}
/** case / default:头部到冒号为止,子块来自它下面的语句。 */
@@ -168,40 +270,45 @@ function caseBlock(node: Node): CodeBlock {
const [first] = node.childrenForFieldName('body')
const bodyStart = first?.startIndex ?? node.endIndex
const items = named(node).filter(child => child.startIndex >= bodyStart)
return compoundBlock('if', node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow))
return { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
}
/** 类:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
function classBlock(declaration: Node, outer: Node): CodeBlock {
const body = declaration.childForFieldName('body')
if (!body)
return leafBlock('class', outer)
return compoundBlock('class', outer, body.startIndex, named(body), (list, afterRow) => {
let decorators: Node | undefined
return listBlocks(list, afterRow, (member) => {
if (member.type === 'decorator') {
decorators ??= member
return []
}
const blocks = memberBlocks(member)
const from = decorators
decorators = undefined
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
}, isLineComment)
})
return {
kind: 'class',
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) => {
let decorators: Node[] = []
return listBlocks(list, afterRow, (member) => {
if (member.type === 'decorator') {
decorators.push(member)
return []
}
const blocks = memberBlocks(member, decorators)
const [from] = decorators
decorators = []
return from ? blocks.map((block, i) => i === 0 ? extendHeader(block, from) : block) : blocks
}, isLineComment)
}),
}
}
function memberBlocks(member: Node): CodeBlock[] {
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
switch (member.type) {
case 'method_definition':
return [bodyBlock('function', member, member.childForFieldName('body'))]
case 'method_signature':
case 'abstract_method_signature':
return [leafBlock('function', member)]
return [functionBlock(member, member, member.childForFieldName('name'), memberAccess(member), decorators)]
case 'class_static_block':
return [bodyBlock('statement', member, member.childForFieldName('body'))]
default: {
const body = functionBody(member.childForFieldName('value'))
return [body ? bodyBlock('function', member, body) : leafBlock('field', member)]
const value = member.childForFieldName('value')
if (value && functionBody(value))
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
return [leafBlock('field', member)]
}
}
}
@@ -211,8 +318,11 @@ function membersBlock(declaration: Node, outer: Node): CodeBlock {
const body = declaration.childForFieldName('body')
if (!body)
return leafBlock('type', outer)
return compoundBlock('type', outer, body.startIndex, named(body), (list, afterRow) =>
listBlocks(list, afterRow, member => [leafBlock('field', member)], isLineComment))
return {
kind: 'type',
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) =>
listBlocks(list, afterRow, member => [leafBlock('field', member)], isLineComment)),
}
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────