378 lines
14 KiB
TypeScript
378 lines
14 KiB
TypeScript
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<BlockView[]> {
|
|
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<AgentRow, StoreError>',
|
|
' 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<String> { 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<String> { 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<String> — 没存过时是 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])
|
|
})
|
|
})
|