import type { BlockView } from './blockView' import type { CodeBlock, LanguageAdapter, Translator } from './contract' import { describe, expect, it } from 'vitest' import { blockAttributes, fileAttributes, mergeAttributeNames } from './attributes' import { toBlockViews } from './blockView' import { createPythonAdapter } from './languages/python' import { createTypeScriptAdapter } from './languages/typescript' import { propertyLines, WASM_DIR, withBlocks } from './testSupport' import { pythonZhTranslator } from './translators/pythonZh' import { typescriptZhTranslator } from './translators/typescriptZh' const ts = createTypeScriptAdapter(WASM_DIR) const py = createPythonAdapter(WASM_DIR) function views(adapter: LanguageAdapter, languageId: string, source: string, translator?: Translator): Promise { return withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator)) } /** 每块的筛选属性,按 header 起始行(1 起)索引;同一行有几块取外层那块。 */ function attributesByLine(list: BlockView[], out: Record> = {}): Record> { for (const view of list) { out[view.startLine + 1] ??= view.attributes attributesByLine(view.children, out) } return out } const TS_SOURCE = [ 'import type { A } from \'a\'', // 1 'import { b } from \'b\'', // 2 'export abstract class Repo {', // 3 ' private async load() {}', // 4 ' count = 0', // 5 '}', // 6 'class Plain {}', // 7 'const LIMIT = 3', // 8 'export function run() {', // 9 ' if (ok) {', // 10 ' return 1', // 11 ' }', // 12 '}', // 13 ].join('\n') describe('块的筛选属性:宿主从 kind 和 facts 推出通用属性', () => { it('typeScript:种类、访问级别、异步、抽象、仅类型;没有访问级别的块(条件、返回、字段)不带 access', async () => { expect(attributesByLine(await views(ts, 'typescript', TS_SOURCE))).toEqual({ 1: { kind: 'import', typeOnly: 'true' }, 2: { kind: 'import', typeOnly: 'false' }, 3: { kind: 'class', access: 'public', abstract: 'true' }, 4: { kind: 'function', access: 'private', async: 'true' }, 5: { kind: 'field' }, 7: { kind: 'class', access: 'private', abstract: 'false' }, 8: { kind: 'const', access: 'private' }, 9: { kind: 'function', access: 'public', async: 'false' }, 10: { kind: 'if' }, 11: { kind: 'return' }, }) }) it('tSX 和 TypeScript 一样分抽象类、仅类型导入', async () => { const found = attributesByLine(await views(ts, 'typescriptreact', 'import type { A } from \'a\'\nabstract class B {}\nconst C = () => { return
}')) expect(found[1]).toEqual({ kind: 'import', typeOnly: 'true' }) expect(found[2]).toEqual({ kind: 'class', access: 'private', abstract: 'true' }) expect(found[3]).toEqual({ kind: 'function', access: 'private', async: 'false' }) }) it('javaScript / JSX 没有抽象类、仅类型导入:块上没有这两个属性,属性表里也没有这两行', async () => { const source = 'import { b } from \'b\'\nexport class K {}\nasync function f() {}' for (const languageId of ['javascript', 'javascriptreact']) { const list = await views(ts, languageId, source, typescriptZhTranslator) expect(attributesByLine(list)).toEqual({ 1: { kind: 'import' }, 2: { kind: 'class', access: 'public' }, 3: { kind: 'function', access: 'private', async: 'true' }, }) const rows = list.map(view => 'properties' in view ? propertyLines(view.properties) : []) expect(rows[0]).toEqual(['来源: \'b\'', '导入项: 1 个', ' b']) expect(rows[1]).toEqual(['名称: K', '访问级别: 公共']) } }) it('python:访问级别、异步;没有抽象、仅类型', async () => { const source = [ 'import os', // 1 'class _Hidden:', // 2 ' async def run(self):', // 3 ' pass', // 4 'LIMIT = 3', // 5 'def load():', // 6 ' for x in y:', // 7 ' pass', // 8 ].join('\n') const found = attributesByLine(await views(py, 'python', source)) expect(found[1]).toEqual({ kind: 'import' }) expect(found[2]).toEqual({ kind: 'class', access: 'private' }) expect(found[3]).toEqual({ kind: 'function', access: 'public', async: 'true' }) expect(found[5]).toEqual({ kind: 'const', access: 'public' }) expect(found[6]).toEqual({ kind: 'function', access: 'public', async: 'false' }) expect(found[7]).toEqual({ kind: 'loop' }) }) it('语言插件自己报的属性叠在上面:加新属性、盖过 facts 推出的值(Kotlin 的 internal),kind 总是块的种类', async () => { await withBlocks(ts, 'typescript', 'export function f() {}', ([block]) => { const extended: CodeBlock = { ...block, attributes: { access: 'internal', unsafe: 'true', kind: 'other' } } expect(blockAttributes(extended)).toEqual({ kind: 'function', access: 'internal', async: 'false', unsafe: 'true' }) }) }) }) function view(id: string, attributes: Record, children: BlockView[] = []): BlockView { return { id, anchor: id, kind: 'statement', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes, children, translation: [] } } describe('文件里出现过的属性', () => { const blocks = [ view('0', { kind: 'const', unsafe: 'true', access: 'private' }), view('1', { kind: 'function', access: 'public', async: 'false', pure: 'yes' }, [ view('1.0', { kind: 'if' }), view('1.1', { kind: 'function', async: 'true', unsafe: 'false' }), ]), view('2', { kind: 'import', typeOnly: 'false' }), ] it('通用属性按契约的顺序排在前面,语言自己的按第一次出现的先后排在后面;值按第一次出现的先后,带所有层的块数', () => { const found = fileAttributes(blocks, undefined) expect(found.map(attribute => attribute.id)).toEqual(['kind', 'access', 'async', 'typeOnly', 'unsafe', 'pure']) expect(found[0].values).toEqual([ { id: 'const', name: 'const', count: 1 }, { id: 'function', name: 'function', count: 2 }, { id: 'if', name: 'if', count: 1 }, { id: 'import', name: 'import', count: 1 }, ]) expect(found[4].values.map(value => [value.id, value.count])).toEqual([['true', 1], ['false', 1]]) }) it('显示名取翻译插件给的,没给的属性、值显示 id', () => { const found = fileAttributes(blocks, { attributes: { access: '访问级别' }, values: { access: { public: '公共' } } }) expect(found[1]).toEqual({ id: 'access', name: '访问级别', values: [{ id: 'private', name: 'private', count: 1 }, { id: 'public', name: '公共', count: 1 }] }) expect(found[0].name).toBe('kind') }) it('几份叫法合成一份:同一个属性、值排在前面的(翻译插件的)先用,没给的再取后面的(库规则的)', () => { expect(mergeAttributeNames([ { attributes: { access: '访问级别' }, values: { access: { public: '公共' } } }, undefined, { attributes: { access: '可见性', httpMethod: '请求方法' }, values: { access: { public: '开放', private: '内部' } } }, ])).toEqual({ attributes: { access: '访问级别', httpMethod: '请求方法' }, values: { access: { public: '公共', private: '内部' } }, }) }) it('中文直译给通用属性和值起名,和属性表里的叫法一样', async () => { const list = await views(ts, 'typescript', TS_SOURCE, typescriptZhTranslator) const found = fileAttributes(list, typescriptZhTranslator.attributeNames) const names = Object.fromEntries(found.map(attribute => [attribute.name, attribute.values.map(value => `${value.name} ${value.count}`)])) expect(names).toEqual({ 种类: ['导入 2', '类 2', '函数 2', '字段 1', '常量 1', '条件 1', '返回 1'], 访问级别: ['公共 2', '私有 3'], 异步: ['是 1', '否 1'], 抽象: ['是 1', '否 1'], 仅类型: ['是 1', '否 1'], }) const labels = list.flatMap(block => 'properties' in block ? block.properties.map(row => row.label) : []) expect(labels).toEqual(expect.arrayContaining(['访问级别', '抽象', '仅类型'])) expect(pythonZhTranslator.attributeNames).toEqual(typescriptZhTranslator.attributeNames) }) })