✨ feat(vscode): 代码阅读函数、导入、常量、变量块预览改成属性表

This commit is contained in:
cruldra committed 2026-09-25 04:56:22 +08:00
1 parent 0c4c9b0fba
commit dd9b39ac5d
19 files changed
+1458 -213

No files matched your search

@@ -1,9 +1,9 @@
import type { LanguageAdapter, Segment, Translator } from '../contract'
import type { LanguageAdapter, PropertyRow, Segment, Translator } from '../contract'
import type { ZhTable } from './zhTemplate'
import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from '../languages/python'
import { createTypeScriptAdapter } from '../languages/typescript'
import { readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
import { propertyLines, readFixture, walk, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from './pythonZh'
import { typescriptZhTranslator } from './typescriptZh'
import { createZhTemplateTranslator } from './zhTemplate'
@@ -13,19 +13,40 @@ const py = createPythonAdapter(WASM_DIR)
interface Translated { kind: string, segments: Segment[] }
/** 每块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。 */
/** 一行译文块的译文,按 header 起始行(1 起)索引;同一行有多块时取外层那块。属性表块见 describeAll。 */
function translateAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, Translated>> {
return withBlocks(adapter, languageId, source, (blocks) => {
const byLine = new Map<number, Translated>()
walk(blocks, (block) => {
const line = block.header.startRow + 1
if (!byLine.has(line))
if (!byLine.has(line) && !('facts' in block))
byLine.set(line, { kind: block.kind, segments: translator.translate(block, source) })
})
return byLine
})
}
/** 属性表块(函数、导入、常量、变量)的属性表,按 header 起始行(1 起)索引。 */
function describeAll(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string): Promise<Map<number, PropertyRow[]>> {
return withBlocks(adapter, languageId, source, (blocks) => {
const byLine = new Map<number, PropertyRow[]>()
walk(blocks, (block) => {
const line = block.header.startRow + 1
if (!byLine.has(line) && 'facts' in block)
byLine.set(line, translator.describe(block, source))
})
return byLine
})
}
function rowTexts(map: Map<number, PropertyRow[]>): Record<number, string[]> {
return Object.fromEntries([...map].map(([line, rows]) => [line, propertyLines(rows)]))
}
function valueOf(rows: PropertyRow[] | undefined, label: string): Segment[] {
return rows?.find(row => row.label === label)?.value ?? []
}
function text(segments: Segment[] | undefined): string {
return (segments ?? []).map(segment => segment.text).join('')
}
@@ -60,29 +81,22 @@ describe('typeScript 中文直译', () => {
it('spec 样例逐字一致', async () => {
const env = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
const issue = await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
expect(text(env.get(23)?.segments)).toBe('公共 异步 函数 findEnvFiles(workspaceRoot: 字符串): Promise<字符串[]>')
expect(text(env.get(38)?.segments)).toBe('异步 函数 chmodAll(workspaceRoot: 字符串, mode: 数值): Promise<EnvLockResult>')
expect(text(env.get(12)?.segments)).toBe('导入 { promises 作为 fsp } 来自 \'node:fs\'')
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(env.get(53)?.segments)).toBe('Set every `.env*` file in the workspace to 0o444 (read-only).')
expect(text(issue.get(14)?.segments)).toBe('常量 issue = issues.find(i => i.number 全等 issueNumber)')
expect(text(issue.get(20)?.segments)).toBe('返回 { locked: 真, prerequisiteNumber: prereq.number, prerequisiteColumn: prereq.column }')
})
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[24]).toBe('常量 folder = workspace.workspaceFolders?.find(f => f.uri.fsPath 全等 workspaceRoot)')
expect(env[28]).toBe('常量 uris = 等待 workspace.findFiles(include, EXCLUDE_PATTERN)')
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) })')
expect(env[1]).toBe('.env file lock / unlock helpers. Recursively scans the main workspace folder for `.env*` files (skipping `node_modules`, `.git`, `.claude`) and chmods them to 0o444 (locked) or 0o644 (unlocked) in bulk. Backs the toolbar Lock toggle in `PanelHeader`. 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.')
expect(issue[1]).toBe('导入 类型 { Issue } 来自 \'../gitea/types\'')
expect(issue[5]).toBe('prerequisiteNumber?: 数值')
expect(issue[9]).toBe('纯判定:工单被未完成的前置挡住才算锁定。快照里找不到工单或前置一律放行, 与 webview 的 `isIssueLocked` 保持同一语义。')
expect(issue[15]).toBe('如果 (非 issue 或者 issue.prerequisite 全等 未定义)')
@@ -91,13 +105,12 @@ describe('typeScript 中文直译', () => {
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(env.get(23)?.segments.filter(segment => segment.role !== 'plain').slice(0, 4)).toEqual([
expect(env.get(32)?.segments.filter(segment => segment.role !== 'plain')).toEqual([
{ text: '公共', role: 'keyword' },
{ text: '异步', role: 'storage' },
{ text: '函数', role: 'storage' },
{ text: 'findEnvFiles', role: 'function' },
{ text: '接口', role: 'storage' },
{ text: 'EnvLockResult', role: 'type' },
])
expect(issue.get(14)?.segments).toContainEqual({ text: '全等', role: 'operator' })
expect(issue.get(15)?.segments).toContainEqual({ text: '全等', role: 'operator' })
expect(env.get(26)?.segments).toEqual([{ text: '返回', role: 'keyword' }, { text: ' ', role: 'plain' }, { text: '空数组', role: 'literal' }])
expectWordsApart(env)
expectWordsApart(issue)
@@ -105,16 +118,18 @@ describe('typeScript 中文直译', () => {
it('属性名、对象键、非空断言不被误翻', async () => {
const source = [
'const o = { if: 1, return: i.number, string: x!.type, new: a.default }',
'let n = a !== b && !c || d == e != f',
'o = { if: 1, return: i.number, string: x!.type, new: a.default }',
'n = a !== b && !c || d == e != f',
'type S = string | void',
// eslint-disable-next-line no-template-curly-in-string -- 被翻译的源码文本里本来就有模板字符串
'const s: string | void = typeof v === \'string\' ? `${v}` : \'number\'',
's = typeof v === \'string\' ? `${v}` : \'number\'',
].join('\n')
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
expect(map[1]).toBe('常量 o = { if: 1, return: i.number, string: x!.type, new: a.default }')
expect(map[2]).toBe('变量 n = a 不全等 b 并且 非 c 或者 d 等于 e 不等于 f')
expect(map[1]).toBe('o = { if: 1, return: i.number, string: x!.type, new: a.default }')
expect(map[2]).toBe('n = a 不全等 b 并且 非 c 或者 d 等于 e 不等于 f')
expect(map[3]).toBe('类型 S = 字符串 | 无')
// eslint-disable-next-line no-template-curly-in-string -- 模板字符串原样照抄进译文
expect(map[3]).toBe('常量 s: 字符串 | 无 = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'')
expect(map[4]).toBe('s = 如果 类型为 v 全等 \'string\' 则 `${v}` 否则 \'number\'')
})
it('逻辑与、或译成「并且」「或者」', async () => {
@@ -133,34 +148,34 @@ describe('typeScript 中文直译', () => {
it('as / in / extends / default 按所在语法节点换译法', async () => {
const source = [
'import * as path from \'node:path\'',
'export * as path from \'node:path\'',
'export { run as start } from \'./run\'',
'const v = x as string',
'const k = [\'a\'] as const',
'v = x as string',
'k = [\'a\'] as const',
'for (const key in obj) {}',
'const has = \'k\' in obj',
'has = \'k\' in obj',
'class A extends B implements C {}',
'interface I extends J, K {}',
'function f<T extends X>(t: T) {}',
'type F<T extends X> = (t: T) => void',
'type R<T> = T extends string ? 1 : 2',
'export default function g() {}',
'export default g',
'switch (x) {',
' default:',
' break',
'}',
].join('\n')
const map = texts(await translateAll(ts, 'typescript', typescriptZhTranslator, source))
expect(map[1]).toBe('导入 * 作为 path 来自 \'node:path\'')
expect(map[1]).toBe('公共 * 作为 path 来自 \'node:path\'')
expect(map[2]).toBe('公共 { run 作为 start } 来自 \'./run\'')
expect(map[3]).toBe('常量 v = x 断言为 字符串')
expect(map[4]).toBe('常量 k = [\'a\'] 断言为 常量')
expect(map[3]).toBe('v = x 断言为 字符串')
expect(map[4]).toBe('k = [\'a\'] 断言为 常量')
expect(map[5]).toBe('遍历 (常量 key 取自 obj)')
expect(map[6]).toBe('常量 has = \'k\' 在 obj')
expect(map[6]).toBe('has = \'k\' 在 obj')
expect(map[7]).toBe('类 A 继承 B 实现 C')
expect(map[8]).toBe('接口 I 继承 J, K')
expect(map[9]).toBe('函数 f<T 限定为 X>(t: T)')
expect(map[9]).toBe('类型 F<T 限定为 X> = (t: T) => 无')
expect(map[10]).toBe('类型 R<T> = T 符合 字符串 ? 1 : 2')
expect(map[11]).toBe('公共 默认 函数 g()')
expect(map[11]).toBe('公共 默认 g')
expect(map[13]).toBe('默认情况')
})
@@ -168,15 +183,15 @@ describe('typeScript 中文直译', () => {
const source = [
'err instanceof Error ? err.message : String(err)',
'z = x ? (y ? 1 : 2) : 3',
'const w = a ? b : c ? d : e',
'const v = a !== b && !c ? typeof d : await e',
'w = a ? b : c ? d : e',
'v = a !== b && !c ? typeof d : await e',
].join('\n')
const map = await translateAll(ts, 'typescript', typescriptZhTranslator, source)
expect(texts(map)).toEqual({
1: '如果 err 属于 Error 则 err.message 否则 String(err)',
2: 'z = 如果 x 则 (如果 y 则 1 否则 2) 否则 3',
3: '常量 w = 如果 a 则 b 否则 如果 c 则 d 否则 e',
4: '常量 v = 如果 a 不全等 b 并且 非 c 则 类型为 d 否则 等待 e',
3: 'w = 如果 a 则 b 否则 如果 c 则 d 否则 e',
4: 'v = 如果 a 不全等 b 并且 非 c 则 类型为 d 否则 等待 e',
})
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword')).toEqual([
{ text: '如果', role: 'keyword' },
@@ -192,18 +207,11 @@ describe('python 中文直译', () => {
const map = await translateAll(py, 'python', pythonZhTranslator, readFixture('sample.py'))
expect(texts(map)).toEqual({
1: 'Scan a workspace for .env files.',
3: '导入 os',
4: '从 pathlib 导入 Path 作为 P',
6: 'MAX_DEPTH = 8',
7: 'skipped = 空列表',
10: '函数 find_env_files(root: 字符串) -> 列表[字符串]',
11: 'Return every .env file under root.',
12: '跳过备份文件',
13: 'found = [p 遍历 p 取自 os.listdir(root) 如果 p.startswith(\'.env\') 并且 非 p.endswith(\'.bak\')]',
14: '如果 非 found',
15: '返回 空列表',
16: '否则如果 len(found) > MAX_DEPTH',
17: 'found = found[:MAX_DEPTH]',
18: '否则',
19: '跳过',
20: '遍历 name 取自 found',
@@ -221,8 +229,6 @@ describe('python 中文直译', () => {
34: 'total: 整数',
35: 'ok: 列表[字符串] = 空列表',
36: 'failed: 字典[字符串, 字符串] = 空字典',
37: 'label = \'env\'',
39: '函数 summary(self, verbose: 布尔 = 假) -> 字符串',
40: '返回 如果 verbose 不是 空 则 f\'{self.total} files\' 否则 \'\'',
})
expectWordsApart(map)
@@ -233,16 +239,23 @@ describe('python 中文直译', () => {
'def pick(items: dict[str, int], list: set) -> tuple:',
' return [str(k) for k in items if k not in list]',
].join('\n')
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
expect(map[1]).toBe('函数 pick(items: 字典[字符串, 整数], list: 集合) -> 元组')
expect(map[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
expect(rowTexts(await describeAll(py, 'python', pythonZhTranslator, source))[1]).toEqual([
'名称: pick',
'访问级别: 公共',
'异步: 否',
'输入参数: 2 个',
' items | 字典[字符串, 整数] | ',
' list | 集合 | ',
'输出参数: 元组',
])
expect(texts(await translateAll(py, 'python', pythonZhTranslator, source))[2]).toBe('返回 [str(k) 遍历 k 取自 items 如果 k 不在 list]')
})
it('逻辑与、或译成「并且」「或者」', async () => {
expect(texts(await translateAll(py, 'python', pythonZhTranslator, 'a and b or c'))).toEqual({ 1: 'a 并且 b 或者 c' })
})
it('from:导入里「从」,raise … from 里「源自」,yield from 里「自」', async () => {
it('from:raise … from 里「源自」,yield from 里「自」', async () => {
const source = [
'from typing import Annotated',
'def walk(nodes):',
@@ -250,7 +263,6 @@ describe('python 中文直译', () => {
' raise RuntimeError(str(err)) from err',
].join('\n')
const map = texts(await translateAll(py, 'python', pythonZhTranslator, source))
expect(map[1]).toBe('从 typing 导入 Annotated')
expect(map[3]).toBe('产出 自 nodes')
expect(map[4]).toBe('抛出 RuntimeError(str(err)) 源自 err')
})
@@ -289,20 +301,166 @@ describe('python 中文直译', () => {
'z = (a if x else b) if y else c',
'v = [k for k in items if k not in seen] if not done and ok else None',
].join('\n')
const map = await translateAll(py, 'python', pythonZhTranslator, source)
expect(texts(map)).toEqual({
1: '_default_port = 如果 _role 等于 "beat" 则 5680 否则 5679',
2: 'y = 如果 x 则 a 否则 如果 y 则 b 否则 c',
3: 'z = 如果 y 则 (如果 x 则 a 否则 b) 否则 c',
4: 'v = 如果 非 done 并且 ok 则 [k 遍历 k 取自 items 如果 k 不在 seen] 否则 空',
const map = await describeAll(py, 'python', pythonZhTranslator, source)
expect(Object.fromEntries([...map].map(([line, rows]) => [line, text(valueOf(rows, '初始值'))]))).toEqual({
1: '如果 _role 等于 "beat" 则 5680 否则 5679',
2: '如果 x 则 a 否则 如果 y 则 b 否则 c',
3: '如果 y 则 (如果 x 则 a 否则 b) 否则 c',
4: '如果 非 done 并且 ok 则 [k 遍历 k 取自 items 如果 k 不在 seen] 否则 空',
})
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
expect(valueOf(map.get(1), '初始值').filter(segment => segment.role === 'keyword' || segment.role === 'operator')).toEqual([
{ text: '如果', role: 'keyword' },
{ text: '等于', role: 'operator' },
{ text: '则', role: 'keyword' },
{ text: '否则', role: 'keyword' },
])
expectWordsApart(map)
})
})
describe('属性表', () => {
it('spec 样例逐字一致', async () => {
const env = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt')))
const issue = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt')))
const routes = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('routes.py')))
expect(env[23]).toEqual([
'名称: findEnvFiles',
'访问级别: 公共',
'异步: 是',
'输入参数: 1 个',
' workspaceRoot | 字符串 | ',
'输出参数: Promise<字符串[]>',
])
expect(env[38]).toEqual([
'名称: chmodAll',
'访问级别: 私有',
'异步: 是',
'输入参数: 2 个',
' workspaceRoot | 字符串 | ',
' mode | 数值 | ',
'输出参数: Promise<EnvLockResult>',
])
expect(env[12]).toEqual(['来源: \'node:fs\'', '仅类型: 否', '导入项: 1 个', ' promises 作为 fsp'])
expect(env[15]).toEqual(['名称: ENV_PATTERN', '访问级别: 私有', '值: \'**/.env*\''])
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', '初始值: { total: files.length, ok: 空数组, failed: 空数组 }'])
expect(issue[1]).toEqual(['来源: \'../gitea/types\'', '仅类型: 是', '导入项: 1 个', ' Issue'])
expect(issue[14]).toEqual(['名称: issue', '初始值: issues.find(i => i.number 全等 issueNumber)'])
expect(routes[19]).toEqual([
'来源: .schemas',
'导入项: 3 个',
' UserAvatarUploadResponse',
' UserDetailResponse',
' UserUpdateRequest',
])
expect(routes[29]).toEqual([
'名称: get_user',
'访问级别: 公共',
'异步: 是',
'装饰器: @router.get("/{user_id}", response_model=UserDetailResponse)',
'输入参数: 3 个',
' user_id | Int4Path | ',
' db | AsyncSession | 默认 Depends(get_db)',
' current_user | User | 默认 Depends(get_current_user)',
'输出参数: UserDetailResponse',
])
expect(routes[111]).toContain(' file | Annotated[UploadFile, File(..., description="头像文件")] | ')
expect(routes[118]).toEqual(['名称: payload', '初始值: 等待 file.read()'])
const sample = rowTexts(await describeAll(py, 'python', pythonZhTranslator, readFixture('sample.py')))
expect(sample[10]).toEqual(['名称: find_env_files', '访问级别: 公共', '异步: 否', '输入参数: 1 个', ' root | 字符串 | ', '输出参数: 列表[字符串]'])
const port = rowTexts(await describeAll(py, 'python', pythonZhTranslator, '_default_port = 5680 if _role == "beat" else 5679'))
expect(port[1]).toEqual(['名称: _default_port', '访问级别: 私有', '初始值: 如果 _role 等于 "beat" 则 5680 否则 5679'])
})
it('属性名是纯文字;值按词性着色,说明词(未标注、N 个、默认)用 hint', async () => {
const env = await describeAll(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
const rows = env.get(23) ?? []
expect(rows.map(row => row.label)).toEqual(['名称', '访问级别', '异步', '输入参数', '输出参数'])
expect(rows[0].value).toEqual([{ text: 'findEnvFiles', role: 'function' }])
expect(rows[1].value).toEqual([{ text: '公共', role: 'keyword' }])
expect(rows[2].value).toEqual([{ text: '是', role: 'storage' }])
expect(rows[3].value).toEqual([{ text: '1 个', role: 'hint' }])
expect(rows[3].items).toEqual([{ cells: [[{ text: 'workspaceRoot', role: 'variable' }], [{ text: '字符串', role: 'type' }], []] }])
expect(rows[4].value).toEqual([
{ text: 'Promise', role: 'type' },
{ text: '<', role: 'plain' },
{ text: '字符串', role: 'type' },
{ text: '[]>', role: 'plain' },
])
expect(valueOf(env.get(12), '来源')).toEqual([{ text: '\'node:fs\'', role: 'string' }])
expect(env.get(12)?.at(-1)?.items?.[0].cells[0]).toEqual([
{ text: 'promises', role: 'variable' },
{ text: ' ', role: 'plain' },
{ text: '作为', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: 'fsp', role: 'variable' },
])
const sample = await describeAll(py, 'python', pythonZhTranslator, readFixture('sample.py'))
expect(valueOf(sample.get(3), '来源')).toEqual([{ text: 'os', role: 'type' }])
expect(sample.get(39)?.find(row => row.label === '输入参数')?.items).toEqual([
{ cells: [[{ text: 'self', role: 'variable' }], [{ text: '未标注', role: 'hint' }], []] },
{ cells: [[{ text: 'verbose', role: 'variable' }], [{ text: '布尔', role: 'type' }], [{ text: '默认 ', role: 'hint' }, { text: '假', role: 'storage' }]] },
])
})
it('导入:默认导出、全部、整个模块、副作用导入;Python 没有「仅类型」', async () => {
const tsSource = [
'import d, { b as c } from \'y\'',
'import * as ns from \'z\'',
'import \'./side\'',
'import fs = require(\'fs\')',
].join('\n')
expect(rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, tsSource))).toEqual({
1: ['来源: \'y\'', '仅类型: 否', '导入项: 2 个', ' 默认导出 作为 d', ' b 作为 c'],
2: ['来源: \'z\'', '仅类型: 否', '导入项: 1 个', ' 全部 作为 ns'],
3: ['来源: \'./side\'', '仅类型: 否', '导入项: 无'],
4: ['来源: \'fs\'', '仅类型: 否', '导入项: 1 个', ' 整个模块 作为 fs'],
})
const pySource = [
'import numpy.linalg as la',
'import os, sys as system',
'from m import *',
].join('\n')
expect(rowTexts(await describeAll(py, 'python', pythonZhTranslator, pySource))).toEqual({
1: ['来源: numpy.linalg', '导入项: 1 个', ' 整个模块 作为 la'],
2: ['来源: os, sys', '导入项: 2 个', ' os', ' sys 作为 system'],
3: ['来源: m', '导入项: 1 个', ' 全部'],
})
})
it('函数:匿名、可省略参数、嵌套函数没有访问级别、类成员受保护、装饰器照抄源码', async () => {
const source = [
'export default async function () {}',
'function f(b?: number, c = 1): void {',
' function inner() {}',
'}',
'class K {',
' @memo()',
' protected load(): x is Foo {}',
'}',
].join('\n')
expect(rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, source))).toEqual({
1: ['名称: 匿名', '访问级别: 公共', '异步: 是', '输入参数: 无', '输出参数: 未标注'],
2: ['名称: f', '访问级别: 私有', '异步: 否', '输入参数: 2 个', ' b? | 数值 | ', ' c | 未标注 | 默认 1', '输出参数: 无'],
3: ['名称: inner', '异步: 否', '输入参数: 无', '输出参数: 未标注'],
6: ['名称: load', '访问级别: 受保护', '异步: 否', '装饰器: @memo()', '输入参数: 无', '输出参数: x is Foo'],
})
})
it('变量:一条语句多个变量各占一组,没初始值写无,带语句块主体的回调只翻到主体开始之前', async () => {
const source = [
'let i = 0, n: number',
'function f() {',
' const v = useMemo(() => {',
' return 1',
' }, [])',
'}',
].join('\n')
const rows = rowTexts(await describeAll(ts, 'typescript', typescriptZhTranslator, source))
expect(rows[1]).toEqual(['名称: i', '访问级别: 私有', '初始值: 0', '名称: n', '访问级别: 私有', '类型: 数值', '初始值: 无'])
expect(rows[3]).toEqual(['名称: v', '初始值: useMemo(() =>'])
})
})
@@ -339,7 +497,7 @@ describe('中文模板引擎', () => {
' if k in seen:',
' pass',
' else:',
' x = a if c else b',
' print(a if c else b)',
'else:',
' pass',
].join('\n')
@@ -347,7 +505,7 @@ describe('中文模板引擎', () => {
expect(map[1]).toBe('for k 乙 items')
expect(map[2]).toBe('if k 甲 seen')
expect(map[4]).toBe('丁')
expect(map[5]).toBe('x = a if c 丙 b')
expect(map[5]).toBe('print(a if c 丙 b)')
expect(map[6]).toBe('戊')
})
@@ -359,41 +517,53 @@ describe('中文模板引擎', () => {
operatorParents: ['comparison_operator'],
nodeTemplates: {
conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }],
assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
augmented_assignment: ['令', { field: 'left' }, '加上', { field: 'right' }],
},
})
const source = [
'n: int = a if k in seen else b',
'n += a if k in seen else b',
'print(a if c else b if d else e)',
'w = (a # 注释不占序号',
' if k == 1 else b)',
'print((a # 注释不占序号',
' if k == 1 else b))',
].join('\n')
const map = await translateAll(py, 'python', translator, source)
expect(texts(map)).toEqual({
1: '令 n 类型 int 为 若 k 在 seen 则 a 不然 b',
1: '令 n 加上 若 k 在 seen 则 a 不然 b',
2: 'print(若 c 则 a 不然 若 d 则 b 不然 e)',
3: 'w = (若 k 等于 1 则 a 不然 b)',
3: 'print((若 k 等于 1 则 a 不然 b))',
})
expect(map.get(1)?.segments.filter(segment => segment.role === 'keyword').map(segment => segment.text))
.toEqual(['令', '类型', '为', '若', '则', '不然'])
.toEqual(['令', '加上', '若', '则', '不然'])
})
it('属性表里的类型、值用同一张表和同一套模板翻', async () => {
const translator = testTranslator({
tokenRole: () => 'variable',
keywords: { in: '在' },
builtinTypes: { int: '整型' },
isTypePosition: leaf => leaf.parent?.type === 'type',
nodeTemplates: { conditional_expression: ['若', { namedChild: 1 }, '则', { namedChild: 0 }, '不然', { namedChild: 2 }] },
})
const rows = await describeAll(py, 'python', translator, 'n: int = a if k in seen else b')
expect(rowTexts(rows)[1]).toEqual(['名称: n', '访问级别: 公共', '类型: 整型', '初始值: 若 k 在 seen 则 a 不然 b'])
})
it('没命中模板的节点照抄源码结构;模板引用的子节点不存在时也不重排', async () => {
const translator = testTranslator({
keywords: { in: '在', else: { zh: '丙', byContext: { else_clause: '丁' } } },
nodeTemplates: {
assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
augmented_assignment: ['令', { field: 'left' }, '类型', { field: 'type' }, '为', { field: 'right' }],
},
})
const source = [
'x = a if k in seen else b',
'x += a if k in seen else b',
'if k in seen:',
' pass',
'else:',
' pass',
].join('\n')
expect(texts(await translateAll(py, 'python', translator, source))).toEqual({
1: 'x = a if k 在 seen 丙 b',
1: 'x += a if k 在 seen 丙 b',
2: 'if k 在 seen',
3: 'pass',
4: '丁',