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

261 lines
13 KiB
TypeScript

import type { BlockView } from './blockView'
import type { DefinitionStart, IndexedName, NameSources, ShownName, TextRange } from './nameResolution'
import { describe, expect, it } from 'vitest'
import { readBlockViews } from './blockView'
import { createPythonAdapter } from './languages/python'
import { createTypeScriptAdapter } from './languages/typescript'
import { displayNameIndex, DocumentNames, indexedNames, renderedNames, resolveNames, shiftNames } from './nameResolution'
import { enumRule } from './rules/enum'
import { MemoryIndexHost, propertyLines, runtime, WASM_DIR } from './testSupport'
import { pythonZhTranslator } from './translators/pythonZh'
import { typescriptZhTranslator } from './translators/typescriptZh'
import { WorkspaceIndexes } from './workspaceIndex'
const py = createPythonAdapter(WASM_DIR)
const ts = createTypeScriptAdapter(WASM_DIR)
const ROUTES = [
'async def delete_item(',
' item_id: int,',
' current_user: User = Depends(get_current_user), # @Comment 当前用户',
'):',
' if current_user.role != UserRole.ADMIN:',
' raise Forbidden()',
'',
].join('\n')
function read(source: string): ReturnType<typeof readBlockViews> {
return readBlockViews(runtime, py, pythonZhTranslator, 'python', source, { rules: { rules: [enumRule], report: () => {} } })
}
function allProperties(blocks: readonly BlockView[]): string[] {
return blocks.flatMap(block => [...('properties' in block ? propertyLines(block.properties) : []), ...allProperties(block.children)])
}
/** 源码里第 n 个 text 的下标 */
function at(source: string, text: string, n = 0): number {
let index = -1
for (let i = 0; i <= n; i++)
index = source.indexOf(text, index + 1)
return index
}
describe('中文名记号不是说明文字:宿主把语言插件认出的记号交给翻译插件去掉', () => {
it('python 枚举成员的行尾注释:只有记号时没有说明,记号前面的字照常是说明;readBlockViews 交出这份源码起了中文名的定义', async () => {
const source = [
'import enum',
'class Role(str, enum.Enum):',
' ADMIN = "admin" # 管理员账号 @Comment 管理员',
' MEMBER = "member" # @Comment 普通成员',
' GUEST = "guest" # 游客',
].join('\n')
const { blocks, displayNames } = await read(source)
expect(allProperties(blocks).filter(line => line.startsWith(' ') && line.includes('|'))).toEqual([
' ADMIN | "admin" | 管理员账号',
' MEMBER | "member" | ',
' GUEST | "guest" | 游客',
])
expect(displayNames.map(({ name, text }) => `${source.slice(name.startIndex, name.endIndex)} → ${text}`)).toEqual(['ADMIN → 管理员', 'MEMBER → 普通成员'])
})
it('typescript 枚举成员同样去掉;一个成员都没有说明时没有说明这一列', async () => {
const source = 'export enum Role {\n ADMIN = \'admin\', // @Comment 管理员\n USER = \'user\', // @Comment 普通用户\n}\n'
const { blocks } = await readBlockViews(runtime, ts, typescriptZhTranslator, 'typescript', source)
expect(allProperties(blocks).filter(line => line.startsWith(' '))).toEqual([' ADMIN | \'admin\'', ' USER | \'user\''])
})
})
/** 假的语言扩展:各处出现按名字文字认(测试里的名字都不重名),定义按表查。 */
function fakeSources(source: string, options: {
highlights?: Record<number, TextRange[]>
definitions?: Record<number, DefinitionStart[]>
indexed?: Record<string, IndexedName[]>
asked?: string[]
} = {}): NameSources {
return {
uri: 'file:///w/routes.py',
occurrences: async offset => options.highlights?.[offset] ?? [],
definitions: async (offset) => {
options.asked?.push(`definition ${offset}`)
return options.definitions?.[offset] ?? []
},
indexed: async (name) => {
options.asked?.push(`indexed ${name}`)
return options.indexed?.[name] ?? []
},
}
}
describe('中文名落到哪些名字上', () => {
it('renderedNames:一行译文、属性表的值和各层子行里带下标的名字,下标 → 原文', async () => {
const { blocks } = await read(ROUTES)
const rendered = renderedNames(blocks)
expect(rendered.get(at(ROUTES, 'current_user'))).toBe('current_user')
expect(rendered.get(at(ROUTES, 'current_user.role'))).toBe('current_user')
expect(rendered.get(at(ROUTES, 'ADMIN'))).toBe('ADMIN')
expect(rendered.get(at(ROUTES, 'delete_item'))).toBe('delete_item')
})
it('renderedNames:一行译文下面那张表(跨几行写的调用的实参)、值那一格里套着的表里带下标的名字也算', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n owner=Owner(\n name=user.name,\n ),\n )\n'
const rendered = renderedNames((await read(source)).blocks)
expect(rendered.get(at(source, 'id='))).toBe('id')
expect(rendered.get(at(source, 'user.id'))).toBe('user')
expect(rendered.get(at(source, 'Owner('))).toBe('Owner')
expect(rendered.get(at(source, 'name='))).toBe('name')
expect(rendered.get(at(source, 'user.name'))).toBe('user')
})
it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => {
const { blocks, displayNames } = await read(ROUTES)
const definition = at(ROUTES, 'current_user')
const use = at(ROUTES, 'current_user.role')
const sources = fakeSources(ROUTES, { highlights: { [definition]: [{ start: definition, end: definition + 12 }, { start: use, end: use + 12 }, { start: 3, end: 5 }, { start: at(ROUTES, 'ADMIN'), end: at(ROUTES, 'ADMIN') + 2 }] } })
const reports: [ShownName[], boolean][] = []
const settled = await resolveNames(displayNames, blocks, sources, (names, final) => reports.push([names, final]))
expect(settled).toBe(true)
const expected = [
{ offset: definition, length: 12, name: '当前用户', definition: true },
{ offset: use, length: 12, name: '当前用户' },
]
expect(reports).toEqual([[expected, false], [expected, true]])
})
it('别的文件定义的:原名在索引里(别的文件)的名字才问定义,定义处正好是索引里那个定义才换', async () => {
const { blocks } = await read(ROUTES)
const admin = at(ROUTES, 'ADMIN')
const role = at(ROUTES, 'role')
const asked: string[] = []
const models = 'file:///w/models.py'
const sources = fakeSources(ROUTES, {
asked,
indexed: {
ADMIN: [{ location: { uri: models, start: { line: 6, character: 4 }, end: { line: 6, character: 9 } }, text: '管理员' }],
role: [{ location: { uri: models, start: { line: 11, character: 4 }, end: { line: 11, character: 8 } }, text: '角色' }],
User: [{ location: { uri: 'file:///w/routes.py', start: { line: 0, character: 0 }, end: { line: 0, character: 4 } }, text: '本文件的不算' }],
},
definitions: {
[admin]: [{ uri: models, line: 6, character: 4 }],
// 定义提供者说 role 在别处(同名的另一个定义)
[role]: [{ uri: models, line: 20, character: 4 }],
},
})
const reports: ShownName[][] = []
const settled = await resolveNames([], blocks, sources, names => reports.push(names))
expect(settled).toBe(true)
expect(reports.at(-1)).toEqual([{ offset: admin, length: 5, name: '管理员' }])
expect(asked.filter(line => line.startsWith('definition')).sort()).toEqual([`definition ${admin}`, `definition ${role}`].sort())
})
it('语言扩展还没起来:定义处自己都查不到出现,或要问的定义一个都没答,算没答完(过一会儿再查)', async () => {
const { blocks, displayNames } = await read(ROUTES)
expect(await resolveNames(displayNames, blocks, fakeSources(ROUTES), () => {})).toBe(false)
const admin = at(ROUTES, 'ADMIN')
const indexed = { ADMIN: [{ location: { uri: 'file:///w/models.py', start: { line: 6, character: 4 }, end: { line: 6, character: 9 } }, text: '管理员' }] }
expect(await resolveNames([], blocks, fakeSources(ROUTES, { indexed }), () => {})).toBe(false)
expect(await resolveNames([], blocks, fakeSources(ROUTES, { indexed, definitions: { [admin]: [{ uri: 'file:///w/other.py', line: 0, character: 0 }] } }), () => {})).toBe(true)
})
})
describe('中文名跟着文档版本走', () => {
const name = (offset: number, text = '甲', length = 3): ShownName => ({ offset, length, name: text })
it('shiftNames:改动在前面的往后挪,碰到名字的丢掉,后面的不动;紧贴在名字前面插字也往后挪', () => {
const names = [name(10), name(20), name(30)]
expect(shiftNames(names, [{ offset: 0, length: 0, text: 'xx' }])).toEqual([name(12), name(22), name(32)])
expect(shiftNames(names, [{ offset: 21, length: 1, text: '' }])).toEqual([name(10), name(29)])
expect(shiftNames(names, [{ offset: 33, length: 5, text: '' }, { offset: 10, length: 0, text: 'y' }])).toEqual([name(11), name(21), name(31)])
})
it('每个版本查一次;手上没有时查到一步交一步,有上一版挪过来的就等查完再换;查的时候文档又改了,查到的丢掉', async () => {
const delivered: string[] = []
const names = new DocumentNames((version, list) => delivered.push(`${version}:${list.map(item => item.offset).join(',')}`), [])
let runs = 0
let finish: (() => void) | undefined
const run = (list: ShownName[]) => async (report: (names: ShownName[], final: boolean) => void): Promise<boolean> => {
runs++
report(list.slice(0, 1), false)
await new Promise<void>((resolve) => {
finish = resolve
})
report(list, true)
return true
}
names.resolve(1, run([name(10), name(20)]))
names.resolve(1, run([]))
expect(runs).toBe(1)
finish?.()
await Promise.resolve()
await Promise.resolve()
expect(delivered).toEqual(['1:10', '1:10,20'])
expect(names.known(1)).toEqual([name(10), name(20)])
names.edited(2, [{ offset: 0, length: 0, text: 'x' }])
expect(names.known(2)).toEqual([name(11), name(21)])
expect(names.known(1)).toEqual([])
names.resolve(2, run([name(11), name(21), name(40)]))
// 版本 2 查到一半文档又改了:查到的丢掉,版本 3 先用挪过的
names.edited(3, [{ offset: 0, length: 1, text: '' }])
finish?.()
await Promise.resolve()
await Promise.resolve()
expect(delivered).toEqual(['1:10', '1:10,20'])
expect(names.known(3)).toEqual([name(10), name(20)])
})
it('没答完隔一会儿再查,按给的间隔、最多那么多次;答完了就不再查', async () => {
const scheduled: number[] = []
const pending: (() => void)[] = []
const names = new DocumentNames(() => {}, [5, 10], (task, ms) => {
scheduled.push(ms)
pending.push(task)
return setTimeout(() => {}, 0)
})
let runs = 0
names.resolve(1, async () => {
runs++
return false
})
for (let i = 0; i < 4; i++) {
await Promise.resolve()
pending.shift()?.()
}
await Promise.resolve()
expect(runs).toBe(3)
expect(scheduled).toEqual([5, 10])
names.dispose()
})
})
describe('工作区里的中文名索引', () => {
it('python:别的文件里起了中文名的定义按原名查,给出定义处名字的位置和中文名;没写中文名的文件不解析', async () => {
const host = new MemoryIndexHost({
'app/models.py': 'import enum\n\n\nclass UserRole(str, enum.Enum):\n """角色。"""\n\n ADMIN = "admin" # @Comment 管理员\n',
'app/plain.py': 'ADMIN = 1\n',
}, py)
const indexes = new WorkspaceIndexes(host)
const found = await indexedNames((index, key) => indexes.find(index, key), py, 'ADMIN')
expect(found).toEqual([{ location: { uri: 'file:///w/app/models.py', start: { line: 6, character: 4 }, end: { line: 6, character: 9 } }, text: '管理员' }])
expect(await indexedNames((index, key) => indexes.find(index, key), py, 'UserRole')).toEqual([])
expect(host.reads.sort()).toEqual(['app/models.py', 'app/plain.py'])
indexes.dispose()
})
it('一种 languageId 一份索引;语言插件不认中文名、不管这种文件时没有', () => {
expect(displayNameIndex(py, 'python')?.id).toBe('displayNames:python')
expect(displayNameIndex(ts, 'typescriptreact')?.include).toBe('**/*.tsx')
expect(displayNameIndex(py, 'typescript')).toBeUndefined()
const { displayNames: _, ...plain } = py
expect(displayNameIndex(plain, 'python')).toBeUndefined()
})
it('查到的项带着认的时候给的中文名,库规则的 lookup 照旧只给位置', async () => {
const host = new MemoryIndexHost({ 'm.py': 'LIMIT = 1 # @Comment 上限\n' }, py)
const indexes = new WorkspaceIndexes(host)
const index = displayNameIndex(py, 'python')!
expect((await indexes.find(index, 'LIMIT')).map(({ entry }) => entry.text)).toEqual(['上限'])
expect(await indexes.lookup(index, 'LIMIT')).toEqual([{ uri: 'file:///w/m.py', start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }])
indexes.dispose()
})
})