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

169 lines
8.4 KiB
TypeScript

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<BlockView[]> {
return withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator))
}
/** 每块的筛选属性,按 header 起始行(1 起)索引;同一行有几块取外层那块。 */
function attributesByLine(list: BlockView[], out: Record<number, Record<string, string>> = {}): Record<number, Record<string, string>> {
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 <div /> }'))
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<string, string>, children: BlockView[] = []): BlockView {
return { id, anchor: id, kind: 'statement', startLine: 0, endLine: 0, headerEndLine: 0, code: '', codeTokens: [], attributes, concepts: [{ id: 'statement', label: '语句', icon: 'code' }], 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)
})
})