✨ feat(vscode): 代码阅读的 TypeScript interface 换成一张属性表卡片
契约加 InterfaceBlock / InterfaceFacts(种类「接口」,图标、颜色同类型块);属性签名和索引签名连同讲它的注释并进「属性 | 类型 | 说明」表,可省略带 ?、只读写在类型前、没标注写未标注,每行讲的定义是属性名;方法签名是函数子块,调用签名、构造签名照旧是子块;declare interface 也认;「了解一块代码」照旧按 接口名.属性名 找得到属性
This commit is contained in:
1 parent
f487b841ce
commit
6b1e6f3487
16 files changed
+334
-46
No files matched your search
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user