✨ feat(vscode): 代码阅读函数、导入、常量、变量块预览改成属性表
This commit is contained in:
1 parent
0c4c9b0fba
commit
dd9b39ac5d
19 files changed
+1458
-213
No files matched your search
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
|
||||
@@ -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' }] },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user