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 = {} const registry = new CodeReaderRegistry({ get: (key: string): T | undefined => store[key] as T | undefined, update: async (key: string, value: unknown) => { store[key] = value }, } as Pick) 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) 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) 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) 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('语法包加载失败') }) })