✨ feat(vscode): 代码阅读类块改成属性表并默认折叠,类的说明进「说明」行

This commit is contained in:
cruldra committed 2026-09-25 14:46:33 +08:00
1 parent 3329aca2e1
commit 43323659e1
22 files changed
+580 -121

No files matched your search

+66 -5
View File
@@ -90,7 +90,7 @@ describe('python 代码块树', () => {
})
})
describe('python 函数的 docstring', () => {
describe('python 函数、类的 docstring', () => {
const source = [
'class K:',
' """类说明"""',
@@ -105,10 +105,9 @@ describe('python 函数的 docstring', () => {
'def g(): "单行"',
].join('\n')
it('算进函数头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块;类的 docstring 照旧是注释块', async () => {
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块', async () => {
expect(await tree(source)).toEqual([
{ at: 1, kind: 'class', kids: [
{ at: 2, kind: 'comment' },
{ at: [1, 2], kind: 'class', kids: [
{ at: [3, 8], kind: 'function', kids: [
{ at: 9, kind: 'comment' },
{ at: 10, kind: 'return' },
@@ -118,9 +117,11 @@ describe('python 函数的 docstring', () => {
])
})
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有', async () => {
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有;模块开头的 docstring 照旧是注释块', async () => {
const found = await facts(source)
expect(found[1]).toMatchObject({ name: 'K', doc: '"""类说明"""' })
expect(found[3]).toMatchObject({ name: 'run', doc: '"""跑一遍。\n\n 细节。\n """' })
expect(await tree('"""模块说明"""\nclass K:\n pass')).toEqual([{ at: 1, kind: 'comment' }, { at: 2, kind: 'class', kids: [{ at: 3, kind: 'statement' }] }])
expect(found[11]).toMatchObject({ name: 'g', doc: '"单行"' })
expect(await facts('def f():\n x = 1\n """not a docstring"""')).not.toHaveProperty('1.doc')
})
@@ -236,3 +237,63 @@ describe('python 属性表信息(facts)', () => {
expect(found[10]).not.toHaveProperty('access')
})
})
describe('python 类', () => {
it('classes.py:docstring(连同它前面的注释)算进类头部,类里的类、函数里的类照常成块', async () => {
expect(await tree(readFixture('classes.py'))).toEqual([
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: [6, 10], kind: 'class', kids: [
{ at: 12, kind: 'field' },
{ at: 14, kind: 'function', kids: [{ at: 15, kind: 'variable' }] },
] },
{ at: [18, 20], kind: 'class', kids: [{ at: 21, kind: 'variable' }] },
{ at: [24, 25], kind: 'class', kids: [
{ at: 26, kind: 'class', kids: [{ at: 27, kind: 'variable' }] },
{ at: 29, kind: 'class', kids: [{ at: 30, kind: 'statement' }] },
] },
{ at: 33, kind: 'class', kids: [{ at: 34, kind: 'statement' }] },
{ at: 37, kind: 'function', kids: [
{ at: [38, 39], kind: 'class' },
{ at: 40, kind: 'return' },
] },
])
})
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
const found = await facts(readFixture('classes.py'))
expect(found[6]).toEqual({
name: 'AppException',
doc: '"""应用异常的基类。\n\n 带上 HTTP 状态码。\n """',
access: 'public',
decorators: [],
typeParameters: [],
bases: [{ value: 'Exception' }],
implements: [],
})
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppException' }] })
expect(found[24]).toEqual({
name: 'Meta',
access: 'public',
decorators: ['@dataclass(frozen=True)'],
typeParameters: [],
bases: [{ value: 'Base' }, { value: 'metaclass=ABCMeta' }],
implements: [],
})
expect(found[26]).toMatchObject({ name: 'Config', access: 'public', bases: [] })
expect(found[29]).toMatchObject({ name: '_Hidden', access: 'private' })
expect(found[33]).toMatchObject({ name: '_Private', access: 'private' })
expect(found[38]).toEqual({ name: 'Local', doc: '"""局部类。"""', decorators: [], typeParameters: [], bases: [], implements: [] })
})
it('没有「抽象」这个概念;Python 3.12 的泛型参数进 typeParameters;*bases、**kw 也是父类里的一项', async () => {
expect((await facts('class G[T](Generic[T], *mixins, **kw):\n pass'))[1]).toEqual({
name: 'G',
access: 'public',
decorators: [],
typeParameters: ['T'],
bases: [{ value: 'Generic[T]' }, { value: '*mixins' }, { value: '**kw' }],
implements: [],
})
})
})
+34 -13
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, CodeBlock, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BlockShape, ClassFacts, CodeBlock, 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'
@@ -9,7 +9,7 @@ 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'])
// 只有 # 行注释;函数的说明是主体开头的 docstring,不是写在前面的注释
// 只有 # 行注释;函数、类的说明是主体开头的 docstring,不是写在前面的注释
const COMMENTS: CommentRules = { isLine: () => true, isDoc: () => false }
function statements(items: Node[], scope: Scope, afterRow: number, docstring: boolean): CodeBlock[] {
@@ -73,14 +73,12 @@ function statementBlocks(node: Node, scope: Scope, docstring: boolean): CodeBloc
/** function_definition / class_definition;outer 是带装饰器的整体时头部从装饰器开始。 */
function definitionBlock(definition: Node, outer: Node, scope: Scope): CodeBlock {
const body = definition.childForFieldName('body')
if (definition.type === 'class_definition')
return { kind: 'class', ...bodyShape(outer, body, 'class', true) }
const doc = docstringOf(body)
return {
kind: 'function',
...(body && doc ? documentedShape(outer, body, doc) : bodyShape(outer, body, 'other')),
facts: functionFacts(definition, outer, scope, doc),
}
const inner: Scope = definition.type === 'class_definition' ? 'class' : 'other'
const shape = body && doc ? documentedShape(outer, body, doc, inner) : bodyShape(outer, body, inner)
if (definition.type === 'class_definition')
return { kind: 'class', ...shape, facts: classFacts(definition, outer, scope, doc) }
return { kind: 'function', ...shape, facts: functionFacts(definition, outer, scope, doc) }
}
/** 主体开头的 docstring:第一条语句(注释不算)是单独一个字符串表达式时,返回那个字符串节点。 */
@@ -97,10 +95,10 @@ function docstringIn(statement: Node): Node | undefined {
return rest.length === 0 && (expression?.type === 'string' || expression?.type === 'concatenated_string') ? expression : undefined
}
/** 函数的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
function documentedShape(outer: Node, body: Node, doc: Node): BlockShape {
/** 函数、类的 docstring 是它自己的说明:算进头部(连同它前面的注释),子块从它后面开始。 */
function documentedShape(outer: Node, body: Node, doc: Node, scope: Scope): BlockShape {
const rest = named(body).filter(item => item.startIndex >= doc.endIndex)
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, 'other', afterRow, false))
return compoundShape(outer, doc.endIndex, rest, (list, afterRow) => statements(list, scope, afterRow, false))
}
/** 单下划线开头的名字是私有(`_x`、`__x`),前后双下划线的特殊方法(`__init__`)是公共。 */
@@ -140,7 +138,7 @@ function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node |
doc,
access: name ? scopeAccess(name, scope) : undefined,
async: children(definition).some(child => child.type === 'async'),
decorators: named(outer).filter(child => child.type === 'decorator'),
decorators: decoratorsOf(outer),
parameters: syntaxChildren(definition.childForFieldName('parameters'))
.filter(param => !PARAMETER_SEPARATORS.has(param.type))
.map(parameterFacts),
@@ -148,6 +146,26 @@ function functionFacts(definition: Node, outer: Node, scope: Scope, doc: Node |
}
}
/** 父类是类名后面括号里的参数列表,照写的顺序一项一项取,`metaclass=…` 这类关键字参数也在里面。 */
function classFacts(definition: Node, outer: Node, scope: Scope, doc: Node | undefined): ClassFacts {
const name = definition.childForFieldName('name')
return {
name: name ?? undefined,
doc,
access: name ? scopeAccess(name, scope) : undefined,
decorators: decoratorsOf(outer),
// Python 3.12 的 `class G[T]:`
typeParameters: syntaxChildren(definition.childForFieldName('type_parameters')),
bases: syntaxChildren(definition.childForFieldName('superclasses')).map(value => ({ value })),
implements: [],
}
}
/** 装饰器在 decorated_definition 里,和被装饰的定义是兄弟节点。 */
function decoratorsOf(outer: Node): Node[] {
return named(outer).filter(child => child.type === 'decorator')
}
/** 带注解的参数(typed_parameter)语法没给名字起字段名,名字是第一个具名子节点(可能是 `*args`)。 */
function parameterFacts(param: Node): ParameterFacts {
switch (param.type) {
@@ -301,6 +319,9 @@ function identifierRole(leaf: Node): TokenRole {
return 'function'
if (parent.type === 'class_definition' && sameNode(parent.childForFieldName('name'), leaf))
return 'type'
// 父类:类名后面括号里直接写的名字
if (parent.type === 'argument_list' && parent.parent?.type === 'class_definition')
return 'type'
if (parent.type === 'decorator' || isCallee(leaf))
return 'function'
if (parent.type === 'attribute' && sameNode(parent.childForFieldName('attribute'), leaf) && isCallee(parent))
+6 -6
View File
@@ -1,4 +1,4 @@
import type { BlockShape, CodeBlock, FunctionBlock, Node, SourceSpan, TextKind } from '../contract'
import type { BlockShape, CodeBlock, DocumentedBlock, Node, SourceSpan, TextKind } from '../contract'
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
@@ -89,19 +89,19 @@ export function compoundShape(outer: Node, bodyStart: number, items: Node[], lis
export interface CommentRules {
/** 行注释:相邻几行合成一块。 */
isLine: (comment: Node) => boolean
/** 文档注释(JSDoc):紧贴在函数块前面时是这个函数的说明,见 FunctionFacts.doc。 */
/** 文档注释(JSDoc):紧贴在函数、类块前面时是它的说明,见 FunctionFacts.doc、ClassFacts.doc。 */
isDoc: (comment: Node) => boolean
}
/** 函数块带上说明:说明进 facts.doc,头部往上延到它开头。 */
function documented(block: FunctionBlock, doc: Node): FunctionBlock {
/** 函数、类块带上说明:说明进 facts.doc,头部往上延到它开头。 */
function documented<B extends DocumentedBlock>(block: B, doc: Node): B {
return { ...extendHeader(block, doc), facts: { ...block.facts, doc } }
}
/**
* 一串语句 → 块。注释单独成块,相邻行的行注释合成一块;
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
* 文档注释和下一条语句之间只有空白、下一条语句是函数块时,它是这个函数的说明,不单独成块;
* 文档注释和下一条语句之间只有空白、下一条语句是函数或类块时,它是这个函数、类的说明,不单独成块;
* 中间隔着的装饰器(不出块的语句)不算隔开。
* afterRow 是这串语句之前的最后一行(头部结尾行),模块顶层传 -1;
* toBlocks 的 first 表示它是这串里第一条非注释语句(Python 认 docstring 用)。
@@ -145,7 +145,7 @@ export function listBlocks(
else {
const blocks = toBlocks(item, first)
const [head] = blocks
if (head?.kind === 'function' && doc) {
if ((head?.kind === 'function' || head?.kind === 'class') && doc) {
blocks[0] = documented(head, doc)
doc = undefined
}
@@ -424,3 +424,61 @@ describe('typeScript 属性表信息(facts)', () => {
})
})
})
describe('typeScript 类', () => {
it('classes.ts:紧贴的 JSDoc(隔着装饰器也算)算进类头部;类表达式赋给变量、类字段也是类块;函数里的类照常成块', async () => {
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
{ at: 1, kind: 'import' },
{ at: [3, 9], kind: 'class', kids: [
{ at: 10, kind: 'comment' },
{ at: 11, kind: 'field' },
{ at: 13, kind: 'function' },
{ at: 15, kind: 'class', kids: [{ at: 16, kind: 'function' }] },
] },
{ at: 20, kind: 'class', kids: [{ at: 21, kind: 'field' }] },
{ at: [24, 25], kind: 'class' },
{ at: 27, kind: 'function', kids: [
{ at: 28, kind: 'class' },
{ at: 29, kind: 'return' },
] },
{ at: 32, kind: 'class' },
])
})
it('classes.ts:名称、说明、访问级别、抽象、装饰器、泛型参数、父类(泛型实参归它)、实现的接口', async () => {
const found = await facts(readFixture('classes.ts.txt'))
expect(found[3]).toEqual({
name: 'Repository',
doc: '/**\n * 仓库基类。\n *\n * 子类实现 `load`。\n */',
access: 'public',
abstract: true,
decorators: ['@Injectable()'],
typeParameters: ['K', 'T extends Entity = Entity'],
bases: [{ value: 'Base', typeArguments: '<string>' }],
implements: ['Loader<T>', 'Disposable'],
})
// 类字段的值是类表达式:名称取字段名,访问级别同类成员
expect(found[15]).toEqual({ name: 'Query', access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [{ value: 'QueryBase' }], implements: [] })
expect(found[20]).toEqual({ access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] })
// 赋给变量的类表达式:名称取变量名
expect(found[24]).toMatchObject({ name: 'Mixed', doc: '/** 类表达式 */', access: 'public', bases: [{ value: 'mixin(A, B)' }] })
expect(found[28]).toEqual({ name: 'Local', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] })
expect(found[32]).toMatchObject({ name: 'Plain', access: 'private', decorators: ['@sealed'] })
})
it('装饰器写在 export 后面也算;export default class Foo 是公共;JavaScript 的父类直接跟在 extends 后面', async () => {
const source = [
'export @dec class A {}',
'export default class B extends C {}',
'const D = class {}',
].join('\n')
expect(await facts(source)).toEqual({
1: { name: 'A', access: 'public', abstract: false, decorators: ['@dec'], typeParameters: [], bases: [], implements: [] },
2: { name: 'B', access: 'public', abstract: false, decorators: [], typeParameters: [], bases: [{ value: 'C' }], implements: [] },
3: { name: 'D', access: 'private', abstract: false, decorators: [], typeParameters: [], bases: [], implements: [] },
})
const js = await facts('class A extends mixin(B) {}\nexport default class extends C {}', 'javascript')
expect(js[1]).toMatchObject({ name: 'A', bases: [{ value: 'mixin(B)' }], implements: [] })
expect(js[2]).toMatchObject({ access: 'public', bases: [{ value: 'C' }] })
})
})
+65 -21
View File
@@ -1,4 +1,4 @@
import type { Access, BlockShape, CodeBlock, DeclaratorFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, 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'
@@ -98,7 +98,7 @@ function exportBlocks(node: Node, top: boolean): CodeBlock[] {
return declarationBlocks(declaration, node, top)
const value = node.childForFieldName('value')
if (value && CLASSES.has(value.type))
return [classBlock(value, node)]
return [classBlock(node, value, value.childForFieldName('name'), 'public')]
if (value && functionBody(value))
return [functionBlock(node, value, value.childForFieldName('name'), 'public')]
return [leafBlock('statement', node)]
@@ -120,18 +120,20 @@ function declarationBlocks(declaration: Node, outer: Node, top: boolean): CodeBl
return [leafBlock('type', outer)]
default: {
if (CLASSES.has(declaration.type))
return [classBlock(declaration, outer)]
return [classBlock(outer, declaration, declaration.childForFieldName('name'), declarationAccess(outer, top))]
const body = declaration.childForFieldName('body')
return [body?.type === 'statement_block' ? bodyBlock('statement', outer, body) : leafBlock('statement', outer)]
}
}
}
/** 唯一声明值是带语句块主体的函数 → function;否则模块顶层的 const → const,其余 → variable。 */
/** 唯一声明值是带语句块主体的函数 → function,是类表达式 → class;否则模块顶层的 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
if (value && CLASSES.has(value.type))
return classBlock(outer, value, declarators[0].childForFieldName('name'), access)
if (value && functionBody(value))
return functionBlock(outer, value, declarators[0].childForFieldName('name'), access)
const isConst = declaration.type === 'lexical_declaration' && declaration.childForFieldName('kind')?.type === 'const'
@@ -276,29 +278,68 @@ function caseBlock(node: Node): CodeBlock {
return { kind: 'if', ...compoundShape(node, bodyStart, items, (list, afterRow) => statements(list, false, afterRow)) }
}
/** 类:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
function classBlock(declaration: Node, outer: Node): CodeBlock {
/**
* 类块:declaration 是类声明或类表达式,name 是它在源码里的名字(类名;赋给变量、类字段的类表达式取变量名、字段名),
* decorators 是类主体里排在它前面的装饰器(类字段的值是类表达式时)。
*/
function classBlock(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[] = []): CodeBlock {
const body = declaration.childForFieldName('body')
const facts = classFacts(outer, declaration, name, access, decorators)
if (!body)
return leafBlock('class', outer)
return { kind: 'class', ...leafShape(outer), facts }
return { kind: 'class', ...compoundShape(outer, body.startIndex, named(body), classMembers), facts }
}
/** 类成员:成员前面的装饰器是类主体里的兄弟节点,算进成员头部,也交给成员当装饰器。 */
function classMembers(list: Node[], afterRow: number): CodeBlock[] {
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
}, COMMENTS)
}
/**
* 类的装饰器写在 export 前面时挂在 export 语句上,写在 class 前面时挂在类声明上;
* 父类、实现的接口在 class_heritage 里:TypeScript 分 extends_clause / implements_clause,JavaScript 直接是 extends 后面的表达式。
*/
function classFacts(outer: Node, declaration: Node, name: Node | null, access: Access | undefined, decorators: Node[]): ClassFacts {
const holders = sameNode(outer, declaration) ? [declaration] : [outer, declaration]
const heritage = syntaxChildren(named(declaration).find(child => child.type === 'class_heritage'))
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
}, COMMENTS)
name: name ?? undefined,
access,
abstract: declaration.type === 'abstract_class_declaration',
decorators: [...decorators, ...holders.flatMap(holder => named(holder).filter(child => child.type === 'decorator'))],
typeParameters: syntaxChildren(declaration.childForFieldName('type_parameters')),
bases: heritage.flatMap((part) => {
if (part.type === 'extends_clause')
return extendsBases(part)
return part.type === 'implements_clause' ? [] : [{ value: part }]
}),
implements: heritage.filter(part => part.type === 'implements_clause').flatMap(syntaxChildren),
}
}
/** extends 后面的一串:每个表达式是一项,跟在它后面的泛型实参 `<T>` 归它。 */
function extendsBases(clause: Node): BaseFacts[] {
const bases: BaseFacts[] = []
for (const part of syntaxChildren(clause)) {
const last = bases.at(-1)
if (part.type === 'type_arguments' && last)
last.typeArguments = part
else
bases.push({ value: part })
}
return bases
}
function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
switch (member.type) {
case 'method_definition':
@@ -309,6 +350,8 @@ function memberBlocks(member: Node, decorators: Node[]): CodeBlock[] {
return [bodyBlock('statement', member, member.childForFieldName('body'))]
default: {
const value = member.childForFieldName('value')
if (value && CLASSES.has(value.type))
return [classBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
if (value && functionBody(value))
return [functionBlock(member, value, member.childForFieldName('name'), memberAccess(member), decorators)]
return [leafBlock('field', member)]
@@ -385,7 +428,8 @@ function nameRole(leaf: Node): TokenRole {
return 'function'
if (parent.type === 'member_expression' && sameNode(parent.childForFieldName('property'), leaf) && isCallee(parent))
return 'function'
if (parent.type === 'new_expression')
// 父类 `extends Base`:TypeScript 在 extends_clause 里,JavaScript 直接在 class_heritage 里
if (parent.type === 'new_expression' || parent.type === 'extends_clause' || parent.type === 'class_heritage')
return 'type'
if (parent.type === 'variable_declarator' && sameNode(parent.childForFieldName('name'), leaf) && functionBody(parent.childForFieldName('value')))
return 'function'