199 lines
10 KiB
TypeScript
199 lines
10 KiB
TypeScript
import type { Memento } from 'vscode'
|
||
import type { LibraryRule } from '../contract'
|
||
import { describe, expect, it } from 'vitest'
|
||
import { packageCommandRules } from '../commands/packageCommands'
|
||
import { shellCommandRules } from '../commands/shellCommands'
|
||
import { createBashAdapter } from '../languages/bash'
|
||
import { createDockerfileAdapter } from '../languages/dockerfile'
|
||
import { createPythonAdapter } from '../languages/python'
|
||
import { createTypeScriptAdapter } from '../languages/typescript'
|
||
import { CodeReaderRegistry } from '../registry'
|
||
import { fastapiRule } from '../rules/fastapi'
|
||
import { sqlalchemyRule } from '../rules/sqlalchemy'
|
||
import { DOCKERFILE_WASM_DIR, runtime, WASM_DIR } from '../testSupport'
|
||
import { bashZhTranslator } from '../translators/bashZh'
|
||
import { dockerfileZhTranslator } from '../translators/dockerfileZh'
|
||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
||
import { documentMessage } from './documentMessage'
|
||
|
||
function registryWithTypeScript(): CodeReaderRegistry {
|
||
const store: Record<string, unknown> = {}
|
||
const registry = new CodeReaderRegistry({
|
||
get: <T>(key: string): T | undefined => store[key] as T | undefined,
|
||
update: async (key: string, value: unknown) => {
|
||
store[key] = value
|
||
},
|
||
} as Pick<Memento, 'get' | 'update'>)
|
||
registry.registerLanguage(createTypeScriptAdapter(WASM_DIR))
|
||
registry.registerTranslator(typescriptZhTranslator)
|
||
return registry
|
||
}
|
||
|
||
function registryWithPython(...rules: LibraryRule[]): CodeReaderRegistry {
|
||
const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick<Memento, 'get' | 'update'>)
|
||
registry.registerLanguage(createPythonAdapter(WASM_DIR))
|
||
registry.registerTranslator(pythonZhTranslator)
|
||
for (const rule of rules)
|
||
registry.registerLibraryRule(rule)
|
||
return registry
|
||
}
|
||
|
||
function ignore(): void {}
|
||
|
||
const MODEL = [
|
||
'from sqlalchemy.orm import Mapped, mapped_column',
|
||
'',
|
||
'class User(Base):',
|
||
' __tablename__ = "users"',
|
||
' id: Mapped[int] = mapped_column(primary_key=True)',
|
||
'',
|
||
].join('\n')
|
||
|
||
function doc(languageId: string, text: string, version = 3): { languageId: string, version: number, getText: () => string } {
|
||
return { languageId, version, getText: () => text }
|
||
}
|
||
|
||
describe('预览的 document 消息', () => {
|
||
it('受支持的语言:带文档版本、代码块、可选翻译和选中的翻译', async () => {
|
||
const { message } = await documentMessage(doc('typescript', 'if (!folder)\n return []\n'), registryWithTypeScript(), runtime, ignore)
|
||
expect(message).toMatchObject({
|
||
type: 'document',
|
||
version: 3,
|
||
languageId: 'typescript',
|
||
translators: [{ id: typescriptZhTranslator.id, label: typescriptZhTranslator.label }],
|
||
translatorId: typescriptZhTranslator.id,
|
||
})
|
||
expect(message.error).toBeUndefined()
|
||
expect(message.blocks.map(b => b.kind)).toEqual(['if'])
|
||
expect(message.blocks[0].children).toMatchObject([{ kind: 'return', translation: [{ text: '返回' }, { text: ' ' }, { text: '空数组' }] }])
|
||
expect(message.attributes).toEqual([
|
||
{ id: 'kind', name: '种类', values: [{ id: 'if', name: '条件', count: 1 }, { id: 'return', name: '返回', count: 1 }] },
|
||
])
|
||
})
|
||
|
||
it('没有语言插件的语言:带 error,不解析', async () => {
|
||
const { message } = await documentMessage(doc('rust', 'fn main() {}'), registryWithTypeScript(), runtime, ignore)
|
||
expect(message.blocks).toEqual([])
|
||
expect(message.attributes).toEqual([])
|
||
expect(message.translators).toEqual([])
|
||
expect(message.error).toContain('rust')
|
||
})
|
||
|
||
it('库规则认领的块:种类是规则的种类 id,筛选栏里叫规则给的名字', async () => {
|
||
const { message } = await documentMessage(doc('python', MODEL), registryWithPython(sqlalchemyRule), runtime, ignore)
|
||
expect(message.blocks.map(b => b.kind)).toEqual(['import', 'sqlalchemy.table'])
|
||
expect(message.attributes.find(attribute => attribute.id === 'kind')?.values).toEqual([
|
||
{ id: 'import', name: '导入', count: 1 },
|
||
{ id: 'sqlalchemy.table', name: '数据表', count: 1 },
|
||
])
|
||
})
|
||
|
||
it('库规则自己的筛选属性:名字取规则给的(FastAPI 接口的「请求方法」),值照写;规则起名时抛错记下来、照写 id', async () => {
|
||
const routes = [
|
||
'from fastapi import APIRouter',
|
||
'',
|
||
'router = APIRouter(prefix="/users")',
|
||
'',
|
||
'@router.get("/{user_id}")',
|
||
'def get_user(user_id: int):',
|
||
' pass',
|
||
'',
|
||
'@router.put("/{user_id}")',
|
||
'def update_user(user_id: int):',
|
||
' pass',
|
||
'',
|
||
'@router.get("")',
|
||
'def list_users():',
|
||
' pass',
|
||
'',
|
||
].join('\n')
|
||
const { message } = await documentMessage(doc('python', routes), registryWithPython(fastapiRule), runtime, ignore)
|
||
expect(message.attributes.find(attribute => attribute.id === 'httpMethod')).toEqual({
|
||
id: 'httpMethod',
|
||
name: '请求方法',
|
||
values: [{ id: 'GET', name: 'GET', count: 2 }, { id: 'PUT', name: 'PUT', count: 1 }],
|
||
})
|
||
|
||
const reports: string[] = []
|
||
const broken: LibraryRule = { ...fastapiRule, attributeNames: () => {
|
||
throw new Error('词表坏了')
|
||
} }
|
||
const again = await documentMessage(doc('python', routes), registryWithPython(broken), runtime, text => reports.push(text))
|
||
expect(again.message.attributes.find(attribute => attribute.id === 'httpMethod')?.name).toBe('httpMethod')
|
||
expect(reports).toEqual(['库规则 fastapi 给筛选属性起名时出错,筛选栏照写 id:词表坏了'])
|
||
})
|
||
|
||
it('调用的名字定义成哪种块交给 calleeKind 问:跨几行新建对象的一行译文下面带实参表', async () => {
|
||
const source = 'def f(user):\n return Detail(\n id=user.id,\n name=user.name,\n )\n'
|
||
const asked: string[] = []
|
||
const { message } = await documentMessage(doc('python', source), registryWithPython(), runtime, ignore, undefined, (call) => {
|
||
asked.push(call.callee.text)
|
||
return 'class'
|
||
})
|
||
expect(asked).toEqual(['Detail'])
|
||
expect(message.blocks[0].children[0]).toMatchObject({
|
||
kind: 'return',
|
||
translation: [{ text: '返回' }, { text: ' ' }, { text: '新建' }, { text: ' ' }, { text: 'Detail', role: 'type' }, { text: ' ' }, { text: '· 2 个参数', role: 'hint' }],
|
||
table: { columns: ['参数', '值'], items: [{ cells: [[{ text: 'id' }], expect.anything()] }, { cells: [[{ text: 'name' }], expect.anything()] }] },
|
||
})
|
||
})
|
||
|
||
it('库规则抛错:不算解析失败,交给 report 记下,这块照常显示', async () => {
|
||
const broken: LibraryRule = {
|
||
id: 'broken',
|
||
label: '坏规则',
|
||
languageIds: ['python'],
|
||
libraries: ['sqlalchemy'],
|
||
claim: () => {
|
||
throw new Error('读不懂')
|
||
},
|
||
}
|
||
const reports: string[] = []
|
||
const { message } = await documentMessage(doc('python', MODEL), registryWithPython(broken), runtime, text => reports.push(text))
|
||
expect(message.error).toBeUndefined()
|
||
expect(message.blocks.map(b => b.kind)).toEqual(['import', 'class'])
|
||
expect(reports).toEqual([1, 3, 4, 5].map((line, i) =>
|
||
`库规则 broken 看第 ${line} 行的 ${['import', 'class', 'variable', 'field'][i]} 块时出错,这块照常显示:读不懂`))
|
||
})
|
||
|
||
it('shell:注册的命令规则读命令,筛选栏有 shell 的种类,同一文件里的名字交给宿主跳转', async () => {
|
||
const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick<Memento, 'get' | 'update'>)
|
||
registry.registerLanguage(createBashAdapter(WASM_DIR))
|
||
registry.registerTranslator(bashZhTranslator)
|
||
for (const rule of shellCommandRules)
|
||
registry.registerCommandRule(rule)
|
||
const source = '#!/usr/bin/env bash\nset -eu\ndest="$1"\nrm -rf "$dest"\n'
|
||
const { message, symbols } = await documentMessage(doc('shellscript', source), registry, runtime, ignore)
|
||
expect(message.blocks.map(b => b.kind)).toEqual(['interpreter', 'options', 'variable', 'command'])
|
||
expect(message.blocks[3]).toMatchObject({ translation: [{ text: '删除' }, { text: ' ' }, { text: 'dest' }, { text: '(' }, { text: '连同里面的东西' }, { text: '、' }, { text: '不提示' }, { text: ')' }] })
|
||
expect(message.attributes.find(attribute => attribute.id === 'kind')?.values.map(value => value.name)).toEqual(['解释器', '选项', '变量', '命令'])
|
||
expect(symbols.map(symbol => symbol.occurrences.map(range => source.slice(range.startIndex, range.endIndex)))).toEqual([['dest', 'dest']])
|
||
})
|
||
|
||
it('dockerfile:RUN 里的 shell 交给注册的 shell 语言插件、翻译插件和命令规则读,名字连同 shell 里的一起交给宿主跳转', async () => {
|
||
const registry = new CodeReaderRegistry({ get: () => undefined, update: async () => {} } as Pick<Memento, 'get' | 'update'>)
|
||
registry.registerLanguage(createBashAdapter(WASM_DIR))
|
||
registry.registerLanguage(createDockerfileAdapter(DOCKERFILE_WASM_DIR))
|
||
registry.registerTranslator(bashZhTranslator)
|
||
registry.registerTranslator(dockerfileZhTranslator)
|
||
for (const rule of [...shellCommandRules, ...packageCommandRules])
|
||
registry.registerCommandRule(rule)
|
||
const source = 'FROM node:22 AS deps\nRUN x=1 && echo "$x" && apt-get update\nCMD ["node", "server.js"]\n'
|
||
const { message, symbols } = await documentMessage(doc('dockerfile', source), registry, runtime, ignore)
|
||
const [stage] = message.blocks
|
||
expect(stage.kind).toBe('stage')
|
||
expect(stage.children.map(child => [child.kind, child.children.map(step => step.kind)])).toEqual([['run', ['variable', 'command', 'command']], ['start', []]])
|
||
expect(stage.children[0].children[2]).toMatchObject({ translation: [{ text: '更新软件包列表' }] })
|
||
expect(message.attributes.find(attribute => attribute.id === 'kind')?.values.map(value => value.name)).toEqual(['阶段', '执行', '变量', '命令', '启动'])
|
||
expect(symbols.map(symbol => symbol.occurrences.map(range => source.slice(range.startIndex, range.endIndex)))).toEqual([['deps'], ['x', 'x']])
|
||
})
|
||
|
||
it('解析失败:把原因放进 error', async () => {
|
||
const failing = { parse: async () => Promise.reject(new Error('语法包加载失败')) }
|
||
const { message } = await documentMessage(doc('typescript', 'const a = 1'), registryWithTypeScript(), failing, ignore)
|
||
expect(message.blocks).toEqual([])
|
||
expect(message.error).toBe('语法包加载失败')
|
||
})
|
||
})
|