Files
superwork/vscode/src/codeReader/blockView.test.ts
T

483 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { BlockView } from './blockView'
import type { LanguageAdapter, LibraryRule, Translator } from './contract'
import { describe, expect, it } from 'vitest'
import { readBlockViews, readDefinitionKind, toBlockViews } from './blockView'
import { replaceLines } from './editText'
import { createBashAdapter } from './languages/bash'
import { createDockerfileAdapter } from './languages/dockerfile'
import { createPythonAdapter } from './languages/python'
import { createTypeScriptAdapter } from './languages/typescript'
import { commentText, DOCKERFILE_WASM_DIR, previewText, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from './testSupport'
import { bashZhTranslator } from './translators/bashZh'
import { dockerfileZhTranslator } from './translators/dockerfileZh'
import { pythonZhTranslator } from './translators/pythonZh'
import { typescriptZhTranslator } from './translators/typescriptZh'
const ts = createTypeScriptAdapter(WASM_DIR)
const py = createPythonAdapter(WASM_DIR)
const bash = createBashAdapter(WASM_DIR)
function views(adapter: LanguageAdapter, languageId: string, translator: Translator | undefined, source: string): Promise<BlockView[]> {
return withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator))
}
function flatten(list: BlockView[]): BlockView[] {
return list.flatMap(view => [view, ...flatten(view.children)])
}
function byId(list: BlockView[], id: string): BlockView {
const found = flatten(list).find(view => view.id === id)
if (!found)
throw new Error(`没有块 ${id}`)
return found
}
const SAMPLES: [LanguageAdapter, string, Translator, string][] = [
[ts, 'typescript', typescriptZhTranslator, 'envLock.ts.txt'],
[ts, 'typescript', typescriptZhTranslator, 'issueLock.ts.txt'],
[ts, 'typescript', typescriptZhTranslator, 'classes.ts.txt'],
[py, 'python', pythonZhTranslator, 'sample.py'],
[py, 'python', pythonZhTranslator, 'routes.py'],
[py, 'python', pythonZhTranslator, 'classes.py'],
[py, 'python', pythonZhTranslator, 'attributeDocs.py'],
]
const PROPERTY_KINDS = new Set(['function', 'class', 'import', 'const', 'variable'])
function translationText(view: BlockView): string {
if (!('translation' in view))
throw new Error(`块 ${view.id} 是属性表`)
return previewText(view.translation)
}
describe('blockView', () => {
it('有名字的函数、类(含嵌在里面的方法)标上能看调用树,别的块不标', async () => {
const source = 'import os\n\nLIMIT = 1\n\n\nclass Order:\n def pay(self):\n print(os.name)\n\n\ndef main():\n Order().pay()\n'
const found = flatten(await views(py, 'python', pythonZhTranslator, source)).map(view => [view.kind, view.callTree])
expect(found).toEqual([
['import', undefined],
['const', undefined],
['class', {}],
['function', {}],
['statement', undefined],
['function', {}],
['statement', undefined],
])
})
it.each(SAMPLES)('%#: codeTokens 拼起来等于 code,code 是编辑范围整行去共同缩进', async (adapter, languageId, translator, fixture) => {
const source = readFixture(fixture)
const lines = source.split('\n')
for (const view of flatten(await views(adapter, languageId, translator, source))) {
expect(view.codeTokens.map(token => token.text).join(''), view.id).toBe(view.code)
const rows = lines.slice(view.startLine, view.endLine + 1)
const indent = Math.min(...rows.filter(row => row.trim()).map(row => row.length - row.trimStart().length))
expect(view.code, view.id).toBe(rows.map(row => row.slice(indent)).join('\n'))
if ('properties' in view)
expect(view.properties.length, view.id).toBeGreaterThan(0)
else
expect(previewText(view.translation), view.id).not.toBe('')
}
})
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))
}
})
it.each(SAMPLES)('%#: 编辑范围是块的完整范围:盖住各子孙块,兄弟块之间最多共用一行,首尾不是空行,原样写回不变;头部是开头那几行', async (adapter, languageId, translator, fixture) => {
const source = readFixture(fixture)
const lines = source.split('\n')
const visit = (list: BlockView[], parent: BlockView | undefined): void => {
list.forEach((view, i) => {
expect(lines[view.startLine].trim(), view.id).not.toBe('')
expect(lines[view.endLine].trim(), view.id).not.toBe('')
expect(view.headerEndLine, view.id).toBeGreaterThanOrEqual(view.startLine)
expect(view.headerEndLine, view.id).toBeLessThanOrEqual(view.endLine)
if (parent) {
expect(view.startLine, `${parent.id} 盖住 ${view.id}`).toBeGreaterThanOrEqual(parent.startLine)
expect(view.endLine, `${parent.id} 盖住 ${view.id}`).toBeLessThanOrEqual(parent.endLine)
}
const next = list[i + 1]
if (next)
expect(view.endLine, `${view.id} / ${next.id}`).toBeLessThanOrEqual(next.startLine)
expect(replaceLines(source, view.startLine, view.endLine, view.code), view.id).toBe(source)
visit(view.children, view)
})
}
visit(await views(adapter, languageId, translator, source), undefined)
})
it('编辑父块时编辑框里是整块代码:头部、各子块、结尾都在,改完整块写回;子块的编辑框只有它自己连同它的子块', async () => {
const source = [
'/** 窗口顶部 40px.',
'侧栏开关只在侧栏顶部. */',
'export const DesktopTitlebar = ({ platform }: { platform: DesktopShell["platform"] }) => {',
' if (platform === "macos") {',
' return null',
' }',
' return <div data-tauri-drag-region="deep" />',
'}',
'',
].join('\n')
const list = await views(ts, 'typescriptreact', typescriptZhTranslator, source)
const bar = byId(list, '0')
expect(bar).toMatchObject({ kind: 'function', startLine: 0, headerEndLine: 2, endLine: 7, code: source.trimEnd() })
expect(byId(list, '0.0')).toMatchObject({ kind: 'if', startLine: 3, headerEndLine: 3, endLine: 5, code: 'if (platform === "macos") {\n return null\n}' })
expect(byId(list, '0.0.0')).toMatchObject({ kind: 'return', startLine: 4, endLine: 4, code: 'return null' })
expect(replaceLines(source, bar.startLine, bar.endLine, bar.code.replace('return null', 'return <span />'))).toBe(source.replace('return null', 'return <span />'))
})
it('展平成兄弟块的 else、elif、catch:各到下一块开头之前,最后一块到整条语句结尾(Shell 的 fi 归它);同一行写的 `} else {` 两块都有', async () => {
const code = async (adapter: LanguageAdapter, languageId: string, source: string): Promise<[string, number, number, string][]> =>
(await views(adapter, languageId, undefined, source)).map(view => [view.kind, view.startLine, view.endLine, view.code])
expect(await code(ts, 'typescript', 'if (a) {\n x()\n} else if (b) {\n y()\n}\nelse {\n z()\n}\ntry {\n x()\n}\ncatch {\n y()\n}\n')).toEqual([
['if', 0, 2, 'if (a) {\n x()\n} else if (b) {'],
['else', 2, 4, '} else if (b) {\n y()\n}'],
['else', 5, 7, 'else {\n z()\n}'],
['try', 8, 10, 'try {\n x()\n}'],
['catch', 11, 13, 'catch {\n y()\n}'],
])
expect(await code(py, 'python', 'if a:\n x()\nelif b:\n y()\nelse:\n z()\n')).toEqual([
['if', 0, 1, 'if a:\n x()'],
['else', 2, 3, 'elif b:\n y()'],
['else', 4, 5, 'else:\n z()'],
])
expect(await code(bash, 'shellscript', 'if [ -n "$a" ]; then\n x\nelif [ -n "$b" ]; then\n y\nelse\n z\nfi\n')).toEqual([
['if', 0, 1, 'if [ -n "$a" ]; then\n x'],
['else', 2, 3, 'elif [ -n "$b" ]; then\n y'],
['else', 4, 6, 'else\n z\nfi'],
])
})
it('函数的说明算在函数块的头部里:编辑时和签名一起显示、一起改', async () => {
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
expect(byId(env, '5')).toMatchObject({ kind: 'function', startLine: 17, headerEndLine: 22, endLine: 29 })
expect(byId(env, '5').code.split('\n').slice(0, 6)).toEqual([
'/**',
' * Scan the workspace folder rooted at `workspaceRoot` for all `.env*`',
' * files, skipping `node_modules`, `.git`, and `.claude`. Returns absolute',
' * fs paths. Empty array if the workspace folder isn\'t currently open.',
' */',
'export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {',
])
expect(byId(env, '4')).toMatchObject({ kind: 'const', startLine: 15, endLine: 15 })
const routes = await views(py, 'python', pythonZhTranslator, readFixture('routes.py'))
const getUser = flatten(routes).find(view => view.kind === 'function' && view.startLine === 28)
expect(getUser).toMatchObject({ startLine: 28, headerEndLine: 37 })
expect(getUser?.code.split('\n').slice(5, 10)).toEqual([
') -> UserDetailResponse:',
' """获取用户详情。',
'',
' 管理员可查看任意用户,普通用户只能查看自己。',
' """',
])
expect(getUser?.children[0]).toMatchObject({ kind: 'if', startLine: 38, endLine: 40, headerEndLine: 39 })
expect(commentText(getUser?.children[0])).toBe('非管理员只能查看自己')
})
it('类的说明同样算在类块的头部里:JSDoc 连着中间的装饰器,docstring 连着它前面的注释', async () => {
const repo = byId(await views(ts, 'typescript', typescriptZhTranslator, readFixture('classes.ts.txt')), '1')
expect(repo).toMatchObject({ kind: 'class', startLine: 2, headerEndLine: 8 })
expect(repo.code.split('\n').slice(4, 7)).toEqual([
' */',
'@Injectable()',
'export abstract class Repository<K, T extends Entity = Entity> extends Base<string> implements Loader<T>, Disposable {',
])
expect('properties' in repo && repo.properties[1]).toEqual({ label: '说明', value: { markdown: '仓库基类。\n\n子类实现 `load`。' } })
const notFound = byId(await views(py, 'python', pythonZhTranslator, readFixture('classes.py')), '3')
expect(notFound).toMatchObject({ kind: 'class', startLine: 17, headerEndLine: 19 })
expect(notFound.code.split('\n').slice(0, 3)).toEqual([
'class NotFound(AppResponse): # 行尾',
' # 说明前面的注释',
' """找不到资源。"""',
])
expect(notFound.children[0]).toMatchObject({ kind: 'variable', startLine: 20, endLine: 20 })
})
it('属性 docstring 算在常量、变量、字段块里:常量、变量的「说明」紧跟名称,字段的说明排在译文下面;编辑范围连着它', async () => {
const source = [
'CASE_DEPLOYMENT_UNIQUE_CONSTRAINT = "uq_case_library_deployment"',
'"""一 deployment 一案例的唯一约束名; 入库侧按它认"重复入库", 故与建表同源。"""',
'',
'',
'class CaseStatus:',
' ANALYZING = "analyzing"',
'',
' """正在分析源 deployment."""',
' title: str = ""',
' """案例标题,',
' 用户可编辑."""',
].join('\n')
const list = await views(py, 'python', pythonZhTranslator, source)
const snapshot = flatten(list).map(view => ({
id: view.id,
kind: view.kind,
lines: [view.startLine, view.endLine],
...('properties' in view ? { rows: propertyLines(view.properties) } : { translation: previewText(view.translation), doc: view.doc }),
}))
expect(snapshot).toEqual([
{
id: '0',
kind: 'const',
lines: [0, 1],
rows: [
'名称: CASE_DEPLOYMENT_UNIQUE_CONSTRAINT',
'说明: 一 deployment 一案例的唯一约束名; 入库侧按它认"重复入库", 故与建表同源。',
'访问级别: 公共',
'值: "uq_case_library_deployment"',
],
},
{ id: '1', kind: 'class', lines: [4, 10], rows: ['名称: CaseStatus', '访问级别: 公共'] },
{ id: '1.0', kind: 'variable', lines: [5, 7], rows: ['名称: ANALYZING', '说明: 正在分析源 deployment.', '访问级别: 公共', '初始值: "analyzing"'] },
{ id: '1.1', kind: 'field', lines: [8, 10], translation: 'title: 字符串 = ""', doc: { markdown: '案例标题,\n用户可编辑.' } },
])
expect(byId(list, '0').code).toBe(source.split('\n').slice(0, 2).join('\n'))
expect(byId(list, '1.0').code).toBe('ANALYZING = "analyzing"\n\n"""正在分析源 deployment."""')
expect(byId(list, '1.1').code).toBe('title: str = ""\n"""案例标题,\n用户可编辑."""')
for (const view of flatten(list))
expect(replaceLines(source, view.startLine, view.endLine, view.code), view.id).toBe(source)
expect(replaceLines(source, 8, 10, 'title: str = "x"\n"""新说明"""').split('\n').slice(8)).toEqual([' title: str = "x"', ' """新说明"""'])
})
it('注释块的译文、函数的「说明」是 Markdown 原文,webview 按它渲染', async () => {
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
expect(byId(env, '0')).toMatchObject({ kind: 'comment', translation: { markdown: expect.stringMatching(/^\.env file lock \/ unlock helpers\.\n\nRecursively/) } })
const findEnvFiles = byId(env, '5')
expect('properties' in findEnvFiles && findEnvFiles.properties[1]).toEqual({ label: '说明', value: { markdown: expect.stringMatching(/^Scan the workspace folder/) } })
})
it('id 是块在树里的位置,行号 0 起,多行块去掉共同缩进', async () => {
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
expect(byId(env, '5.1.0')).toMatchObject({ kind: 'return', startLine: 25, endLine: 25, code: 'return []' })
expect(byId(env, '5.1')).toMatchObject({ kind: 'if', startLine: 24, endLine: 25, code: 'if (!folder)\n return []' })
expect(byId(env, '7.2.1')).toMatchObject({
kind: 'catch',
startLine: 45,
endLine: 47,
code: 'catch (err) {\n result.failed.push({ path: f, reason: err instanceof Error ? err.message : String(err) })\n}',
})
const issue = await views(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
expect(byId(issue, '2.4')).toMatchObject({
kind: 'return',
startLine: 19,
endLine: 23,
code: 'return {\n locked: true,\n prerequisiteNumber: prereq.number,\n prerequisiteColumn: prereq.column,\n}',
})
})
it('源码着色来自语法树:每个叶子一段,空白和标点归 plain', async () => {
const env = await views(ts, 'typescript', typescriptZhTranslator, readFixture('envLock.ts.txt'))
const tokens = byId(env, '5.3').codeTokens
expect(byId(env, '5.3').code).toBe('const uris = await workspace.findFiles(include, EXCLUDE_PATTERN)')
expect(tokens.slice(0, 7)).toEqual([
{ text: 'const', role: 'storage' },
{ text: ' ', role: 'plain' },
{ text: 'uris', role: 'variable' },
{ text: ' = ', role: 'plain' },
{ text: 'await', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: 'workspace', role: 'variable' },
])
expect(tokens).toContainEqual({ text: 'findFiles', role: 'function' })
expect(byId(env, '0').codeTokens).toEqual([{ text: byId(env, '0').code, role: 'comment' }])
// 中文注释之后的行:语法树下标按字符串下标算,不按 UTF-8 字节
const issue = await views(ts, 'typescript', typescriptZhTranslator, readFixture('issueLock.ts.txt'))
expect(byId(issue, '2').codeTokens.slice(0, 7)).toEqual([
{ text: '/**\n * 纯判定:工单被未完成的前置挡住才算锁定。快照里找不到工单或前置一律放行,\n * 与 webview 的 `isIssueLocked` 保持同一语义。\n */', role: 'comment' },
{ text: '\n', role: 'plain' },
{ text: 'export', role: 'keyword' },
{ text: ' ', role: 'plain' },
{ text: 'function', role: 'storage' },
{ text: ' ', role: 'plain' },
{ text: 'evaluateLockedReason', role: 'function' },
])
const sample = await views(py, 'python', pythonZhTranslator, readFixture('sample.py'))
const signature = byId(sample, '5').codeTokens
expect(signature).toContainEqual({ text: 'def', role: 'storage' })
expect(signature).toContainEqual({ text: 'find_env_files', role: 'function' })
// 签名里注解的 str 是类型,函数体里调用的 str(err) 是函数
expect(signature.filter(token => token.text === 'str')).toEqual([{ text: 'str', role: 'type' }, { text: 'str', role: 'type' }, { text: 'str', role: 'function' }])
})
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 })
})
it('\\r\\n 和制表符缩进:code 用 \\n 连接,去掉共同的制表符', async () => {
const source = 'function f() {\r\n\tif (a &&\r\n\t\tb) {\r\n\t\treturn 1\r\n\t}\r\n}\r\n'
const list = await views(ts, 'typescript', undefined, source)
const cond = byId(list, '0.0')
expect(cond).toMatchObject({ kind: 'if', startLine: 1, headerEndLine: 2, endLine: 4, code: 'if (a &&\n\tb) {\n\treturn 1\n}', translation: [] })
expect(cond.codeTokens.map(token => token.text).join('')).toBe(cond.code)
expect(byId(list, '0')).toMatchObject({ kind: 'function', properties: [] })
})
it('readBlockViews:解析、转视图、释放语法树', async () => {
const { blocks: list } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', 'def f(x: int) -> bool:\n return not x\n')
expect(list).toHaveLength(1)
expect('properties' in list[0] && propertyLines(list[0].properties)).toEqual([
'名称: f',
'访问级别: 公共',
'异步: 否',
'输入参数: 1 个',
' x | 整数 | ',
'输出参数: 布尔',
])
expect(translationText(list[0].children[0])).toBe('返回 非 x')
})
it('跨几行写的调用:一行译文下面带摊开实参的表;calleeKind 交给翻译插件分新建还是调用', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n )\n'
const { blocks: list } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { calleeKind: call => call.callee.text === 'Detail' ? 'class' : undefined })
const returned = list[0].children[0]
expect(translationText(returned)).toBe('返回 新建 Detail · 1 个参数')
expect('table' in returned && returned.table).toMatchObject({
columns: ['参数', '值'],
items: [{ cells: [[{ text: 'id', offset: source.indexOf('id=') }], [{ text: 'user' }, { text: '.' }, { text: 'id' }]] }],
})
})
it('跨几行写的列表:一行译文下面带拆开的各项,项里的调用带着自己的实参表', async () => {
const source = 'def f(user):\n return [\n Detail(\n id=user.id,\n ),\n user,\n ]\n'
const { blocks: list } = await readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { calleeKind: call => call.callee.text === 'Detail' ? 'class' : undefined })
const returned = list[0].children[0]
expect(translationText(returned)).toBe('返回 列表 2 项')
expect(returned).not.toHaveProperty('table')
expect('items' in returned && returned.items).toMatchObject([
{ cells: [[{ text: '第 1 项' }], [{ text: '新建' }, { text: ' ' }, { text: 'Detail', offset: source.indexOf('Detail') }, { text: ' ' }, { text: '· 1 个参数' }]], table: { columns: ['参数', '值'] } },
{ cells: [[{ text: '第 2 项' }], [{ text: 'user', offset: source.indexOf('user,') }]] },
])
})
it('概念列表:语言的种类在前,语言另认的接着,规则的种类和另认的在后;同一个 id 只留第一个', async () => {
const language: LanguageAdapter = {
...py,
concepts: block => block.kind === 'class'
? [
{ id: 'class', label: '重复的类', icon: 'zap' },
{ id: 'python.abstract', label: '抽象', icon: 'braces' },
]
: [],
}
const rule: LibraryRule = {
id: 'demo',
label: '演示',
languageIds: ['python'],
libraries: [],
claim: block => block.kind === 'class'
? {
kind: 'demo.thing',
badge: { label: '东西', color: { dark: '#111111', light: '#222222' }, icon: 'package' },
rows: [],
absorbs: [],
concepts: [
{ id: 'class', label: '又一个类', icon: 'lock' },
{ id: 'demo.thing', label: '重复的东西', icon: 'tag', color: { dark: '#333333', light: '#444444' } },
{ id: 'demo.extra', label: '额外', icon: 'lock', color: { dark: '#555555', light: '#666666' } },
],
}
: undefined,
}
const source = 'class Box:\n pass\n'
const [view] = await withBlocks(language, 'python', source, blocks =>
toBlockViews(blocks, { source, language, markers: [] }, pythonZhTranslator, { rules: [rule], report: () => {} }))
expect(view?.concepts).toEqual([
{ id: 'class', label: '类', icon: 'box' },
{ id: 'python.abstract', label: '抽象', icon: 'braces' },
{ id: 'demo.thing', label: '东西', icon: 'package', color: { dark: '#111111', light: '#222222' } },
{ id: 'demo.extra', label: '额外', icon: 'lock', color: { dark: '#555555', light: '#666666' } },
])
expect(view?.kind).toBe('demo.thing')
expect(view?.attributes.kind).toBe('demo.thing')
})
it('readDefinitionKind:offset 落在哪块自己的名字上就是那块的种类,里层的块优先;不在名字上没有', async () => {
const source = 'class UserService:\n def update_user(self):\n pass\n\nclass NotFound(Exception):\n pass\n\nlimit = 3\n'
const kind = (text: string): Promise<string | undefined> => readDefinitionKind(runtime, py, 'python', source, source.indexOf(text))
expect(await kind('UserService')).toBe('class')
expect(await kind('update_user')).toBe('function')
expect(await kind('NotFound')).toBe('exception')
expect(await kind('limit')).toBe('variable')
expect(await kind('pass')).toBeUndefined()
})
})
describe('卡片上的「注释」一行', () => {
it('luffy 的 acquisition/repository.py:讲类的注释是类卡片的「注释」一行(排在名称后面),类的编辑范围、源码连着这几行', async () => {
const source = readFixture('acquisitionRepository.py')
const list = await views(py, 'python', pythonZhTranslator, source)
expect(list.some(view => view.kind === 'comment')).toBe(false)
const repo = list.find(view => view.kind === 'class')
expect(repo).toMatchObject({ startLine: 12, headerEndLine: 20, commentTarget: { kind: 'replace', startLine: 12, endLine: 15 } })
expect('properties' in repo! && repo.properties.map(row => row.label).slice(0, 3)).toEqual(['名称', '注释', '访问级别'])
expect(commentText(repo)).toBe([
'luffy_agent.user.models.User 在各方法体内按需局部导入 (非模块顶层):',
'user 包 __init__ 会级联加载到 acquisition.service, 而 acquisition 是整个',
'应用最早被 import 的域之一 (见 api_routers.py 的 include 顺序), 顶层导入',
'会与该级联互相等待, 构成循环导入。',
].join('\n'))
expect(repo?.code.split('\n').slice(0, 7)).toEqual([
'# luffy_agent.user.models.User 在各方法体内按需局部导入 (非模块顶层):',
'# user 包 __init__ 会级联加载到 acquisition.service, 而 acquisition 是整个',
'# 应用最早被 import 的域之一 (见 api_routers.py 的 include 顺序), 顶层导入',
'# 会与该级联互相等待, 构成循环导入。',
'',
'',
'class AcquisitionChannelRepository(',
])
expect(replaceLines(source, repo!.startLine, repo!.endLine, repo!.code)).toBe(source)
})
it('说明和注释都有时排成 名称、说明、注释;几段注释是几段 Markdown;一行译文的卡片「注释」一行排在译文下面', async () => {
const source = [
'import { readFile } from \'node:fs/promises\'',
'',
'// 讲 load',
'',
'// 第二段',
'/** 读出配置。 */',
'export async function load(path: string) {',
' // 先检查',
' if (!path) {',
' return null',
' }',
' return readFile(path)',
'}',
].join('\n')
const [, load] = await views(ts, 'typescript', typescriptZhTranslator, source)
expect('properties' in load && load.properties.map(row => row.label).slice(0, 4)).toEqual(['名称', '说明', '注释', '访问级别'])
expect(commentText(load)).toBe('讲 load\n\n第二段')
expect(load).toMatchObject({ startLine: 2, commentTarget: { kind: 'replace', startLine: 5, endLine: 5 } })
const [check, returned] = load.children
expect(check).toMatchObject({ kind: 'if', startLine: 7, rows: [{ label: '注释', value: { markdown: '先检查' } }] })
expect(returned).toMatchObject({ kind: 'return', commentTarget: { kind: 'insert', line: 11 } })
expect(returned).not.toHaveProperty('rows')
})
it('嵌着的代码里的块不能写注释:没有 commentTarget', async () => {
const shell = createBashAdapter(WASM_DIR)
const source = 'FROM node:22\nRUN npm ci && npm run build\n'
const { blocks } = await readBlockViews(runtime, createDockerfileAdapter(DOCKERFILE_WASM_DIR), dockerfileZhTranslator, 'dockerfile', source, {
embedded: languageId => languageId === 'shellscript' ? { adapter: shell, translator: bashZhTranslator } : undefined,
})
const run = flatten(blocks).find(view => view.kind === 'run')
expect(run?.commentTarget).toEqual({ kind: 'insert', line: 1, shell: '# _' })
expect(run?.children.length).toBeGreaterThan(0)
expect(run?.children.every(step => step.commentTarget === undefined)).toBe(true)
})
})