✨ feat(vscode): 代码阅读的 TypeScript interface 换成一张属性表卡片

契约加 InterfaceBlock / InterfaceFacts(种类「接口」,图标、颜色同类型块);属性签名和索引签名连同讲它的注释并进「属性 | 类型 | 说明」表,可省略带 ?、只读写在类型前、没标注写未标注,每行讲的定义是属性名;方法签名是函数子块,调用签名、构造签名照旧是子块;declare interface 也认;「了解一块代码」照旧按 接口名.属性名 找得到属性
This commit is contained in:
cruldra committed 2026-10-16 14:27:28 +11:00
1 parent f487b841ce
commit 6b1e6f3487
16 files changed
+334 -46

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ function factAttributes(block: CodeBlock): Partial<Record<CommonAttribute, strin
case 'impl':
case 'module':
return { access: block.facts.access }
case 'interface':
case 'enum':
case 'exception':
case 'const':
+5 -5
View File
@@ -42,7 +42,7 @@ const SAMPLES: [LanguageAdapter, string, Translator, string][] = [
[py, 'python', pythonZhTranslator, 'attributeDocs.py'],
]
const PROPERTY_KINDS = new Set(['function', 'class', 'import', 'const', 'variable'])
const PROPERTY_KINDS = new Set(['function', 'class', 'interface', 'import', 'const', 'variable'])
function translationText(view: BlockView): string {
if (!('translation' in view))
@@ -84,7 +84,7 @@ describe('blockView', () => {
}
})
it.each(SAMPLES)('%#: 函数、类、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
it.each(SAMPLES)('%#: 函数、类、接口、导入、常量、变量块只带属性表,其余块只带一行译文', async (adapter, languageId, translator, fixture) => {
for (const view of flatten(await views(adapter, languageId, translator, readFixture(fixture)))) {
expect('properties' in view, view.id).toBe(PROPERTY_KINDS.has(view.kind))
expect('translation' in view, view.id).toBe(!PROPERTY_KINDS.has(view.kind))
@@ -319,13 +319,13 @@ describe('blockView', () => {
expect(signature.filter(token => token.text === 'str')).toEqual([{ text: 'str', role: 'type' }, { text: 'str', role: 'type' }, { text: 'str', role: 'function' }])
})
it('块自己的名字(defines):函数名、类名(赋给变量的类表达式取变量名)、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块、匿名类没有', async () => {
it('块自己的名字(defines):函数名、类名(赋给变量的类表达式取变量名)、接口名、只声明一个变量时的变量名(解构模式整个算);导入、一条语句几个变量、一行译文的块、匿名类没有', async () => {
const own = (source: string, list: BlockView[]): Record<string, string | undefined> =>
Object.fromEntries(list.map(view => [view.id, view.defines && source.slice(view.defines.startIndex, view.defines.endIndex)]))
const pySource = ['import os', 'def f(x): pass', 'a, b = 1, 2', 'for i in x: pass', 'class K: pass'].join('\n')
expect(own(pySource, await views(py, 'python', pythonZhTranslator, pySource))).toEqual({ 0: undefined, 1: 'f', 2: 'a, b', 3: undefined, 4: 'K' })
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1', 'class A {}', 'const B = class C {}', 'export default class {}'].join('\n')
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h', 3: 'A', 4: 'B', 5: undefined })
const tsSource = ['let i = 0, j', 'const { u, v } = o', 'export const h = () => 1', 'class A {}', 'const B = class C {}', 'export default class {}', 'interface I { x: number }'].join('\n')
expect(own(tsSource, await views(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({ 0: undefined, 1: '{ u, v }', 2: 'h', 3: 'A', 4: 'B', 5: undefined, 6: 'I' })
})
it('\\r\\n 和制表符缩进:块的源码用 \\n 连接,去掉共同的制表符', async () => {
+1
View File
@@ -183,6 +183,7 @@ export function ownName(block: CodeBlock): Node | undefined {
switch (block.kind) {
case 'function':
case 'class':
case 'interface':
case 'enum':
case 'exception':
case 'module':
+1
View File
@@ -197,6 +197,7 @@ function kindName(kind: SymbolKind): string {
case SymbolKind.Constructor:
return 'class'
case SymbolKind.Interface:
return 'interface'
case SymbolKind.TypeParameter:
return 'type'
case SymbolKind.Enum:
+42 -4
View File
@@ -10,9 +10,10 @@ export type { Node }
* run 构建时执行的命令,copy 往镜像里复制文件,setting 一条设置(工作目录、运行身份、端口……),label 镜像的标签,
* health 健康检查,start 容器启动时执行的命令。
* comment 是讲整个文件的注释(文件开头的说明);别的注释不单独成块,挂在它讲的块上(见 BlockShape.comments)。
* interface 是 TypeScript 的接口(属性表),type 是别的类型声明(`type` 别名这类,一行译文)。
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
= | 'import' | 'function' | 'class' | 'interface' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module' | 'attribute'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'
@@ -275,6 +276,40 @@ export interface EnumFacts {
members: EnumMemberFacts[]
}
/** 接口的一个属性:属性签名 `名字?: 类型`,索引签名 `[key: string]: 类型` 也算一个;节点都指向源码。 */
export interface InterfacePropertyFacts {
/** 属性名;索引签名是方括号里的参数名(`[key: string]` 的 `key`)。 */
name: Node
/** 索引签名方括号里参数的类型(`[key: string]` 的 `string`);普通属性没有。 */
index?: Node
/** 属性的类型;没标注时没有。 */
type?: Node
/** 可省略的属性 `x?`。 */
optional: boolean
/** 只读属性 `readonly x`。 */
readonly: boolean
/** 属性的说明文字节点,同 [EnumMemberFacts.docs]:从上一个成员之后起单独成行的注释 / JSDoc、写在它这一行行尾的注释。 */
docs: Node[]
}
/**
* 接口:TypeScript 的 `interface`(含 `declare interface`)由语言插件报。属性(含索引签名)连同讲它的注释并进属性表,
* 方法签名、调用签名、构造签名这些别的成员照旧是子块。
*/
export interface InterfaceFacts {
name: Node
/** 接口自己的说明,同 [ClassFacts.doc]:紧贴在前面的 JSDoc。 */
doc?: Node
/** 同 [ClassFacts.access]。 */
access?: Access
/** 泛型参数,同 [ClassFacts.typeParameters]。 */
typeParameters: Node[]
/** 继承的接口(`extends A, B<T>`),每项一个节点,泛型实参连在里面。 */
extends: Node[]
/** 照源码顺序。 */
properties: InterfacePropertyFacts[]
}
/** 异常类的一个类属性:Python 类主体里给一个名字赋值的语句(带不带注解、有没有值),TypeScript 的类字段(含 static);节点都指向源码。 */
export interface FieldFacts {
name: Node
@@ -551,7 +586,7 @@ export interface VariableFacts {
* - kind:块的种类(BlockKind);
* - access:访问级别,'public' | 'private' | 'protected',语言可以加自己的值(Kotlin 的 'internal',见 BlockShape.attributes);
* - async、abstract、typeOnly:异步、抽象、仅类型,值是 'true' | 'false'。
* 宿主从块的 kind 和 facts 推出它们(FunctionFacts.access / async、ClassFacts.access / abstract、EnumFacts.access、ExceptionFacts.access、ImportFacts.typeOnly、VariableFacts.access),
* 宿主从块的 kind 和 facts 推出它们([FunctionFacts.access] / [FunctionFacts.async]、[ClassFacts.access] / [ClassFacts.abstract]、[InterfaceFacts.access]、[EnumFacts.access]、[ExceptionFacts.access]、[ImportFacts.typeOnly]、[VariableFacts.access]),
* 语言插件不用另报;facts 里没填的(语言没有这个概念、块没有这一项),块就没有这个属性,也不受这个属性的筛选影响。
*/
export const COMMON_ATTRIBUTES = ['kind', 'access', 'async', 'abstract', 'typeOnly'] as const
@@ -593,6 +628,8 @@ export interface ImplBlock extends BlockShape { kind: 'impl', facts: ClassFacts
export interface ModuleBlock extends BlockShape { kind: 'module', facts: ClassFacts }
/** Rust 的内部属性 `#![…]`:写给所在 crate 或模块的编译属性。 */
export interface AttributeBlock extends BlockShape { kind: 'attribute', facts: AttributeFacts }
/** 接口块的头部是整个接口(属性都在属性表里讲),方法签名等别的成员是子块。 */
export interface InterfaceBlock extends BlockShape { kind: 'interface', facts: InterfaceFacts }
/** 枚举块的头部是整个枚举(成员都在属性表里讲),没有子块;库规则认出来的枚举(Python)是整个类,留下的方法等是子块。 */
export interface EnumBlock extends BlockShape { kind: 'enum', facts: EnumFacts }
/** 异常块的头部是整个类(类属性都在属性表里讲),留下的方法(含构造函数)等是子块。 */
@@ -610,11 +647,11 @@ export interface StageBlock extends BlockShape { kind: 'stage', facts: StageFact
export interface HealthBlock extends BlockShape { kind: 'health', facts: HealthFacts }
export interface StartBlock extends BlockShape { kind: 'start', facts: StartFacts }
export type FactBlock
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | AttributeBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
= | FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | AttributeBlock | InterfaceBlock | EnumBlock | ExceptionBlock | ImportBlock | VariableBlock | InterpreterBlock | OptionsBlock
| DirectiveBlock | StageBlock | HealthBlock | StartBlock
/** 带说明(facts.doc)的块。 */
export type DocumentedBlock = FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | EnumBlock | ExceptionBlock | VariableBlock
export type DocumentedBlock = FunctionBlock | ClassBlock | TraitBlock | ImplBlock | ModuleBlock | InterfaceBlock | EnumBlock | ExceptionBlock | VariableBlock
/** 属性表块的种类和 facts,不带块的形状(节点、头部、子块):库规则把一块认成现成的一种属性表块时给它(RuleFacts)。 */
export type BlockFacts = FactBlock extends infer B ? B extends FactBlock ? Pick<B, 'kind' | 'facts'> : never : never
@@ -1068,6 +1105,7 @@ export const KIND_ICONS: Record<BlockKind, KindIconName> = {
module: 'layers',
attribute: 'sliders-horizontal',
type: 'braces',
interface: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
field: 'tag',
@@ -91,7 +91,7 @@ describe('按符号名在源码里找(语言插件解析出的块)', () => {
it('类型(interface、type)和它们的成员按声明的名字找;shell 命令不算名字', async () => {
const types = 'export interface Favorite {\n uri: string\n}\ntype Slot = number\n'
expect(await find(typescript, 'typescript', types, 'Favorite')).toEqual({ text: 'Favorite', kind: 'type' })
expect(await find(typescript, 'typescript', types, 'Favorite')).toEqual({ text: 'Favorite', kind: 'interface' })
expect(await find(typescript, 'typescript', types, 'Favorite.uri')).toEqual({ text: 'uri', kind: 'field' })
expect(await find(typescript, 'typescript', types, 'Slot')).toEqual({ text: 'Slot', kind: 'type' })
expect(await find(rust, 'rust', 'pub type Id = u32;\n', 'Id')).toEqual({ text: 'Id', kind: 'statement' })
@@ -16,7 +16,7 @@ export interface NamedBlock {
children: NamedBlock[]
}
/** 语言插件不给名字、按语法树里的声明找名字的块:类型(TypeScript 的 interface、type)、类型里的成员、别的声明语句(Rust 的 type)。 */
/** 语言插件不给名字、按语法树里的声明找名字的块:类型(TypeScript 的 `type` 别名)、类字段、接口里没并进属性表的成员、别的声明语句(Rust 的 `type`)。 */
const DECLARATIONS = new Set<BlockKind>(['type', 'field', 'statement'])
/**
@@ -32,10 +32,17 @@ function nameOf(block: CodeBlock): Node | undefined {
return declaration.childForFieldName('name') ?? undefined
}
/** 接口的属性并进了属性表、不是子块,照样当它下面的字段找,`Favorite.uri` 才找得到。 */
function propertyNames(block: CodeBlock): NamedBlock[] {
if (block.kind !== 'interface')
return []
return block.facts.properties.map(({ name }) => ({ name: name.text, kind: 'field', range: { startIndex: name.startIndex, endIndex: name.endIndex }, children: [] }))
}
/** 块树里有名字的那些;没名字的块(条件、调用语句)跳过,它里面有名字的往上提一层。 */
export function namedBlocks(blocks: readonly CodeBlock[]): NamedBlock[] {
return blocks.flatMap((block): NamedBlock[] => {
const children = namedBlocks(block.children)
const children = [...namedBlocks(block.children), ...propertyNames(block)].sort((a, b) => a.range.startIndex - b.range.startIndex)
const name = nameOf(block)
return name ? [{ name: name.text, kind: block.kind, range: { startIndex: name.startIndex, endIndex: name.endIndex }, children }] : children
})
@@ -51,6 +51,7 @@ const SYMBOL_ICONS: Record<string, string> = {
exception: 'symbol-class',
impl: 'symbol-class',
trait: 'symbol-interface',
interface: 'symbol-interface',
enum: 'symbol-enum',
module: 'symbol-module',
const: 'symbol-constant',
@@ -394,6 +394,7 @@ function ownDoc(block: CodeBlock): Node | undefined {
case 'trait':
case 'impl':
case 'module':
case 'interface':
case 'enum':
case 'exception':
case 'const':
@@ -13,7 +13,7 @@ function facts(source: string, languageId = 'typescript'): Promise<Record<number
return withBlocks(adapter, languageId, source, factsByLine)
}
// 与原型 docs/prototype/code-reader/figma-plugin/code.js 的 CODE_TREE 一致,只是紧贴函数的 JSDoc 算进了函数头部
// 与原型 docs/prototype/code-reader/figma-plugin/code.js 的 CODE_TREE 一致,只是紧贴函数的 JSDoc 算进了函数头部,interface 是一整块(属性在属性表里讲)
const ENV_LOCK: Outline[] = [
{ at: [1, 10], kind: 'comment' },
{ at: 12, kind: 'import' },
@@ -29,11 +29,7 @@ const ENV_LOCK: Outline[] = [
{ at: 28, kind: 'variable' },
{ at: 29, kind: 'return' },
] },
{ at: 32, kind: 'type', kids: [
{ at: 33, kind: 'field' },
{ at: 34, kind: 'field' },
{ at: 35, kind: 'field' },
] },
{ at: [32, 36], kind: 'interface' },
{ at: 38, kind: 'function', kids: [
{ at: 39, kind: 'variable' },
{ at: 40, kind: 'variable' },
@@ -58,11 +54,7 @@ const ENV_LOCK: Outline[] = [
const ISSUE_LOCK: Outline[] = [
{ at: 1, kind: 'import' },
{ at: 3, kind: 'type', kids: [
{ at: 4, kind: 'field' },
{ at: 5, kind: 'field' },
{ at: 6, kind: 'field' },
] },
{ at: [3, 7], kind: 'interface' },
{ at: [9, 13], kind: 'function', kids: [
{ at: 14, kind: 'variable' },
{ at: 15, kind: 'if', kids: [
@@ -629,6 +621,73 @@ describe('typeScript 枚举', () => {
})
})
describe('typeScript 接口', () => {
const SESSION = [
'/** 会话。 */',
'export interface Session<T extends object = object> extends Base, Store<T> {',
' /** 编号 */',
' readonly id: string',
' // 标题',
' title?: string // 可以不填',
' [key: string]: unknown',
' readonly [index: number]: T',
' /** 关掉会话。 */',
' close(reason?: string): void',
' // 当函数调',
' (input: string): number',
' new (name: string): Session<T>',
' count',
'}',
'',
'interface Empty {}',
'declare interface Ambient { a: number }',
'function local() {',
' interface Inner { x: number }',
'}',
].join('\n')
it('interface 是接口块:头部是整个 interface,紧贴的 JSDoc 算进头部;属性(含索引签名)连同讲它的注释并进属性表,方法签名是函数子块,调用签名、构造签名照旧是子块', async () => {
expect(await tree(SESSION)).toEqual([
{ at: [1, 15], kind: 'interface', kids: [
{ at: [9, 10], kind: 'function' },
{ at: 12, kind: 'field', notes: [11] },
{ at: 13, kind: 'field' },
] },
{ at: 17, kind: 'interface' },
{ at: 18, kind: 'interface' },
{ at: 19, kind: 'function', kids: [{ at: 20, kind: 'interface' }] },
])
})
it('属性表信息:名称、说明、访问级别、泛型参数、extends 每项一个节点;属性照源码顺序,带可省略、只读、索引签名的参数类型,说明收法同枚举成员', async () => {
const byLine = await facts(SESSION)
expect(byLine[1]).toEqual({
name: 'Session',
doc: '/** 会话。 */',
access: 'public',
typeParameters: ['T extends object = object'],
extends: ['Base', 'Store<T>'],
properties: [
{ name: 'id', type: 'string', optional: false, readonly: true, docs: ['/** 编号 */'] },
{ name: 'title', type: 'string', optional: true, readonly: false, docs: ['// 标题', '// 可以不填'] },
{ name: 'key', index: 'string', type: 'unknown', optional: false, readonly: false, docs: [] },
{ name: 'index', index: 'number', type: 'T', optional: false, readonly: true, docs: [] },
{ name: 'count', optional: false, readonly: false, docs: [] },
],
})
expect(byLine[9]).toMatchObject({ name: 'close', doc: '/** 关掉会话。 */', parameters: [{ name: 'reason', optional: true, type: 'string' }], returnType: 'void' })
expect(byLine[17]).toEqual({ name: 'Empty', access: 'private', typeParameters: [], extends: [], properties: [] })
expect(byLine[18]).toMatchObject({ name: 'Ambient', access: 'private', properties: [{ name: 'a', type: 'number' }] })
expect(byLine[20]).toEqual({ name: 'Inner', typeParameters: [], extends: [], properties: [{ name: 'x', type: 'number', optional: false, readonly: false, docs: [] }] })
})
it('jSDoc 挂在同一行上一个属性后面时讲的是下一个属性;最后一个成员后面的注释挂到接口上', async () => {
const source = 'interface E { a: string; /** 第二 */ b: string; c: number // 第三\n // 收尾\n}'
expect(await facts(source)).toMatchObject({ 1: { properties: [{ name: 'a', docs: [] }, { name: 'b', docs: ['/** 第二 */'] }, { name: 'c', docs: ['// 第三'] }] } })
expect(await tree(source)).toEqual([{ at: [1, 3], kind: 'interface', notes: [2] }])
})
})
describe('typeScript 调用', () => {
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'typescript', source, node => adapter.call?.(node))
+76 -11
View File
@@ -1,4 +1,4 @@
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeclaratorFacts, ElementFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, DeclaratorFacts, ElementFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, InterfacePropertyFacts, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
@@ -23,7 +23,7 @@ const COMMENTS: CommentRules = {
lineStyle: comment => comment.text.startsWith('//') ? '//' : undefined,
// `/**/` 是空的块注释,不是 JSDoc
isDoc: comment => comment.text.startsWith('/**') && !comment.text.startsWith('/**/'),
documents: new Set(['function', 'class', 'enum', 'const', 'variable']),
documents: new Set(['function', 'class', 'interface', 'enum', 'const', 'variable']),
}
/** 讲整个文件的 JSDoc:写了 `@file`、`@fileoverview` 或 `@overview`。 */
@@ -104,6 +104,52 @@ function enumMembers(body: Node | null): EnumMemberFacts[] {
return members
}
/** 成员是属性签名、或写了参数名的索引签名(`[key: string]: T`)时它在属性表里的一行,说明是 `docs`;别的成员(方法签名这些)没有。 */
function propertyFacts(member: Node, docs: Node[]): InterfacePropertyFacts | undefined {
const name = member.childForFieldName('name')
const index = member.type === 'index_signature' ? member.childForFieldName('index_type') : null
if (!name || (member.type !== 'property_signature' && !index))
return undefined
const words = children(member)
return {
name,
...(index && { index }),
type: annotatedType(member.childForFieldName('type')),
optional: words.some(word => word.type === '?'),
readonly: words.some(word => word.type === 'readonly'),
docs,
}
}
/**
* interface 主体里的成员分两拨,照源码顺序:属性(见 [propertyFacts])进属性表,说明的收法同 [enumMembers];
* 别的成员(方法签名、调用签名、构造签名)连同讲它们的注释、最后一个成员后面的注释留给子块(`rest`)。
*/
function interfaceMembers(body: Node): { properties: InterfacePropertyFacts[], rest: Node[] } {
const properties: InterfacePropertyFacts[] = []
const rest: Node[] = []
let pending: Node[] = []
/** 上一个成员结束在哪一行,它是属性时还有它的说明:写在这一行行尾的注释归它。 */
let last: { row: number, docs?: Node[] } | undefined
for (const child of named(body)) {
if (child.type === 'comment') {
if (last && pending.length === 0 && child.startPosition.row === last.row && !COMMENTS.isDoc(child))
(last.docs ?? rest).push(child)
else
pending.push(child)
continue
}
const property = propertyFacts(child, pending)
if (property)
properties.push(property)
else
rest.push(...pending, child)
last = { row: child.endPosition.row, docs: property?.docs }
pending = []
}
return { properties, rest: [...rest, ...pending] }
}
/** 类成员:写了 public / private / protected 按写的,#name 是私有,默认公共。 */
function memberAccess(member: Node): Access {
const modifier = named(member).find(child => child.type === 'accessibility_modifier')
@@ -338,12 +384,14 @@ function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
case 'variable_declaration':
return [variableBlock(declaration, outer, top)]
case 'interface_declaration':
return [membersBlock(declaration, outer)]
return [interfaceBlock(declaration, outer, top)]
case 'enum_declaration':
return [enumBlock(declaration, outer, top)]
case 'ambient_declaration': {
// declare enum X {}:别的 declare 写法照旧是一条语句
const inner = named(declaration).find(child => child.type === 'enum_declaration')
// declare enum X {}、declare interface X {}:别的 declare 写法照旧是一条语句
const inner = named(declaration).find(child => child.type === 'enum_declaration' || child.type === 'interface_declaration')
if (inner?.type === 'interface_declaration')
return [interfaceBlock(inner, outer, top)]
return [inner ? enumBlock(inner, outer, top) : leafBlock('statement', outer)]
}
case 'type_alias_declaration':
@@ -481,18 +529,35 @@ function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
}
}
/** interface:成员(属性、方法签名)→ field。 */
function membersBlock(declaration: Node, outer: Node): CodeBlock {
/** interface(含 declare interface):整条声明是头部,属性在属性表里讲,方法签名这些别的成员是子块。 */
function interfaceBlock(declaration: Node, outer: Node, top: boolean): CodeBlock {
const name = declaration.childForFieldName('name')
const body = declaration.childForFieldName('body')
if (!body)
if (!name || !body)
return leafBlock('type', outer)
const { properties, rest } = interfaceMembers(body)
return {
kind: 'type',
...compoundShape(outer, body.startIndex, named(body), (list, afterRow) =>
listBlocks(list, afterRow, member => [leafBlock('field', member)], COMMENTS)),
kind: 'interface',
node: outer,
header: nodeSpan(outer),
children: listBlocks(rest, body.startPosition.row, interfaceMember, COMMENTS),
facts: {
name,
access: declarationAccess(outer, top),
typeParameters: syntaxChildren(declaration.childForFieldName('type_parameters')),
extends: syntaxChildren(named(declaration).find(child => child.type === 'extends_type_clause')),
properties,
},
}
}
/** 接口里没进属性表的成员:方法签名是函数块,别的(调用签名、构造签名)照旧是一行。 */
function interfaceMember(member: Node): CodeBlock[] {
if (member.type === 'method_signature')
return [functionBlock(member, member, member.childForFieldName('name'), memberAccess(member))]
return [leafBlock('field', member)]
}
return root => statements(named(root), true, -1)
}
@@ -1,6 +1,7 @@
import type { BlockKind, CallFacts, Inline, LanguageAdapter, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment, SourceDocument, TokenRole, Translator } from '../contract'
import type { ZhTable } from './zhTemplate'
import { describe, expect, it } from 'vitest'
import { blockAttributes } from '../attributes'
import { createPythonAdapter } from '../languages/python'
import { createTypeScriptAdapter } from '../languages/typescript'
import { flatSegments, inlineOf, listLines, previewText, propertyLines, readFixture, tableLines, walk, WASM_DIR, withBlocks } from '../testSupport'
@@ -115,7 +116,6 @@ describe('typeScript 中文直译', () => {
expect(text(env.get(25)?.segments)).toBe('如果 (folder 不成立)')
expect(text(env.get(26)?.segments)).toBe('返回 空数组')
expect(text(env.get(41)?.segments)).toBe('遍历 (常量 f 取自 files)')
expect(text(env.get(35)?.segments)).toBe('failed: { path: 字符串, reason: 字符串 }[]')
expect(text(issue.get(20)?.segments)).toBe('返回 对象 3 项')
expect(listLines(issue.get(20)?.items)).toEqual(['locked | 真', 'prerequisiteNumber | prereq.number', 'prerequisiteColumn | prereq.column'])
})
@@ -123,7 +123,6 @@ describe('typeScript 中文直译', () => {
it('其余块:结构照抄,复合语句去掉结尾的 {,注释去掉注释符号、保留换行', async () => {
const env = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
const issue = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
expect(env[32]).toBe('公共 接口 EnvLockResult')
expect(env[42]).toBe('尝试')
expect(env[46]).toBe('捕获 (err)')
expect(env[47]).toBe('result.failed.push({ path: f, reason: 如果 err 属于 Error 则 err.message 否则 String(err) })')
@@ -137,17 +136,18 @@ describe('typeScript 中文直译', () => {
'No persistent watcher — re-scan happens on every toggle, so newly created',
'`.env` files are only picked up the next time the user clicks the button.',
].join('\n'))
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
expect(issue[15]).toBe('如果 (issue 不成立 或者 issue.prerequisite 全等 未定义)')
})
it('每个被翻的词单独成段,着色取语法树词性', async () => {
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
expect(colors(flatSegments(env.get(32)?.segments ?? []).filter(segment => segment.role !== 'plain'))).toEqual([
const alias = await translateAll(ts, 'typescript', typescriptZhTranslator, 'export type EnvLockResult = Result')
expect(colors(flatSegments(alias.get(1)?.segments ?? []).filter(segment => segment.role !== 'plain'))).toEqual([
{ text: '公共', role: 'keyword' },
{ text: '接口', role: 'storage' },
{ text: '类型', role: 'storage' },
{ text: 'EnvLockResult', role: 'type' },
{ text: 'Result', role: 'type' },
])
expect(issue.get(15)?.segments).toContainEqual({ text: '全等', role: 'operator' })
expect(env.get(26)?.segments).toEqual([{ text: '返回', role: 'keyword' }, { text: ' ', role: 'plain' }, { text: '空数组', role: 'literal' }])
@@ -192,7 +192,7 @@ describe('typeScript 中文直译', () => {
'for (const key in obj) {}',
'has = \'k\' in obj',
'use(class A extends B implements C {})',
'interface I extends J, K {}',
'type U<T> = T extends [infer H extends string] ? H : never',
'type F<T extends X> = (t: T) => void',
'type R<T> = T extends string ? 1 : 2',
'export default g',
@@ -209,7 +209,7 @@ describe('typeScript 中文直译', () => {
expect(map[5]).toBe('遍历 (常量 key 取自 obj)')
expect(map[6]).toBe('has = \'k\' 在 obj')
expect(map[7]).toBe('use(类 A 继承 B 实现 C {})')
expect(map[8]).toBe('接口 I 继承 J, K')
expect(map[8]).toBe('类型 U<T> = T 符合 [infer H 限定为 字符串] ? H : 永不')
expect(map[9]).toBe('类型 F<T 限定为 X> = (t: T) => 无')
expect(map[10]).toBe('类型 R<T> = T 符合 字符串 ? 1 : 2')
expect(map[11]).toBe('公共 默认 g')
@@ -549,6 +549,8 @@ describe('属性表', () => {
expect(env[27]).toEqual(['名称: include', '初始值: 新建 RelativePattern(folder, ENV_PATTERN)'])
expect(env[28]).toEqual(['名称: uris', '初始值: 等待 workspace.findFiles(include, EXCLUDE_PATTERN)'])
expect(env[40]).toEqual(['名称: result', '类型: EnvLockResult', '初始值: 对象 3 项', ' total | files.length', ' ok | 空数组', ' failed | 空数组'])
expect(env[32]).toEqual(['名称: EnvLockResult', '访问级别: 公共', '属性: 3 个', ' total | 数值', ' ok | 字符串[]', ' failed | { path: 字符串, reason: 字符串 }[]'])
expect(issue[3]).toEqual(['名称: LockedReason', '访问级别: 公共', '属性: 3 个', ' locked | 布尔', ' prerequisiteNumber? | 数值', ' prerequisiteColumn? | 字符串'])
expect(issue[1]).toEqual(['来源: \'../gitea/types\'', '仅类型: 是', '导入项: 1 个', ' Issue'])
expect(issue[14]).toEqual(['名称: issue', '初始值: issues.find(i => i.number 全等 issueNumber)'])
expect(routes[19]).toEqual([
@@ -911,6 +913,75 @@ describe('属性表', () => {
})
})
describe('接口的属性表', () => {
const SESSION = [
'/** 会话。 */',
'export interface Session<T extends Base = Base> extends Base, Store<T> {',
' /** 编号 */',
' readonly id: string',
' // 标题',
' title?: string // 可以不填',
' [key: string]: unknown',
' readonly [index: number]: T',
' /** 关掉会话。 */',
' close(reason?: string): void',
' count',
'}',
'',
'interface Empty {}',
'interface Point { x: number; readonly y?: number }',
].join('\n')
it('名称 / 说明 / 访问级别 / 类型参数 / 继承有才显示;属性表「属性 | 类型 | 说明」:可省略的带 ?,只读写在类型前,索引签名写 [参数: 类型],没写类型写未标注,说明是注释、JSDoc 的原文', async () => {
const rows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, SESSION))
expect(rows[1]).toEqual([
'名称: Session',
'说明: 会话。',
'访问级别: 公共',
'类型参数: T 限定为 Base = Base',
'继承: Base, Store<T>',
'属性: 5 个',
' id | 只读 字符串 | 编号',
' title? | 字符串 | 标题 可以不填',
' [key: 字符串] | 未知 | ',
' [index: 数值] | 只读 T | ',
' count | 未标注 | ',
])
expect(rows[9]).toEqual(['名称: close', '说明: 关掉会话。', '访问级别: 公共', '异步: 否', '输入参数: 1 个', ' reason? | 字符串 | ', '输出参数: 无'])
expect(rows[14]).toEqual(['名称: Empty', '访问级别: 私有'])
expect(rows[15]).toEqual(['名称: Point', '访问级别: 私有', '属性: 2 个', ' x | 数值', ' y? | 只读 数值'])
})
it('一个属性都没有说明时没有「说明」列;每行讲的定义是属性名(索引签名是方括号里的参数名),类型参数一行讲这几个泛型参数', async () => {
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, SESSION)
const table = (line: number): PropertyRow | undefined => rows.get(line)?.find(row => row.label === '属性')
expect(table(1)?.columns).toEqual(['属性', '类型', '说明'])
expect(table(15)?.columns).toEqual(['属性', '类型'])
const spans = (row: { defines?: { startIndex: number, endIndex: number } } | undefined): string | undefined => row?.defines && SESSION.slice(row.defines.startIndex, row.defines.endIndex)
expect(table(1)?.items?.map(spans)).toEqual(['id', 'title', 'key', 'index', 'count'])
expect(spans(rows.get(1)?.find(row => row.label === '类型参数'))).toBe('T extends Base = Base')
expect(spans(rows.get(1)?.find(row => row.label === '属性'))).toBeUndefined()
})
it('着色:名称按类型,属性名按变量、带源码下标,只读按 storage,未标注按 hint,说明按 comment', async () => {
const rows = (await describeAll(ts, 'typescript', typescriptZhTranslator, SESSION)).get(1)
expect(colors(valueOf(rows, '名称'))).toEqual([{ text: 'Session', role: 'type' }])
const items = rows?.find(row => row.label === '属性')?.items ?? []
expect(items[0].cells[0]).toEqual([{ text: 'id', role: 'variable', offset: SESSION.indexOf('id: string') }])
expect(colors(items[0].cells.slice(1))).toEqual([
[{ text: '只读', role: 'storage' }, { text: ' ', role: 'plain' }, { text: '字符串', role: 'type' }],
[{ text: '编号', role: 'comment' }],
])
expect(colors(items[4].cells[1])).toEqual([{ text: '未标注', role: 'hint' }])
})
it('筛选:种类是 interface、叫「接口」,访问级别从 facts 推出', async () => {
const attributes = await withBlocks(ts, 'typescript', SESSION, blocks => blocks.map(blockAttributes))
expect(attributes).toEqual([{ kind: 'interface', access: 'public' }, { kind: 'interface', access: 'private' }, { kind: 'interface', access: 'private' }])
expect(typescriptZhTranslator.attributeNames?.values.kind?.interface).toBe('接口')
})
})
describe('源码下标(跳到定义用)', () => {
function itemCells(rows: PropertyRow[] | undefined, label: string): PreviewText[] {
return rows?.find(row => row.label === label)?.items?.flatMap(item => item.cells) ?? []
@@ -1,4 +1,4 @@
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ChainArgument, ChainFacts, ChainGroup, ChainStep, ChainStepFacts, ChainView, ClassFacts, CollectionFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, DeferredFacts, Defines, DirectiveFacts, Doc, ElementFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { Access, AttributeNames, BaseFacts, BlockKind, CallArgument, CallFacts, ChainArgument, ChainFacts, ChainGroup, ChainStep, ChainStepFacts, ChainView, ClassFacts, CollectionFacts, CommandContext, CommandFacts, CommandForm, CommandReading, CommandRule, CommandWord, CommonAttribute, DeclaratorFacts, DeferredFacts, Defines, DirectiveFacts, Doc, ElementFacts, EmbeddedReading, EntryFacts, EnumFacts, ExceptionFacts, FactBlock, FunctionFacts, HealthFacts, ImportFacts, ImportItem, Inline, InstructionFlag, InterfaceFacts, InterfacePropertyFacts, InterpreterFacts, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Markdown, Node, OptionsFacts, ParameterFacts, PositionalParameter, PreviewTable, PropertyItem, PropertyRow, Redirect, Segment, SegmentGroup, SourceDocument, SourceRange, SourceSpan, StageFacts, StartFacts, TextBlock, TextTranslation, TokenRole, Translator, VariableBlock } from '../contract'
import type { ParamDocs } from '../rules/docRows'
import { readDoc } from '../docTags'
import { commonIndent, splitLines } from '../editText'
@@ -220,6 +220,7 @@ export const KIND_ZH: Record<BlockKind, string> = {
import: '导入',
function: '函数',
class: '类',
interface: '接口',
trait: '特质',
impl: '实现',
module: '模块',
@@ -1024,6 +1025,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
case 'impl':
case 'module':
return classRows(block.facts, phrases)
case 'interface':
return interfaceRows(block.facts, phrases)
case 'enum':
return enumRows(block.facts, phrases)
case 'exception':
@@ -1480,8 +1483,6 @@ function parameterRow(label: string, parameters: ParameterFacts[], say: Phrases,
* 类没有构造参数表可对,说明里的 `@param` 照写成「构造参数」一张「名称 | 说明」表;`@throws` 是「异常」一行。
*/
function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
const listRow = (label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] =>
parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
const doc = facts.doc && say.doc(facts.doc)
return [
{ label: '名称', value: facts.name ? say.name(facts.name, 'type') : [hint('匿名')] },
@@ -1497,6 +1498,47 @@ function classRows(facts: ClassFacts, say: Phrases): PropertyRow[] {
]
}
/** 几项写成一行、逗号隔开(类型参数、父类、实现的接口这些);一项都没有时没有这一行。 */
function listRow(label: string, parts: Inline[][], defines: Defines = {}): PropertyRow[] {
return parts.length > 0 ? [{ label, value: joined(parts, ', '), ...defines }] : []
}
/**
* 接口:名称 / 说明 / 访问级别 / 类型参数 / 继承(extends 后面的各项,逗号隔开)/ 属性,说明及以后有才显示。
* 属性收成一张表:属性 | 类型 | 说明。可省略的属性名后面带 `?`(同参数表),索引签名写成 `[参数名: 类型]`;
* 只读的类型格前面写「只读」,没写类型写「未标注」;说明同枚举成员,一个属性都没有说明时没有这一列。每行讲的定义是属性名。
*/
function interfaceRows(facts: InterfaceFacts, say: Phrases): PropertyRow[] {
const docs = facts.properties.map(property => docsText(property.docs, say))
const described = docs.some(Boolean)
const plain = (text: string): Segment => ({ text, role: 'plain' })
const nameCell = ({ name, index, optional }: InterfacePropertyFacts): Inline[] => [
...(index ? [plain('['), ...say.name(name, 'variable'), plain(': '), ...say.translated(index), plain(']')] : say.name(name, 'variable')),
...(optional ? [plain('?')] : []),
]
const typeCell = ({ type, readonly }: InterfacePropertyFacts): Inline[] => [
...(readonly ? [{ text: '只读', role: 'storage' as const }, plain(' ')] : []),
...(type ? say.translated(type) : [hint('未标注')]),
]
const properties: PropertyRow = {
label: '属性',
value: count(facts.properties.length),
columns: described ? ['属性', '类型', '说明'] : ['属性', '类型'],
items: facts.properties.map((property, i) => ({
cells: [nameCell(property), typeCell(property), ...(described ? [docCell(docs[i])] : [])],
...defining(property.name),
})),
}
return [
{ label: '名称', value: say.name(facts.name, 'type') },
...docBodyRows(facts.doc && say.doc(facts.doc)),
...accessRows(facts.access),
...listRow('类型参数', facts.typeParameters.map(say.translated), defining(...facts.typeParameters)),
...listRow('继承', facts.extends.map(say.translated)),
...(facts.properties.length > 0 ? [properties] : []),
]
}
/** 父类的一项:TypeScript 的泛型实参紧跟在后面。 */
function baseText(base: BaseFacts, say: Phrases): Inline[] {
return base.typeArguments ? [...say.translated(base.value), ...say.translated(base.typeArguments)] : say.translated(base.value)
@@ -18,7 +18,7 @@ body {
图标区分具体是哪种(kindIcons.tsx)。卡片取自己的种类,子层取所属块的种类 */
:is([data-kind="import"], [data-kind="copy"]) { --kind: var(--cr-kind-import); }
[data-kind="function"] { --kind: var(--cr-kind-function); }
:is([data-kind="class"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
:is([data-kind="class"], [data-kind="interface"], [data-kind="type"]) { --kind: var(--cr-kind-type); }
[data-kind="trait"] { --kind: var(--cr-storage); }
[data-kind="attribute"] { --kind: var(--cr-constant); }
[data-kind="impl"] { --kind: var(--cr-kind-function); }
@@ -26,6 +26,7 @@ describe('块的种类图标', () => {
module: 'layers',
attribute: 'sliders-horizontal',
type: 'braces',
interface: 'braces',
enum: 'list-ordered',
exception: 'triangle-alert',
field: 'tag',
+1 -1
View File
@@ -3,7 +3,7 @@
*/
export type BlockKind
= | 'import' | 'function' | 'class' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
= | 'import' | 'function' | 'class' | 'interface' | 'enum' | 'exception' | 'type' | 'field' | 'const' | 'variable'
| 'trait' | 'impl' | 'module' | 'attribute'
| 'if' | 'else' | 'loop' | 'try' | 'catch' | 'finally' | 'return' | 'statement' | 'comment'
| 'interpreter' | 'options' | 'case' | 'command' | 'pipeline'