✨ feat(vscode): 代码阅读加异常卡片,Python、TypeScript 的异常类画成同一张表(属性、构造参数),方法照常是子块

This commit is contained in:
cruldra committed 2026-09-27 10:42:10 +08:00
1 parent d5a092dd6f
commit 0afd012c94
21 files changed
+1118 -66

No files matched your search

@@ -0,0 +1,339 @@
import type { BlockView } from '../blockView'
import type { Inline, LanguageAdapter, PropertyRow, Segment, Translator } from '../contract'
import { describe, expect, it } from 'vitest'
import { fileAttributes } from '../attributes'
import { readBlockViews, toBlockViews } from '../blockView'
import { replaceLines } from '../editText'
import { celeryRule } from '../rules/celery'
import { dataclassRule } from '../rules/dataclass'
import { enumRule } from '../rules/enum'
import { sqlalchemyRule } from '../rules/sqlalchemy'
import { flatSegments, outline, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { typescriptZhTranslator } from '../translators/typescriptZh'
import { createPythonAdapter } from './python'
import { createTypeScriptAdapter } from './typescript'
const py = createPythonAdapter(WASM_DIR)
const ts = createTypeScriptAdapter(WASM_DIR)
const APP = readFixture('appExceptions.py')
const COMMERCE = readFixture('commerceExceptions.py')
const TS = readFixture('exceptions.ts.txt')
/** 和扩展里注册的顺序一样。 */
const RULES = [sqlalchemyRule, celeryRule, enumRule, dataclassRule]
function views(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<BlockView[]> {
return withBlocks(adapter, languageId, source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, translator, { rules: RULES, report: () => {} }))
}
const pyViews = (source: string): Promise<BlockView[]> => views(py, 'python', pythonZhTranslator, source)
const tsViews = (source: string): Promise<BlockView[]> => views(ts, 'typescript', typescriptZhTranslator, source)
function flatten(list: readonly BlockView[]): BlockView[] {
return list.flatMap(view => [view, ...flatten(view.children)])
}
function properties(view: BlockView | undefined): PropertyRow[] {
if (!view || !('properties' in view))
throw new Error(`块 ${view?.id} 不是属性表`)
return view.properties
}
function named(list: readonly BlockView[], name: string): BlockView {
const found = flatten(list).find(view => 'properties' in view && propertyLines(view.properties)[0] === `名称: ${name}`)
if (!found)
throw new Error(`没有 ${name}`)
return found
}
function lines(list: readonly BlockView[], name: string): string[] {
return propertyLines(properties(named(list, name)))
}
/** 属性表里所有带下标的词(连表格里的格子)。 */
function linkedSegments(rows: PropertyRow[]): Segment[] {
const pieces: Inline[] = rows.flatMap(row => [
...('markdown' in row.value ? [] : row.value),
...(row.items ?? []).flatMap(item => item.cells.flat()),
])
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
}
const PY_CASES = [
'from dataclasses import dataclass',
'import typing',
'',
'',
'class Late(Base):',
' """写在父类前面,照样认得出。"""',
'',
'',
'class Base(Exception):',
' # 错误码',
' # 机器可读',
' code = "E"',
' retry: bool = False # 能不能重试',
' detail: str',
' """额外的说明。"""',
'',
' # 讲方法的注释',
' def describe(self) -> str:',
' return self.code',
'',
' level = 1',
' a = b = 2',
' x, y = 1, 2',
'',
' def __init__(self, message: str, *, level: int = 1, **extra) -> None:',
' super().__init__(message)',
'',
' # 收尾',
'',
'',
'class Deeper(Late):',
' pass',
'',
'',
'class Stop(StopIteration): ...',
'class Warned(UserWarning): ...',
'class Remote(pkg.errors.AppException): ...',
'class Typed(typing.Generic[T], KeyboardInterrupt): ...',
'class Plain(Model, metaclass=ErrorMeta): ...',
'class Other(Mapping[str, int]): ...',
'class Starred(*bases): ...',
'',
'',
'class Loose(Exception):',
' def __init__(*args):',
' pass',
'',
'',
'@dataclass',
'class Payload(ValueError):',
' field: int = 0',
'',
'',
'def factory():',
' class Local(RuntimeError):',
' pass',
' return Local',
'',
].join('\n')
describe('异常卡片(Python)', () => {
it('luffy 的 core/exceptions:基类的类属性、构造参数,子类只改类属性;方法留作子块', async () => {
const list = await pyViews(APP)
expect(list.map(view => view.kind)).toEqual(['comment', 'import', 'import', ...Array.from({ length: 12 }, () => 'exception')])
expect(lines(list, 'AppException')).toEqual([
'名称: AppException',
'说明: 应用异常基类.\n\nAttributes:\n status_code: HTTP 状态码。\n error_code: 业务错误码(机器可读)。\n message: 错误描述(人类可读)。\n detail: 额外错误详情。',
'访问级别: 公共',
'继承: Exception',
'属性: 3 个',
' status_code | 整数 | 500',
' error_code | 字符串 | "INTERNAL_ERROR"',
' message | 字符串 | "服务器内部错误"',
'构造参数: 3 个',
' message | 字符串 | 空 | 空',
' detail | Any | 空',
' error_code | 字符串 | 空 | 空',
])
const app = properties(named(list, 'AppException'))
expect(app.map(row => row.columns)).toEqual([undefined, undefined, undefined, undefined, ['属性', '类型', '值'], ['名称', '类型', '默认值']])
expect(named(list, 'AppException').children.map(child => [child.kind, child.startLine])).toEqual([['function', 25], ['function', 39]])
expect(lines(list, 'NotFound')).toEqual([
'名称: NotFound',
'说明: 资源不存在 (404).',
'访问级别: 公共',
'继承: AppException',
'属性: 3 个',
' status_code | 404',
' error_code | "NOT_FOUND"',
' message | "资源不存在"',
])
expect(properties(named(list, 'NotFound')).at(-1)?.columns).toEqual(['属性', '值'])
expect(named(list, 'NotFound').children).toEqual([])
expect(lines(list, 'GatewayTimeout').slice(0, 2)).toEqual([
'名称: GatewayTimeout',
'说明: 上游服务响应超时 (504).\n\n用于表达 "请求已发往上游但在时限内未拿到响应":区别于 502\n(上游明确报错) 和 503 (上游不可达/未就绪)。',
])
})
it('luffy 的 commerce/exceptions:一串子类都是异常;只有说明的一张卡片四行,写了 __init__ 的多一行构造参数', async () => {
const list = await pyViews(COMMERCE)
expect(list.map(view => view.kind)).toEqual(['comment', 'import', ...Array.from({ length: 16 }, () => 'exception')])
expect(lines(list, 'CommerceError')).toEqual(['名称: CommerceError', '说明: commerce 域基础异常.', '访问级别: 公共', '继承: Exception'])
expect(lines(list, 'ProductNotFoundError')).toEqual(['名称: ProductNotFoundError', '说明: 商品不存在或已下架.', '访问级别: 公共', '继承: CommerceError'])
expect(lines(list, 'UnsupportedPaymentMethodError')).toEqual([
'名称: UnsupportedPaymentMethodError',
'说明: 未配置当前 payment_method 对应的 driver. 通常意味着环境变量缺失.',
'访问级别: 公共',
'继承: CommerceError',
'构造参数: 1 个',
' method | 字符串 | ',
])
expect(named(list, 'UnsupportedPaymentMethodError').children.map(child => child.kind)).toEqual(['function'])
})
it('认哪些类:父类是自带的异常类(含 Warning)、同一文件里的异常类(写在前面后面都行,一层层往下传)、名字以 Error / Exception / Warning 结尾;关键字参数、泛型、展开不算', async () => {
const list = await pyViews(PY_CASES)
const kinds = Object.fromEntries(flatten(list).filter(view => view.kind === 'exception' || view.kind === 'class')
.map(view => [propertyLines(properties(view))[0].replace('名称: ', ''), view.kind]))
expect(kinds).toEqual({
Late: 'exception',
Base: 'exception',
Deeper: 'exception',
Stop: 'exception',
Warned: 'exception',
Remote: 'exception',
Typed: 'exception',
Plain: 'class',
Other: 'class',
Starred: 'class',
Loose: 'exception',
Payload: 'exception',
Local: 'exception',
})
})
it('类属性:给一个名字赋值(带不带注解、有没有值),说明取上面的注释块、行尾注释、属性 docstring;连着赋值、解构赋值、方法照常是子块;构造参数去掉 self', async () => {
const list = await pyViews(PY_CASES)
expect(lines(list, 'Base')).toEqual([
'名称: Base',
'访问级别: 公共',
'继承: Exception',
'属性: 4 个',
' code | | "E" | 错误码 机器可读',
' retry | 布尔 | 假 | 能不能重试',
' detail | 字符串 | | 额外的说明。',
' level | | 1 | ',
'构造参数: 3 个',
' message | 字符串 | ',
' level | 整数 | 1',
' **extra | 未标注 | ',
])
const source = PY_CASES.split('\n')
const base = named(list, 'Base')
expect(base.children.map(child => source[child.startLine].trim())).toEqual([
'# 讲方法的注释',
'def describe(self) -> str:',
'a = b = 2',
'x, y = 1, 2',
'def __init__(self, message: str, *, level: int = 1, **extra) -> None:',
'# 收尾',
])
expect(lines(list, 'Loose').slice(-2)).toEqual(['构造参数: 1 个', ' *args | 未标注 | '])
expect(lines(list, 'Payload')).toEqual(['名称: Payload', '访问级别: 公共', '装饰器: @dataclass', '继承: ValueError', '属性: 1 个', ' field | 整数 | 0'])
expect(named(list, 'Local').attributes).toEqual({ kind: 'exception' })
})
it('编辑范围是整个类(并进来的属性、留下的方法都在里面),改卡片写回整个类;块自己是类名的定义,属性行、参数行各是它那个名字的定义', async () => {
const source = PY_CASES.split('\n')
const base = named(await pyViews(PY_CASES), 'Base')
expect([source[base.startLine], source[base.endLine]]).toEqual(['class Base(Exception):', ' # 收尾'])
expect(replaceLines(PY_CASES, base.startLine, base.endLine, base.code.replace('code = "E"', 'code = "F"')))
.toBe(PY_CASES.replace('code = "E"', 'code = "F"'))
expect(PY_CASES.slice(base.defines?.startIndex, base.defines?.endIndex)).toBe('Base')
const rows = properties(base)
const defined = (label: string): string[] => (rows.find(row => row.label === label)?.items ?? []).map(item => PY_CASES.slice(item.defines?.startIndex, item.defines?.endIndex))
expect(defined('属性')).toEqual(['code', 'retry', 'detail', 'level'])
expect(defined('构造参数')).toEqual(['message', 'level', '**extra'])
const [fieldName] = rows.find(row => row.label === '属性')?.items?.[0].cells[0] as Segment[]
expect(fieldName).toEqual({ text: 'code', role: 'variable', offset: PY_CASES.indexOf('code = "E"') })
for (const segment of linkedSegments(rows))
expect(PY_CASES.slice(segment.offset, (segment.offset ?? 0) + segment.text.length), segment.text).toBe(segment.text)
})
})
describe('异常卡片(TypeScript)', () => {
it('extends Error 的类:JSDoc 当说明,类字段(含 static)进属性表,constructor 的参数(含参数属性)进构造参数,方法留作子块', async () => {
const list = await tsViews(TS)
expect(lines(list, 'ApiError')).toEqual([
'名称: ApiError',
'说明: 调接口出错。',
'访问级别: 公共',
'继承: Error',
'属性: 2 个',
' status | | 500 | ',
' kind | 字符串 | \'API_ERROR\' | 机器可读的错误码',
'构造参数: 2 个',
' message | 字符串 | ',
' code? | 字符串 | ',
])
const api = named(list, 'ApiError')
expect(api.children.map(child => [child.kind, child.startLine])).toEqual([['function', 6], ['function', 10]])
expect([api.startLine, api.endLine]).toEqual([0, 13])
expect(lines(list, 'NotFoundError')).toEqual(['名称: NotFoundError', '访问级别: 公共', '继承: ApiError', '属性: 1 个', ' status | 404 | 找不到'])
})
it('认哪些类:同一文件里的异常类(写在后面也算)、赋给变量的类表达式、`ns.X<T>`;父类名不合习惯、不在这份文件里认出来的照常是类', async () => {
const list = await tsViews(TS)
expect(lines(list, 'Slow')).toEqual(['名称: Slow', '访问级别: 私有', '继承: Timeout'])
expect(named(list, 'Timeout').kind).toBe('exception')
expect(lines(list, 'Cancelled')).toEqual(['名称: Cancelled', '访问级别: 私有', '继承: RangeError'])
expect(lines(list, 'Forbidden')).toEqual(['名称: Forbidden', '访问级别: 公共', '继承: errors.HttpException<字符串>'])
const repository = named(list, 'Repository')
expect(repository.kind).toBe('class')
expect(repository.children.map(child => child.kind)).toEqual(['field'])
})
it('javaScript 的类字段同样认', async () => {
const source = 'class HttpError extends Error {\n status = 500\n static code\n}\n'
const list = await views(ts, 'javascript', typescriptZhTranslator, source)
expect(lines(list, 'HttpError')).toEqual(['名称: HttpError', '访问级别: 私有', '继承: Error', '属性: 2 个', ' status | 500', ' code | '])
})
it('typeScript 和 Python 同样的异常得到一样的种类、属性表行、列名', async () => {
const pyList = await pyViews('class Failed(Exception):\n status = 500 # 状态码\n\n def __init__(self, message: str) -> None:\n pass\n')
const tsList = await tsViews('export class Failed extends Error {\n status = 500 // 状态码\n\n constructor(message: string) {\n super(message)\n }\n}\n')
const shape = (list: BlockView[]): unknown => properties(named(list, 'Failed')).map(row => [row.label, row.columns, row.items?.map(item => item.cells.length)])
expect(named(pyList, 'Failed').kind).toBe('exception')
expect(named(tsList, 'Failed').kind).toBe('exception')
expect(shape(pyList)).toEqual(shape(tsList))
expect(lines(pyList, 'Failed').slice(-4)).toEqual(lines(tsList, 'Failed').slice(-4))
})
})
describe('异常卡片(宿主、筛选、中文名)', () => {
it('筛选:种类是 exception、叫「异常」,访问级别同原来的类;没有种类标签(badge),库规则不认', async () => {
const list = await pyViews(PY_CASES)
expect(named(list, 'Base').attributes).toEqual({ kind: 'exception', access: 'public' })
expect(named(list, 'Base').badge).toBeUndefined()
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values)
.toContainEqual({ id: 'exception', name: '异常', count: 10 })
})
it('各块的源码着色拼起来等于编辑范围的整行源码', async () => {
for (const [list, source] of [[await pyViews(APP), APP], [await pyViews(COMMERCE), COMMERCE], [await tsViews(TS), TS]] as const) {
const rows = source.split('\n')
for (const view of flatten(list)) {
expect(view.codeTokens.map(token => token.text).join(''), view.id).toBe(view.code)
const indent = Math.min(...rows.slice(view.startLine, view.endLine + 1).filter(row => row.trim()).map(row => row.length - row.trimStart().length))
expect(view.code, view.id).toBe(rows.slice(view.startLine, view.endLine + 1).map(row => row.slice(indent)).join('\n'))
}
}
})
it('代码块树:异常块的头部是整个类,留下的方法是子块', async () => {
expect(await withBlocks(py, 'python', 'class E(Exception):\n code = 1\n\n def run(self):\n pass\n', outline)).toEqual([
{ at: [1, 5], kind: 'exception', kids: [{ at: 4, kind: 'function', kids: [{ at: 5, kind: 'statement' }] }] },
])
})
it('属性的中文名:属性格子按下标换字,行尾记号不进说明', async () => {
const source = [
'from typing import Annotated',
'class Failed(Exception):',
' code: Annotated[str, Comment("错误码")] = "E"',
' status = 500 # 状态 @Comment 状态码',
].join('\n')
const { blocks, displayNames } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source)
expect(propertyLines(properties(named(blocks, 'Failed'))).slice(-3)).toEqual([
'属性: 2 个',
' code | Annotated[字符串, Comment("错误码")] | "E" | ',
' status | | 500 | 状态',
])
expect(displayNames.map(({ name, text }) => `${source.slice(name.startIndex, name.endIndex)} → ${text}`)).toEqual(['code → 错误码', 'status → 状态码'])
})
})
@@ -367,15 +367,15 @@ describe('python 类', () => {
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
const found = await facts(readFixture('classes.py'))
expect(found[6]).toEqual({
name: 'AppException',
doc: '"""应用异常的基类。\n\n 带上 HTTP 状态码。\n """',
name: 'AppResponse',
doc: '"""应用响应的基类。\n\n 带上 HTTP 状态码。\n """',
access: 'public',
decorators: [],
typeParameters: [],
bases: [{ value: 'Exception' }],
bases: [{ value: 'BaseModel' }],
implements: [],
})
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppException' }] })
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppResponse' }] })
expect(found[24]).toEqual({
name: 'Meta',
access: 'public',
+119 -3
View File
@@ -1,8 +1,8 @@
import type { Access, BlockShape, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { pythonNameReader } from './pythonNames'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren } from './shared'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren, withExceptions } from './shared'
/** 常量、类字段只认直接写在模块 / 类主体里的赋值;这两处的声明也才有访问级别。 */
type Scope = 'module' | 'class' | 'other'
@@ -398,6 +398,122 @@ function tryBlocks(node: Node): CodeBlock[] {
return blocks
}
// ─── 异常类 ───────────────────────────────────────────────────────────────────
/** Python 自带的异常类(builtins 里的,含各种 Warning)。 */
const BUILTIN_EXCEPTIONS = new Set([
'BaseException',
'BaseExceptionGroup',
'GeneratorExit',
'KeyboardInterrupt',
'SystemExit',
'Exception',
'ArithmeticError',
'FloatingPointError',
'OverflowError',
'ZeroDivisionError',
'AssertionError',
'AttributeError',
'BufferError',
'EOFError',
'ExceptionGroup',
'ImportError',
'ModuleNotFoundError',
'LookupError',
'IndexError',
'KeyError',
'MemoryError',
'NameError',
'UnboundLocalError',
'OSError',
'EnvironmentError',
'IOError',
'WindowsError',
'BlockingIOError',
'ChildProcessError',
'ConnectionError',
'BrokenPipeError',
'ConnectionAbortedError',
'ConnectionRefusedError',
'ConnectionResetError',
'FileExistsError',
'FileNotFoundError',
'InterruptedError',
'IsADirectoryError',
'NotADirectoryError',
'PermissionError',
'ProcessLookupError',
'TimeoutError',
'ReferenceError',
'RuntimeError',
'NotImplementedError',
'PythonFinalizationError',
'RecursionError',
'StopAsyncIteration',
'StopIteration',
'SyntaxError',
'IndentationError',
'TabError',
'SystemError',
'TypeError',
'ValueError',
'UnicodeError',
'UnicodeDecodeError',
'UnicodeEncodeError',
'UnicodeTranslateError',
'Warning',
'BytesWarning',
'DeprecationWarning',
'EncodingWarning',
'FutureWarning',
'ImportWarning',
'PendingDeprecationWarning',
'ResourceWarning',
'RuntimeWarning',
'SyntaxWarning',
'UnicodeWarning',
'UserWarning',
])
/** PEP 8 的命名习惯:异常类名以 Error 结尾(别的文件里的 `AppException`、`*Warning` 也常见)。 */
const EXCEPTION_NAME = /(?:Error|Exception|Warning)$/
/** 父类写的类名:`X`、`pkg.X` 取 X,泛型 `Base[T]` 取 Base 的;关键字参数、`*bases`、调用没有。 */
function className(node: Node | null): string | undefined {
switch (node?.type) {
case 'identifier':
return node.text
case 'attribute':
return node.childForFieldName('attribute')?.text
case 'subscript':
return className(node.childForFieldName('value'))
default:
return undefined
}
}
/**
* 类属性:类主体里给一个名字赋值的语句(`x = 1`、`x: int = 1`、只写注解的 `x: int`);连着赋值(`a = b = 1`)、解构赋值不算。
* 构造函数是 `__init__`,参数去掉第一个(self;`*args` 这类不是 self 的不去)。
*/
const PYTHON_EXCEPTIONS: ExceptionRules = {
baseName: base => className(base.value),
isExceptionName: name => BUILTIN_EXCEPTIONS.has(name) || EXCEPTION_NAME.test(name),
field: (block) => {
const assignment = block.kind === 'field' || block.kind === 'variable' ? assignmentIn(block.node) : undefined
const name = assignment?.childForFieldName('left')
const value = assignment?.childForFieldName('right')
if (!assignment || name?.type !== 'identifier' || value?.type === 'assignment')
return undefined
return { name, type: assignment.childForFieldName('type') ?? undefined, value: value ?? undefined }
},
constructorParameters: (block) => {
if (block.kind !== 'function' || block.facts.name?.text !== '__init__')
return undefined
const [receiver, ...rest] = block.facts.parameters
return receiver?.name.type === 'identifier' ? rest : block.facts.parameters
},
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
const STORAGE_KEYWORDS = new Set(['def', 'class', 'async', 'lambda', 'global', 'nonlocal', 'del'])
@@ -484,7 +600,7 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
id: 'python',
languageIds: ['python'],
grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'),
toBlocks: root => statements(named(root), 'module', -1),
toBlocks: root => withExceptions(statements(named(root), 'module', -1), PYTHON_EXCEPTIONS),
tokenRole: pythonTokenRole,
iteration: pythonIteration,
displayNames: pythonNameReader,
+126 -1
View File
@@ -1,4 +1,4 @@
import type { BlockShape, CodeBlock, DocumentedBlock, Node, SourceSpan, TextKind } from '../contract'
import type { BaseFacts, BlockShape, ClassBlock, ClassFacts, CodeBlock, DocumentedBlock, ExceptionBlock, FieldFacts, Node, ParameterFacts, SourceSpan, TextKind } from '../contract'
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
@@ -164,3 +164,128 @@ export function listBlocks(
export function sameNode(a: Node | null | undefined, b: Node): boolean {
return !!a && a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type
}
// ─── 异常类 ───────────────────────────────────────────────────────────────────
/** 语言怎么认异常类、类主体里哪些子块是类属性、哪个是构造函数。 */
export interface ExceptionRules {
/** 父类一项写的类名(点分写法取最后一节);关键字参数、展开、调用这类不是类名的项没有。 */
baseName: (base: BaseFacts) => string | undefined
/** 光看名字就认得是异常类:语言自带的异常类,或按命名习惯以 Error 这类后缀结尾。 */
isExceptionName: (name: string) => boolean
/** 类主体里的子块是类属性时它的名字、类型、值;不是时没有。 */
field: (block: CodeBlock) => Omit<FieldFacts, 'docs'> | undefined
/** 子块是构造函数时它的参数(Python 去掉 self);不是时没有。 */
constructorParameters: (block: CodeBlock) => ParameterFacts[] | undefined
}
function classBlocks(blocks: readonly CodeBlock[]): ClassBlock[] {
return blocks.flatMap(block => [...(block.kind === 'class' ? [block] : []), ...classBlocks(block.children)])
}
/** 紧挨在 child 前面的注释块(连着几块都算,中间没隔别的子块),照源码顺序:讲的是它下面的这个 child。 */
function leadingComments(children: readonly CodeBlock[], child: CodeBlock): CodeBlock[] {
const at = children.indexOf(child)
let from = at
while (from > 0 && children[from - 1].kind === 'comment')
from--
return children.slice(from, at)
}
/** 注释块里的各条注释(几条行注释连成的一块是好几个节点);Python 当注释用的字符串是那个字符串。 */
function commentNodes(block: CodeBlock): Node[] {
if (block.node.type !== 'comment')
return syntaxChildren(block.node).slice(0, 1)
const nodes: Node[] = []
for (let node: Node | null = block.node; node?.type === 'comment' && node.endIndex <= block.header.endIndex; node = node.nextSibling)
nodes.push(node)
return nodes
}
/** 块自己的说明节点:属性表块的 facts.doc,一行译文块的 doc(Python 字段的属性 docstring)。 */
function ownDoc(block: CodeBlock): Node | undefined {
if (!('facts' in block))
return block.doc
return block.kind === 'import' ? undefined : block.facts.doc
}
/** 类属性的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
function fieldDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] {
const trailing = block.node.nextNamedSibling
const doc = ownDoc(block)
return [
...leadingComments(children, block).flatMap(commentNodes),
...(trailing?.type === 'comment' && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []),
...(doc ? [doc] : []),
]
}
/**
* 类块换成异常块:类属性连同紧挨在它前面的注释块并进属性表,构造函数(写了几个取最后一个,重载的实现写在最后)的参数进属性表,
* 留下的子块(方法、构造函数本身、别的语句)不管写在哪都照源码顺序留作子块;头部是整个类(到它连同各子块的最后一行),
* 「编辑」时整段一起改,webview 在它编辑时不画留下的子块。
*/
function exceptionBlock(block: ClassBlock, rules: ExceptionRules): ExceptionBlock {
const { children, facts } = block
const fields: FieldFacts[] = []
const members: CodeBlock[] = []
let parameters: ParameterFacts[] | undefined
for (const child of children) {
const field = rules.field(child)
if (field) {
fields.push({ ...field, docs: fieldDocs(children, child) })
members.push(child)
}
parameters = rules.constructorParameters(child) ?? parameters
}
const absorbed = new Set(members.flatMap(member => [...leadingComments(children, member), member]))
const ends = [block.header, nodeSpan(block.node), ...children.flatMap(child => [child.header, nodeSpan(child.node)])]
const last = ends.reduce((end, span) => span.endIndex > end.endIndex ? span : end)
return {
kind: 'exception',
node: block.node,
header: { ...block.header, endIndex: last.endIndex, endRow: last.endRow },
children: children.filter(child => !absorbed.has(child)),
...(block.attributes && { attributes: block.attributes }),
facts: {
name: facts.name,
doc: facts.doc,
access: facts.access,
decorators: facts.decorators,
bases: facts.bases,
fields,
...(parameters && { parameters }),
},
}
}
/**
* 认出异常类,把它们的类块换成异常块(见 exceptionBlock)。父类里有一项是异常类就是异常类:光看名字认得出(isExceptionName),
* 或是同一文件里(不论写在前面还是后面、嵌套在哪)认出来的异常类,一层层往下传。父类从别的文件导入、名字又不合命名习惯的认不出来。
*/
export function withExceptions(blocks: CodeBlock[], rules: ExceptionRules): CodeBlock[] {
const classes = classBlocks(blocks)
const local = new Set<string>()
const isException = (facts: ClassFacts): boolean => facts.bases.some((base) => {
const name = rules.baseName(base)
return name !== undefined && (rules.isExceptionName(name) || local.has(name))
})
let grew = true
while (grew) {
grew = false
for (const { facts } of classes) {
const name = facts.name?.text
if (name !== undefined && !local.has(name) && isException(facts)) {
local.add(name)
grew = true
}
}
}
const convert = (list: CodeBlock[]): CodeBlock[] => list.map((block) => {
const children = convert(block.children)
const same = children.every((child, i) => child === block.children[i])
const next: CodeBlock = same ? block : { ...block, children }
return next.kind === 'class' && isException(next.facts) ? exceptionBlock(next, rules) : next
})
return convert(blocks)
}
+37 -3
View File
@@ -1,7 +1,7 @@
import type { Access, BaseFacts, BlockShape, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules } from './shared'
import type { CommentRules, ExceptionRules } from './shared'
import * as path from 'node:path'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren } from './shared'
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
import { typescriptNameReader } from './typescriptNames'
const GRAMMARS: Record<string, string> = {
@@ -476,6 +476,40 @@ function blockReader(typed: boolean): (root: Node) => CodeBlock[] {
return root => statements(named(root), true, -1)
}
// ─── 异常类 ───────────────────────────────────────────────────────────────────
/** 自带的错误类(Error、TypeError、RangeError、AggregateError……)名字都以 Error 结尾,DOMException 以 Exception 结尾;自己写的照这个习惯起名。 */
const EXCEPTION_NAME = /(?:Error|Exception)$/
/** 类字段:TypeScript 的 public_field_definition(名字在 name),JavaScript 的 field_definition(名字在 property)。 */
const FIELDS = new Set(['public_field_definition', 'field_definition'])
/** extends 后面写的类名:`X`、`ns.X` 取 X;调用(`Mixin(Base)`)这类没有。 */
function className(node: Node): string | undefined {
switch (node.type) {
case 'identifier':
case 'type_identifier':
return node.text
case 'member_expression':
return node.childForFieldName('property')?.text
default:
return undefined
}
}
/** 类属性是类字段(含 static,值不是带语句块主体的函数、类表达式的:那些照常是方法、类块);构造函数是 `constructor`。 */
const TYPESCRIPT_EXCEPTIONS: ExceptionRules = {
baseName: base => className(base.value),
isExceptionName: name => EXCEPTION_NAME.test(name),
field: (block) => {
const member = block.node
const name = block.kind === 'field' && FIELDS.has(member.type) ? member.childForFieldName('name') ?? member.childForFieldName('property') : null
if (!name)
return undefined
return { name, type: annotatedType(member.childForFieldName('type')), value: member.childForFieldName('value') ?? undefined }
},
constructorParameters: block => block.kind === 'function' && block.facts.name?.text === 'constructor' ? block.facts.parameters : undefined,
}
// ─── 源码着色 ─────────────────────────────────────────────────────────────────
/** 流程关键字;其余字母关键字都算声明关键字(storage)。 */
@@ -584,7 +618,7 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
throw new Error(`TypeScript 语言插件不支持 ${languageId}`)
return path.join(wasmDir, file)
},
toBlocks: (root, _source, languageId) => blockReader(TYPED_LANGUAGES.has(languageId))(root),
toBlocks: (root, _source, languageId) => withExceptions(blockReader(TYPED_LANGUAGES.has(languageId))(root), TYPESCRIPT_EXCEPTIONS),
tokenRole: typescriptTokenRole,
displayNames: typescriptNameReader,
}