import type { BlockView } from '../blockView' import { describe, expect, it } from 'vitest' import { toBlockViews } from '../blockView' import { blockMarks } from '../diff/viewDiff' import { rustImplRule } from '../rules/rustImpl' import { isCrateRoot } from '../rules/rustTree' import { propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport' import { rustZhTranslator } from '../translators/rustZh' import { createRustAdapter } from './rust' const adapter = createRustAdapter(WASM_DIR) const sample = readFixture('rustLang.rs') function views(source: string, fileName?: string): Promise { return withBlocks(adapter, 'rust', source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [], ...(fileName && { fileName }) }, rustZhTranslator, { rules: [rustImplRule], report: () => {}, })) } function card(list: BlockView[], kind: string): BlockView { const found = list.find(block => block.kind === kind) if (!found) throw new Error(`没有 ${kind}`) return found } function markdown(block: BlockView | undefined): string | undefined { return block && 'translation' in block && 'markdown' in block.translation ? block.translation.markdown : undefined } describe('rust 语言卡片', () => { it('use、结构体、枚举、实现、函数、特质', async () => { expect(viewLines(await views(sample))).toEqual([ 'import 1', ' 来源: serde', ' 导入项: 1 个', ' Serialize | —', 'import 2', ' 来源: std::sync', ' 导入项: 1 个', ' Arc | —', 'import 3', ' 来源: foo', ' 导入项: 2 个', ' Bar | Baz', ' Qux | —', 'class 5', ' 名称: AgentRow', ' 说明: 桌面端列表里的一行代理。', ' 可见性: 公开', ' 字段: 2 个', ' id | Uuid | 公开', ' name | String | 本模块', 'class 12', ' 名称: CrateOnly', ' 可见性: 本 crate', ' 字段: 1 个', ' up | u8 | 父模块', 'enum 16', ' 名称: AgentState', ' 说明: 这个代理现在处在哪一步。', ' 变体: 3 个', ' Idle | —', ' Running | { pid: u32 }', ' Failed | String', 'rust.impl 23', ' 名称: AgentRow 的方法', ' 方法: 1 个', ' rename | &mut self, name: String | ()', ' function 24', ' 名称: rename', ' 可见性: 本模块', ' 参数: 2 个', ' self | &mut self', ' name | String', ' 返回: ()', 'function 27', ' 名称: start_server', ' 说明: 在本机拉起接口服务。', ' 异步: 是', ' 可见性: 公开', ' 输入参数: 1 个', ' addr | SocketAddr', ' 返回: Result<(), StartError>', ' 分情况: 2 支', ' Ok(listener) | 记下监听地址', ' Err(err) | 返回 StartError', 'trait 41', ' 名称: Store', ' 说明: 按 id 读写代理。', ' 方法: 2 个', ' get | &self, id: Uuid | Result', ' save | &mut self, row: AgentRow | Result<(), StoreError>', ]) }) it('实现的方法放进方法表那一行', async () => { const impl = card(await views(sample), 'rust.impl') const [method] = impl.children expect(method?.slot).toEqual({ row: 1, item: 0 }) }) it('给结构体加一个字段,多出来的那一行标成新增', async () => { const before = 'pub struct AgentRow {\n name: String,\n}\n' const after = 'pub struct AgentRow {\n name: String,\n id: Uuid,\n}\n' const [oldCard] = await views(before) const [newCard] = await views(after) const marks = oldCard && newCard ? blockMarks(oldCard, newCard) : undefined const field = newCard && 'properties' in newCard ? newCard.properties.findIndex(row => row.label === '字段') : -1 expect(field).toBeGreaterThanOrEqual(0) expect(marks?.rows?.changed[field]).toEqual({ items: { changed: { 1: { added: true } }, removed: [] }, }) }) it('带身体的模块拆成子块,常量和外部函数是卡片', async () => { const source = [ '/// 平台相关。', 'pub mod platform {', ' /// 读入。', ' use std::io::Read;', ' /// 上限。', ' pub const LIMIT: u32 = 8;', ' static NAME: &str = "a";', ' pub fn open() -> u32 { LIMIT }', ' extern "C" {', ' /// 外部。', ' pub fn bar(x: i32) -> i32;', ' }', ' let local = 1;', ' println!("hi");', '}', 'mod other;', '', 'impl Agent {', ' pub const N: u8 = 2;', '}', '', ].join('\n') const list = await views(source) const mod = list.find(block => block.kind === 'module') expect(mod?.kind).toBe('module') expect(list.some(block => block.kind === 'statement' && block.code.includes('const LIMIT'))).toBe(false) expect(list.filter(block => block.kind === 'statement').map(block => block.code)).toEqual(['mod other;']) expect(mod?.children.map(block => block.kind)).toEqual(['comment', 'import', 'const', 'variable', 'function', 'function', 'statement', 'statement']) expect(viewLines(mod ? [mod] : [])).toEqual([ 'module 1', ' 名称: platform', ' 说明: 平台相关。', ' 可见性: 公开', ' comment 3: 读入。', ' import 4', ' 来源: std::io', ' 导入项: 1 个', ' Read | —', ' const 5', ' 名称: LIMIT', ' 说明: 上限。', ' 访问级别: 公共', ' 类型: u32', ' 值: 8', ' variable 7', ' 名称: NAME', ' 类型: &str', ' 初始值: "a"', ' function 8', ' 名称: open', ' 可见性: 公开', ' 返回: u32', ' function 10', ' 名称: bar', ' 说明: 外部。', ' 可见性: 公开', ' 输入参数: 1 个', ' x | i32', ' 返回: i32', ' statement 13: 令 local = 1;', ' statement 14: println!("hi")', ]) const impl = list.find(block => block.kind === 'rust.impl') const inner = impl?.children.find(block => block.kind === 'const') expect(inner?.kind).toBe('const') expect(viewLines(inner ? [inner] : []).join('\n')).toContain('名称: N') expect(viewLines(inner ? [inner] : []).join('\n')).toMatch(/类型: u8|值: 2/) }) }) describe('rust 注释', () => { it('连着写的 // 合成一块注释,空的 // 是分段,预览是去掉 // 的 Markdown;后面的 use 照常是导入', async () => { const list = await views(readFixture('rustComments.rs')) expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([['comment', 1, 9], ['import', 11, 11]]) expect(markdown(list[0])).toBe([ 'apps/desktop/src-tauri/src/credentials.rs', '', '设备凭证按账号分开存 (键见 `device::account_key`: 后端地址 + 用户 id, 值为凭证 JSON)。', '执行器重启或同账号重登后拿它直接重连, 平台上还是同一台设备: 不必重新注册, 设备列表', '不多出一条, 应用的工作区也不因"换了设备"被重建。', '', '正式版存系统钥匙串 (macOS 钥匙串 / Windows 凭据管理器 / Linux Secret Service)。开发版', '存执行器数据目录下的文件: macOS 钥匙串按程序的确切构建认"谁能读", 没有苹果开发者身份的', '程序每重编一次就被当成新程序, 反复弹授权框。', ].join('\n')) }) it('隔着空行、隔着语句的注释各是一块,行尾的 // 不单独成块', async () => { const source = [ '// 第一段', '// 缩进四格的代码', '', '// 第二段', 'use std::io; // 读写', '// 第三段', 'const N: u8 = 1;', '', ].join('\n') const list = await views(source) expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([ ['comment', 1, 2], ['comment', 4, 4], ['import', 5, 5], ['comment', 6, 6], ['const', 7, 7], ]) expect(markdown(list[0])).toBe('第一段\n 缩进四格的代码') }) it('/// 并进下一个函数当说明,//! 连着几行合成一块,// 和 /// 不合在一起', async () => { const source = [ '//! 本模块管凭证。', '//!', '//! 第二段。', '', '// 普通注释', '/// 读出这个账号的凭证。', '/// 没存过时是 None。', '#[inline]', 'pub fn load() -> Option { None }', '', ].join('\n') const list = await views(source) expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([ ['comment', 1, 3], ['comment', 5, 5], ['function', 6, 9], ]) expect(markdown(list[0])).toBe('本模块管凭证。\n\n第二段。') expect(viewLines([list[2]!])).toContain(' 说明: 读出这个账号的凭证。') }) it('连着几行的 /// 是一段说明,块标签拆开放:@param 进参数表、@return 跟在返回后面、@throws 成异常表;结构体的 @param 对到字段', async () => { const source = [ '/// 读出这个账号的凭证。', '///', '/// @param account 账号', '/// @param ttl 写错的名字', '/// @return 没存过时是 None', '/// @throws [StoreError] 存储读不了', 'pub fn load(account: &str) -> Option { None }', '', '/// 一个凭证。', '/// @param token 访问令牌', 'pub struct Credential { pub token: String, scope: u8 }', ].join('\n') const list = await views(source) const fn = card(list, 'function') const struct = card(list, 'class') expect('properties' in fn && propertyLines(fn.properties)).toEqual([ '名称: load', '说明: 读出这个账号的凭证。', '对不上的参数: 1 个', ' ttl | 写错的名字', '可见性: 公开', '输入参数: 1 个', ' account | &str | 账号', '返回: Option — 没存过时是 None', '异常: 1 个', ' [StoreError] | 存储读不了', ]) expect('properties' in struct && propertyLines(struct.properties)).toEqual([ '名称: Credential', '说明: 一个凭证。', '可见性: 公开', '字段: 2 个', ' token | String | 公开 | 访问令牌', ' scope | u8 | 本模块 | ', ]) }) it('在 mod、impl 身体里同一套', async () => { const source = [ 'mod store {', ' // 一', ' // 二', ' fn a() {}', '}', 'impl Store {', ' // 三', ' //', ' // 四', ' fn b(&self) {}', '}', '', ].join('\n') const list = await views(source) const mod = list.find(block => block.kind === 'module') const impl = list.find(block => block.kind === 'rust.impl') expect(markdown(mod?.children[0])).toBe('一\n二') expect(markdown(impl?.children.find(block => block.kind === 'comment'))).toBe('三\n\n四') }) }) describe('rust 编译属性', () => { it('tauri 的 main.rs 开头的 cfg_attr:属性、生效条件、作用范围、说明', async () => { const source = [ '// Prevents additional console window on Windows in release, DO NOT REMOVE!!', '#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]', '', 'fn main() {}', '', ].join('\n') const list = await views(source, '/repo/apps/desktop/src-tauri/src/main.rs') expect(list.map(block => block.kind)).toEqual(['comment', 'attribute', 'function']) expect(viewLines([card(list, 'attribute')])).toEqual([ 'attribute 2', ' 属性: windows_subsystem = "windows"', ' 生效条件: 非 调试构建', ' 作用范围: 整个 crate', ' 说明: 在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口', ]) const attribute = card(list, 'attribute') const value = 'properties' in attribute ? attribute.properties[0]?.value : undefined expect(Array.isArray(value) && value.map(piece => 'role' in piece && piece.role)).toEqual(['variable', 'plain', 'plain', 'plain', 'string', 'string', 'string']) }) it('allow 列出各个 lint,别的文件顶层是本模块', async () => { const list = await views('#![allow(dead_code, unused_imports)]\n', '/repo/src/credentials.rs') expect(viewLines(list)).toEqual([ 'attribute 1', ' 属性: allow(dead_code, unused_imports)', ' 作用范围: 本模块', ' 说明: 编译检查(lint)级别设为放行:dead_code、unused_imports', ]) }) it('认不得的属性没有说明,不知道文件名时作用范围是所在模块', async () => { expect(viewLines(await views('#![rustfmt::skip]\n'))).toEqual([ 'attribute 1', ' 属性: rustfmt::skip', ' 作用范围: 所在模块', ]) }) it('cfg_attr 写了几个属性是一张表;all、any 套着的条件加括号;mod 身体里是本模块', async () => { const source = [ 'mod platform {', ' #![cfg_attr(all(unix, not(any(test, feature = "mock"))), allow(clippy::foo), deny(warnings))]', '}', '', ].join('\n') const mod = card(await views(source, '/repo/src/main.rs'), 'module') expect(viewLines(mod.children)).toEqual([ 'attribute 2', ' 属性: 2 个', ' allow(clippy::foo) | 编译检查(lint)级别设为放行:clippy::foo', ' deny(warnings) | 编译检查(lint)级别设为报错:warnings', ' 生效条件: unix 并且 非 (测试构建 或者 启用特性 mock)', ' 作用范围: 本模块', ]) }) it('crate 根文件:main.rs、lib.rs、src/bin 下的、包根 tests / examples / benches 里直接放的', () => { expect(['/p/src/main.rs', '/p/src/lib.rs', '/p/src/bin/tool.rs', '/p/tests/api.rs', 'C:\\p\\examples\\demo.rs'].map(isCrateRoot)).toEqual([true, true, true, true, true]) expect(['/p/src/credentials.rs', '/p/src/tests/helpers.rs', '/p/src/bin/tool/args.rs'].map(isCrateRoot)).toEqual([false, false, false]) }) })