From 86422e4671c86cf4cabb04e2b3d3edca248f79fd Mon Sep 17 00:00:00 2001 From: cruldra Date: Thu, 15 Oct 2026 17:07:07 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E9=A2=84=E8=A7=88=20Rust=20=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E4=BD=93=E6=8C=89=E8=AF=AD=E5=8F=A5=E6=8B=86=E6=88=90?= =?UTF-8?q?=E5=AD=90=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit let / let-else、if / else、match 各支、循环、隐式返回、闭包、unsafe / async 块照 Python / TypeScript 的约定拆块;特质同实现,方法卡挂在方法表那一行;函数卡去掉分情况表;Rust 语言插件给出调用结构 --- vscode/src/codeReader/languages/rust.test.ts | 360 ++++++++++++++++++- vscode/src/codeReader/languages/rust.ts | 274 ++++++++++++-- vscode/src/codeReader/rules/rustImpl.ts | 32 +- vscode/src/codeReader/rules/rustLibs.test.ts | 2 + vscode/src/codeReader/rules/rustRows.ts | 65 ++-- vscode/src/codeReader/rules/rustTree.ts | 68 +--- vscode/src/codeReader/translators/rustZh.ts | 16 +- 7 files changed, 682 insertions(+), 135 deletions(-) diff --git a/vscode/src/codeReader/languages/rust.test.ts b/vscode/src/codeReader/languages/rust.test.ts index 1b32617..67fb7f1 100644 --- a/vscode/src/codeReader/languages/rust.test.ts +++ b/vscode/src/codeReader/languages/rust.test.ts @@ -20,6 +20,10 @@ function views(source: string, fileName?: string): Promise { })) } +function tree(source: string): Promise { + return withBlocks(adapter, 'rust', source, outline) +} + function card(list: BlockView[], kind: string): BlockView { const found = list.find(block => block.kind === kind) if (!found) @@ -85,15 +89,31 @@ describe('rust 语言卡片', () => { ' 输入参数: 1 个', ' addr | SocketAddr', ' 返回: Result<(), StartError>', - ' 分情况: 2 支', - ' Ok(listener) | 记下监听地址', - ' Err(err) | 返回 StartError', - 'trait 41', + ' if 29: 匹配 result', + ' if 30: Ok(listener)', + ' statement 31: self.listener = Some(listener);', + ' 注释: 记下监听地址', + ' if 34: Err(err)', + ' return 35: 返回 Err(StartError);', + ' 注释: 返回 StartError', + 'rust.trait 41', ' 名称: Store', ' 说明: 按 id 读写代理。', ' 方法: 2 个', ' get | &self, id: Uuid | Result', ' save | &mut self, row: AgentRow | Result<(), StoreError>', + ' function 43', + ' 名称: get', + ' 参数: 2 个', + ' self | &self', + ' id | Uuid', + ' 返回: Result', + ' function 44', + ' 名称: save', + ' 参数: 2 个', + ' self | &mut self', + ' row | AgentRow', + ' 返回: Result<(), StoreError>', ]) }) @@ -171,6 +191,7 @@ describe('rust 语言卡片', () => { ' 名称: open', ' 可见性: 公开', ' 返回: u32', + ' return 8: 返回 LIMIT', ' function 10', ' 名称: bar', ' 说明: 外部。', @@ -189,6 +210,333 @@ describe('rust 语言卡片', () => { }) }) +describe('rust 函数体拆成子块', () => { + // 执行器的 device_resume(tauri 命令):let、let … else、if、隐式返回 + const RESUME = [ + '#[tauri::command]', + 'async fn device_resume(user_id: u64, app: AppHandle, state: State<\'_, AppState>) -> Result {', + ' let origin = web::api_origin();', + ' let Some(credential) = device::load(origin.as_str(), user_id) else {', + ' return Ok(ResumeReply::Register(device::registration()));', + ' };', + ' if state.linked.lock().unwrap().as_ref() != Some(&link) {', + ' start_connection(link, app, &state)?;', + ' }', + ' Ok(ResumeReply::Linked { device_id })', + '}', + ].join('\n') + + it('let → variable,let … else 的 else 是它的子块;if → if;带分号的调用、? → statement;末尾不带分号的式子是 return', async () => { + expect(await tree(RESUME)).toEqual([ + { at: [1, 2], kind: 'function', kids: [ + { at: 3, kind: 'variable' }, + { at: 4, kind: 'variable', kids: [{ at: 4, kind: 'else', kids: [{ at: 5, kind: 'return' }] }] }, + { at: 7, kind: 'if', kids: [{ at: 8, kind: 'statement' }] }, + { at: 10, kind: 'return' }, + ] }, + ]) + }) + + it('子块照常翻译:变量是属性表,else、条件、语句、返回各一行译文;隐式返回值前面补「返回」', async () => { + expect(viewLines(await views(RESUME))).toEqual([ + 'function 1', + ' 名称: device_resume', + ' 异步: 是', + ' 可见性: 本模块', + ' 输入参数: 3 个', + ' user_id | u64', + ' app | AppHandle', + ' state | State<\'_, AppState>', + ' 返回: Result', + ' variable 3', + ' 名称: origin', + ' 初始值: web::api_origin()', + ' variable 4', + ' 名称: Some(credential)', + ' 初始值: device::load(origin.as_str(), user_id)', + ' else 4: 否则', + ' return 5: 返回 Ok(ResumeReply::Register(device::registration()));', + ' if 7: 如果 state.linked.lock().unwrap().as_ref() != Some(&link)', + ' statement 8: start_connection(link, app, &state)?;', + ' return 10: 返回 Ok(ResumeReply::Linked { device_id })', + ]) + }) + + it('if / else if let / else 展平成兄弟块;match 一块、每一支一块,都是条件;排在最后的 if、match 各支里的值是 return', async () => { + const source = [ + 'fn pick(a: Option, b: bool) -> u8 {', + ' if b {', + ' log();', + ' } else if let Some(x) = a {', + ' x += 1;', + ' } else {', + ' reset();', + ' }', + ' match a {', + ' Some(v) if v > 0 => v,', + ' Some(_) => {', + ' warn();', + ' 0', + ' }', + ' None => return 1,', + ' }', + '}', + 'fn sign(x: i32) -> i32 {', + ' if x > 0 { 1 } else { -1 }', + '}', + ].join('\n') + expect(await tree(source)).toEqual([ + { at: 1, kind: 'function', kids: [ + { at: 2, kind: 'if', kids: [{ at: 3, kind: 'statement' }] }, + { at: 4, kind: 'else', kids: [{ at: 5, kind: 'statement' }] }, + { at: 6, kind: 'else', kids: [{ at: 7, kind: 'statement' }] }, + { at: 9, kind: 'if', kids: [ + { at: 10, kind: 'if', kids: [{ at: 10, kind: 'return' }] }, + { at: 11, kind: 'if', kids: [{ at: 12, kind: 'statement' }, { at: 13, kind: 'return' }] }, + { at: 15, kind: 'if', kids: [{ at: 15, kind: 'return' }] }, + ] }, + ] }, + { at: 18, kind: 'function', kids: [ + { at: 19, kind: 'if', kids: [{ at: 19, kind: 'return' }] }, + { at: 19, kind: 'else', kids: [{ at: 19, kind: 'return' }] }, + ] }, + ]) + expect(viewLines(await views(source)).filter(line => /^ {2,4}if|^ {2}else/.test(line))).toEqual([ + ' if 2: 如果 b', + ' else 4: 否则 如果 令 Some(x) = a', + ' else 6: 否则', + ' if 9: 匹配 a', + ' if 10: Some(v) 如果 v > 0', + ' if 11: Some(_)', + ' if 15: None', + ' if 19: 如果 x > 0', + ' else 19: 否则', + ]) + }) + + it('for / while let / loop → loop,循环体里最后那个式子不是返回值', async () => { + const source = [ + 'fn walk(items: Vec) {', + ' for item in items {', + ' handle(item)', + ' }', + ' while let Some(x) = next() {', + ' continue;', + ' }', + ' \'outer: loop {', + ' break \'outer;', + ' }', + '}', + ].join('\n') + expect(await tree(source)).toEqual([ + { at: 1, kind: 'function', kids: [ + { at: 2, kind: 'loop', kids: [{ at: 3, kind: 'statement' }] }, + { at: 5, kind: 'loop', kids: [{ at: 6, kind: 'statement' }] }, + { at: 8, kind: 'loop', kids: [{ at: 9, kind: 'statement' }] }, + ] }, + ]) + expect(viewLines(await views(source)).filter(line => /^ {2}loop/.test(line))).toEqual([ + ' loop 2: 遍历 item 在 items', + ' loop 5: 当 令 Some(x) = next()', + ' loop 8: \'outer: 循环', + ]) + }) + + it('闭包、嵌套 fn → function;unsafe、async、带标签的块是带子块的 statement,什么都没写的 {} 展平;实参里的闭包、async 块是语句的子块', async () => { + const source = [ + 'fn run() -> u8 {', + ' let add = |a: u8, b| -> u8 { a + b };', + ' let double = |x| x * 2;', + ' fn inner() -> u8 { 1 }', + ' unsafe {', + ' do_it();', + ' }', + ' {', + ' scoped();', + ' }', + ' \'done: {', + ' break \'done;', + ' }', + ' tokio::spawn(async move {', + ' work().await;', + ' });', + ' let task = async { 1 };', + ' items.iter().for_each(|x| {', + ' println!("{x}");', + ' });', + ' add(1, 2)', + '}', + ].join('\n') + expect(await tree(source)).toEqual([ + { at: 1, kind: 'function', kids: [ + { at: 2, kind: 'function', kids: [{ at: 2, kind: 'return' }] }, + { at: 3, kind: 'variable' }, + { at: 4, kind: 'function', kids: [{ at: 4, kind: 'return' }] }, + { at: 5, kind: 'statement', kids: [{ at: 6, kind: 'statement' }] }, + { at: 9, kind: 'statement' }, + { at: 11, kind: 'statement', kids: [{ at: 12, kind: 'statement' }] }, + { at: 14, kind: 'statement', kids: [{ at: 15, kind: 'statement' }] }, + { at: 17, kind: 'variable', kids: [{ at: 17, kind: 'return' }] }, + { at: 18, kind: 'statement', kids: [{ at: 19, kind: 'statement' }] }, + { at: 21, kind: 'return' }, + ] }, + ]) + expect(viewLines(await views(source))).toEqual([ + 'function 1', + ' 名称: run', + ' 可见性: 本模块', + ' 返回: u8', + ' function 2', + ' 名称: add', + ' 输入参数: 2 个', + ' a | u8', + ' b | —', + ' 返回: u8', + ' return 2: 返回 a + b', + ' variable 3', + ' 名称: double', + ' 初始值: |x| x * 2', + ' function 4', + ' 名称: inner', + ' 可见性: 本模块', + ' 返回: u8', + ' return 4: 返回 1', + ' statement 5: 不安全', + ' statement 6: do_it();', + ' statement 9: scoped();', + ' statement 11: \'done', + ' statement 12: 跳出 \'done;', + ' statement 14: tokio::spawn(异步 移动', + ' statement 15: work().等待;', + ' variable 17', + ' 名称: task', + ' 初始值: 异步', + ' return 17: 返回 1', + ' statement 18: items.iter().for_each(|x|', + ' statement 19: println!("{x}");', + ' return 21: 返回 add(1, 2)', + ]) + }) + + it('函数体里的注释同文件顶层:挂到后面那句上,行尾的不单独成块,最后的挂到前面那句', async () => { + const source = [ + 'fn f() {', + ' // 先取', + ' let a = 1;', + ' let b = 2; // 行尾', + ' // 最后的注释', + '}', + ].join('\n') + expect(await tree(source)).toEqual([ + { at: 1, kind: 'function', kids: [ + { at: 3, kind: 'variable', notes: [2] }, + { at: 4, kind: 'variable', notes: [5] }, + ] }, + ]) + }) + + it('特质里带默认实现的方法、实现里的方法都拆函数体;特质同实现,方法卡挂在方法表那一行;特质里、impl Trait for 里的方法跟着特质,没有可见性', async () => { + const source = [ + 'pub trait Greet {', + ' fn name(&self) -> String;', + ' fn hello(&self) -> String {', + ' format!("hi {}", self.name())', + ' }', + '}', + 'impl Greet for Bot {', + ' fn name(&self) -> String {', + ' let n = self.id;', + ' n.to_string()', + ' }', + '}', + ].join('\n') + expect(await tree(source)).toEqual([ + { at: 1, kind: 'trait', kids: [ + { at: 2, kind: 'function' }, + { at: 3, kind: 'function', kids: [{ at: 4, kind: 'return' }] }, + ] }, + { at: 7, kind: 'impl', kids: [ + { at: 8, kind: 'function', kids: [{ at: 9, kind: 'variable' }, { at: 10, kind: 'return' }] }, + ] }, + ]) + const list = await views(source) + expect(viewLines(list)).toEqual([ + 'rust.trait 1', + ' 名称: Greet', + ' 方法: 2 个', + ' name | &self | String', + ' hello | &self | String', + ' function 2', + ' 名称: name', + ' 参数: 1 个', + ' self | &self', + ' 返回: String', + ' function 3', + ' 名称: hello', + ' 参数: 1 个', + ' self | &self', + ' 返回: String', + ' return 4: 返回 format!("hi {}", self.name())', + 'rust.impl 7', + ' 名称: Bot 的方法', + ' 方法: 1 个', + ' name | &self | String', + ' function 8', + ' 名称: name', + ' 参数: 1 个', + ' self | &self', + ' 返回: String', + ' variable 9', + ' 名称: n', + ' 初始值: self.id', + ' return 10: 返回 n.to_string()', + ]) + expect(card(list, 'rust.trait').children.map(child => child.slot)).toEqual([{ row: 1, item: 0 }, { row: 1, item: 1 }]) + }) +}) + +describe('rust 调用', () => { + const calls = (source: string): Promise => nodeFacts(adapter, 'rust', source, node => adapter.call?.(node)) + + it('实参照源码顺序都是按位置传的,注释不算;被调用的取最后那个名字:路径、方法、泛型函数;被调用的不是名字时没有 name,宏调用不算', async () => { + const source = [ + 'fn f() {', + ' device::load(', + ' origin.as_str(), // 后端地址', + ' user_id,', + ' );', + ' parse::(s);', + ' handlers[k]();', + ' println!("{}", x);', + '}', + ].join('\n') + expect(await calls(source)).toEqual([ + { + callee: 'device::load', + name: 'load', + argumentList: '(\n origin.as_str(), // 后端地址\n user_id,\n )', + arguments: [{ kind: 'positional', value: 'origin.as_str()' }, { kind: 'positional', value: 'user_id' }], + }, + { callee: 'origin.as_str', name: 'as_str', argumentList: '()', arguments: [] }, + { callee: 'parse::', name: 'parse', argumentList: '(s)', arguments: [{ kind: 'positional', value: 's' }] }, + { callee: 'handlers[k]', argumentList: '()', arguments: [] }, + ]) + }) + + it('跨几行写的调用排成一行「调用 … · N 个参数」,实参摊成表', async () => { + const source = 'fn f() {\n device::load(\n origin,\n user_id,\n );\n}\n' + expect(viewLines(await views(source))).toEqual([ + 'function 1', + ' 名称: f', + ' 可见性: 本模块', + ' 返回: ()', + ' statement 2: 调用 device::load · 2 个参数;', + ' 第 1 个 | origin', + ' 第 2 个 | user_id', + ]) + }) +}) + describe('rust 字面量的各项', () => { it('数组、元组的各项照源码顺序,注释不算一项;[值; 个数]、别的值不是', async () => { const source = 'const A: [&str; 2] = [\n "a", // 第一个\n "b",\n];\nconst Z: [u8; 4] = [0; 4];\nconst T: (u8, u8) = (1, 2);\nfn f() { g(a, b); }' @@ -401,8 +749,6 @@ describe('rust 编译属性', () => { }) describe('rust 注释挂到它讲的项上', () => { - const tree = (source: string): Promise => withBlocks(adapter, 'rust', source, outline) - it('同一层后面最近的那项(隔着空行也算),和 /// 说明并存;只有注释的 mod 挂到 mod 上;impl 最后的挂到前面那项;文件最后的挂到最后一项;//! 是文件的说明', async () => { const source = [ '//! 凭证存储。', @@ -429,7 +775,7 @@ describe('rust 注释挂到它讲的项上', () => { expect(await tree(source)).toEqual([ { at: 1, kind: 'comment' }, { at: 3, kind: 'import' }, - { at: [7, 8], kind: 'function', notes: [5] }, + { at: [7, 8], kind: 'function', notes: [5], kids: [{ at: 8, kind: 'return' }] }, { at: 10, kind: 'module', notes: [11] }, { at: 14, kind: 'impl', notes: [20], kids: [{ at: 16, kind: 'function', notes: [15, 17] }] }, ]) diff --git a/vscode/src/codeReader/languages/rust.ts b/vscode/src/codeReader/languages/rust.ts index f2e73fa..730b4c3 100644 --- a/vscode/src/codeReader/languages/rust.ts +++ b/vscode/src/codeReader/languages/rust.ts @@ -1,5 +1,5 @@ -import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract' -import type { CommentRules } from './shared' +import type { Access, AttributeFacts, AttributeItem, BlockShape, CallFacts, CfgPredicate, ClassFacts, CodeBlock, CollectionFacts, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TextKind, TokenRole, TraitBlock } from '../contract' +import type { CommentRules, ListBlocks } from './shared' import * as path from 'node:path' import { leadingAttributes, visibilityFacts } from '../rules/rustTree' import { attachComments, children, compoundShape, extendHeader, headerSpan, insideOf, isCommentNode, leafBlock, leafShape, listBlocks, named, syntaxChildren } from './shared' @@ -15,10 +15,33 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter { tokenRole: rustTokenRole, lineComment: '//', freshDoc, + call: rustCall, collection: rustCollection, } } +/** + * 调用 `f(a)`、`obj.method(a)`、`Type::new(a)`、`parse::(s)` 的结构,实参都是按位置传的; + * 元组结构体、变体(`Some(x)`)写法和调用一样,不给 construct,翻译插件问名字的定义是哪种块。 + * 宏调用(`println!(…)`)括号里不是语法树,不算。 + */ +function rustCall(node: Node): CallFacts | undefined { + const callee = node.type === 'call_expression' ? node.childForFieldName('function') : null + const list = node.childForFieldName('arguments') + if (!callee || list?.type !== 'arguments') + return undefined + const target = callee.type === 'generic_function' ? callee.childForFieldName('function') : callee + const name = target?.type === 'identifier' + ? target + : target?.type === 'field_expression' ? target.childForFieldName('field') : target?.type === 'scoped_identifier' ? target.childForFieldName('name') : null + return { + callee, + ...(name && { name }), + argumentList: list, + arguments: syntaxChildren(list).filter(arg => arg.type !== 'attribute_item').map(value => ({ kind: 'positional', value })), + } +} + /** 数组 `[a, b]`、元组 `(a, b)` 的各项;`[值; 个数]` 不是一项项写出来的,别的节点也没有。项前面的 `#[…]` 不算一项。 */ function rustCollection(node: Node): CollectionFacts | undefined { const listed = node.type === 'tuple_expression' || (node.type === 'array_expression' && !node.childForFieldName('length')) @@ -62,7 +85,7 @@ function docMarker(comment: Node): string { return named(comment).find(child => child.type === 'outer_doc_comment_marker' || child.type === 'inner_doc_comment_marker')?.text ?? '' } -/** 一层的各项(文件顶层、模块身体、impl 身体)→ 块;afterRow 同 listBlocks。`#[…]` 和多出来的空 `;` 不出块。 */ +/** 一层的各项(文件顶层,模块、特质、实现的身体)→ 块;afterRow 同 listBlocks。`#[…]` 和多出来的空 `;` 不出块。 */ function topBlocks(items: Node[], afterRow: number): CodeBlock[] { return listBlocks(items.flatMap(unwrapExtern), afterRow, (item) => { if (item.type === 'attribute_item' || item.type === 'empty_statement') @@ -107,7 +130,7 @@ function moduleBlock(node: Node): CodeBlock { const { access, attributes } = visibilityFacts(node) return { kind: 'module', - ...compoundShape(node, body.startIndex, named(body), topBlocks), + ...itemsShape(node), ...(attributes && { attributes }), facts: classFacts(node, access), } @@ -323,29 +346,30 @@ function enumBlock(node: Node): EnumBlock { function traitBlock(node: Node): TraitBlock { const { access, attributes } = visibilityFacts(node) - return { kind: 'trait', ...shape(node), ...(attributes && { attributes }), facts: classFacts(node, access) } + return { kind: 'trait', ...itemsShape(node), ...(attributes && { attributes }), facts: classFacts(node, access) } } function implBlock(node: Node): ImplBlock { const { access, attributes } = visibilityFacts(node) - const body = node.childForFieldName('body') return { kind: 'impl', - ...compoundShape(node, body?.startIndex ?? node.endIndex, named(body), topBlocks), + ...itemsShape(node), ...(attributes && { attributes }), facts: classFacts(node, access, implType(node)), } } +/** 身体是一层项的块(模块、特质、实现):子块是身体里的各项,跟文件顶层同一条路。 */ +function itemsShape(node: Node): BlockShape { + const body = node.childForFieldName('body') + return compoundShape(node, body?.startIndex ?? node.endIndex, named(body), topBlocks) +} + function functionBlock(node: Node): FunctionBlock { const { access, attributes } = visibilityFacts(node) - const body = node.childForFieldName('body') - const headerEnd = body?.startIndex ?? node.endIndex return { kind: 'function', - node, - header: headerSpan(node, headerEnd), - children: [], + ...bodyShape(node, node.childForFieldName('body'), true), ...(attributes && { attributes }), facts: { name: node.childForFieldName('name') ?? undefined, @@ -358,6 +382,213 @@ function functionBlock(node: Node): FunctionBlock { } } +/** + * 块里写的声明(项、`use`、`macro_rules!`……),跟文件顶层的项走同一条路([itemBlock])。 + * 块里别的具名子节点是 `let`、表达式语句,和排在最后、不带分号的那个式子(这一层的值)。 + */ +const DECLARATIONS = new Set([ + 'const_item', + 'static_item', + 'use_declaration', + 'extern_crate_declaration', + 'macro_definition', + 'inner_attribute_item', + 'mod_item', + 'foreign_mod_item', + 'struct_item', + 'union_item', + 'enum_item', + 'type_item', + 'function_item', + 'function_signature_item', + 'impl_item', + 'trait_item', + 'associated_type', +]) + +/** + * 带主体的一块:主体是 `{}` 时子块来自里面的各句,否则主体本身是唯一的子块(match 的一支 `Ok(v) => v`)。 + * 头部到 `{` 之前,带标签的 `'a: {}` 标签算在头部里。tail 见 [statements]。 + */ +function bodyShape(outer: Node, body: Node | null, tail: boolean): BlockShape { + if (!body) + return leafShape(outer) + const list: ListBlocks = (items, afterRow) => statements(items, afterRow, tail) + if (body.type !== 'block') + return compoundShape(outer, body.startIndex, [body], list) + const open = children(body).find(child => child.type === '{') + return compoundShape(outer, open?.startIndex ?? body.startIndex, blockItems(body), list) +} + +function bodyBlock(kind: TextKind, outer: Node, body: Node | null, tail: boolean): CodeBlock { + return { kind, ...bodyShape(outer, body, tail) } +} + +/** `{}` 里的各句和注释;`'a: {}` 的标签不算。 */ +function blockItems(block: Node): Node[] { + return named(block).filter(child => child.type !== 'label') +} + +/** + * 函数体里的一层(函数体、条件的一支、match 的一支、循环体……)→ 块,注释同文件顶层。 + * tail 是这一层算出的值就是所在函数的返回值:函数体、闭包体、async 块是;排在最后、不带分号的条件、match、`{}` 把它传给自己的各支; + * 循环体、`let … else` 的 else 不是。这时最后那个不带分号的式子是 return。 + */ +function statements(items: Node[], afterRow: number, tail: boolean): CodeBlock[] { + const list = items.flatMap(unwrapExtern) + const last = list.filter(item => !isCommentNode(item)).at(-1) + return listBlocks(list, afterRow, item => + statementBlocks(item, tail && item === last).map((block, i) => i === 0 ? withAttributes(block) : block), COMMENTS) +} + +/** 一句 → 块;tail 是这句排在 tail 的那一层最后。`#[…]` 算进后面那句的头部,自己和多出来的空 `;` 不出块。 */ +function statementBlocks(item: Node, tail: boolean): CodeBlock[] { + switch (item.type) { + case 'attribute_item': + case 'empty_statement': + return [] + case 'let_declaration': + return [letBlock(item)] + case 'expression_statement': { + const [expression] = syntaxChildren(item) + // 带分号的式子算出的值扔掉了,不带分号的(条件、match、`{}`)排在最后才是这一层的值 + return expression ? expressionBlocks(item, expression, tail && item.lastChild?.type !== ';') : [leafBlock('statement', item)] + } + default: + return DECLARATIONS.has(item.type) ? [itemBlock(item)] : expressionBlocks(item, item, tail) + } +} + +/** + * 当一句写的式子 → 块:outer 是整句(表达式语句,或一层最后那个不带分号的式子本身)。 + * 条件、match、循环、`{}` 照控制流拆;是这一层的值(tail)时是 return;别的是 statement,带着闭包、async 块时子块来自它(见 [callbackBody])。 + */ +function expressionBlocks(outer: Node, expression: Node, tail: boolean): CodeBlock[] { + switch (expression.type) { + case 'if_expression': + return ifBlocks(expression, outer, 'if', tail) + case 'match_expression': + return [matchBlock(outer, expression, tail)] + case 'for_expression': + case 'while_expression': + case 'loop_expression': + return [bodyBlock('loop', outer, expression.childForFieldName('body'), false)] + case 'block': + // 前面什么都没写的 `{}` 同 TypeScript 展平成各句;带标签的标签是头部 + return named(expression).some(child => child.type === 'label') + ? [bodyBlock('statement', outer, expression, tail)] + : statements(blockItems(expression), expression.startPosition.row, tail) + case 'unsafe_block': + return [bodyBlock('statement', outer, named(expression).find(child => child.type === 'block') ?? null, tail)] + case 'async_block': + return [bodyBlock('statement', outer, deferredBody(expression), true)] + case 'return_expression': + return [leafBlock('return', outer)] + default: + return [tail ? leafBlock('return', outer) : bodyBlock('statement', outer, callbackBody(expression), true)] + } +} + +/** if 的 else / else if 展平成紧跟其后的兄弟块,同 TypeScript;`if let` 的头部连着模式。 */ +function ifBlocks(ifNode: Node, outer: Node, kind: 'if' | 'else', tail: boolean): CodeBlock[] { + const blocks = [bodyBlock(kind, outer, ifNode.childForFieldName('consequence'), tail)] + const alternative = ifNode.childForFieldName('alternative') + if (alternative) { + const [inner = null] = syntaxChildren(alternative) + blocks.push(...(inner?.type === 'if_expression' ? ifBlocks(inner, alternative, 'else', tail) : [bodyBlock('else', alternative, inner, tail)])) + } + return blocks +} + +/** match 是一块条件,每一支是它的子块,也是条件,同 Python 的 match / case、TypeScript 的 switch / case。 */ +function matchBlock(outer: Node, match: Node, tail: boolean): CodeBlock { + const body = match.childForFieldName('body') + if (!body) + return leafBlock('if', outer) + const arms: ListBlocks = (list, afterRow) => listBlocks(list, afterRow, arm => arm.type === 'match_arm' ? [armBlock(arm, tail)] : [], COMMENTS) + return { kind: 'if', ...compoundShape(outer, body.startIndex, named(body), arms) } +} + +/** match 的一支:头部是模式和守卫(到 `=>` 之前),子块来自 `=>` 后面的值。 */ +function armBlock(arm: Node, tail: boolean): CodeBlock { + const shape = bodyShape(arm, arm.childForFieldName('value'), tail) + const arrow = children(arm).find(child => child.type === '=>') + return { kind: 'if', ...shape, header: arrow ? headerSpan(arm, arrow.startIndex) : shape.header } +} + +/** + * `let` → variable,模式是名字;值是主体写成 `{}` 的闭包时是 function,同 TypeScript 把箭头函数赋给变量。 + * `let … else` 的 else 是它的子块;值带着闭包、async 块时子块来自它(见 [callbackBody])。 + */ +function letBlock(node: Node): CodeBlock { + const pattern = node.childForFieldName('pattern') + if (!pattern) + return leafBlock('statement', node) + const value = node.childForFieldName('value') + if (value?.type === 'closure_expression' && deferredBody(value)) + return closureBlock(node, value, pattern) + const alternative = node.childForFieldName('alternative') + return { + kind: 'variable', + ...(alternative ? letElseShape(node, alternative) : bodyShape(node, value && callbackBody(value), true)), + facts: { declarators: [{ name: pattern, type: node.childForFieldName('type') ?? undefined, value: value ?? undefined }] }, + } +} + +/** + * `let 模式 = 值 else { … }`:头部到 else 之前,else 照 if / else 的写法成一块,是唯一的子块。 + * else 的节点仍是整条 `let`(else 这个词不在别的节点里);那一支必须跳出去(return、break……),里面最后那个式子不算返回值。 + */ +function letElseShape(node: Node, alternative: Node): BlockShape { + const keyword = children(node).find(child => child.type === 'else') + if (!keyword) + return leafShape(node) + const branch = bodyShape(node, alternative, false) + const otherwise: CodeBlock = { kind: 'else', ...branch, header: { ...branch.header, startIndex: keyword.startIndex, startRow: keyword.startPosition.row } } + return { node, header: headerSpan(node, keyword.startIndex), children: [otherwise] } +} + +/** 赋给变量、主体写成 `{}` 的闭包 → function:outer 是整条 `let`,名字是它的模式;闭包没有可见性、外部属性。 */ +function closureBlock(outer: Node, closure: Node, name: Node): FunctionBlock { + return { + kind: 'function', + ...bodyShape(outer, deferredBody(closure), true), + facts: { + name, + ...(isAsync(closure) && { async: true }), + decorators: [], + parameters: parametersOf(closure.childForFieldName('parameters')), + returnType: closure.childForFieldName('return_type') ?? undefined, + }, + } +} + +/** 闭包、async 块的 `{}`:它们的值是里面最后那个式子,里面的 return 也只回到它们自己。闭包主体不是 `{}` 时没有。 */ +function deferredBody(node: Node): Node | null { + const body = node.type === 'closure_expression' + ? node.childForFieldName('body') + : node.type === 'async_block' ? named(node).find(child => child.type === 'block') : undefined + return body?.type === 'block' ? body : null +} + +/** + * 式子带着的闭包、async 块的 `{}`,同 TypeScript 的回调:值本身是它(`async { … }`),或调用的实参里第一个 + * (`tokio::spawn(async move { … })`、`.for_each(|x| { … })`);`.await`、`?` 看里面那个式子。 + */ +function callbackBody(expression: Node): Node | null { + let inner: Node | null = expression + while (inner?.type === 'await_expression' || inner?.type === 'try_expression') + inner = inner.firstNamedChild + if (inner?.type !== 'call_expression') + return inner && deferredBody(inner) + for (const arg of named(inner.childForFieldName('arguments'))) { + const body = deferredBody(arg) + if (body) + return body + } + return null +} + function classFacts(node: Node, access: Access | undefined, name = node.childForFieldName('name') ?? undefined): ClassFacts { return { name, access, decorators: leadingAttributes(node), typeParameters: [], bases: [], implements: [] } } @@ -375,22 +606,25 @@ function shape(node: Node): { node: Node, header: { startIndex: number, endIndex function parametersOf(list: Node | null): ParameterFacts[] { if (!list) return [] - return named(list).flatMap((child) => { + return syntaxChildren(list).flatMap((child) => { if (child.type === 'self_parameter') { const name = [...child.children].reverse().find(token => token?.type === 'self') return name ? [{ name, optional: false, type: child }] : [] } - if (child.type !== 'parameter') - return [] - const name = child.childForFieldName('pattern') - const type = child.childForFieldName('type') ?? undefined - return name ? [{ name, optional: false, type }] : [] + if (child.type === 'parameter') { + const name = child.childForFieldName('pattern') + const type = child.childForFieldName('type') ?? undefined + return name ? [{ name, optional: false, type }] : [] + } + // 闭包不写类型的参数(`|x|`)只是一个模式 + return list.type === 'closure_parameters' ? [{ name: child, optional: false }] : [] }) } +/** 写了 `async`:`fn` 的写在 function_modifiers 里,闭包的直接写在前面。 */ function isAsync(node: Node): boolean { - const modifiers = named(node).find(child => child.type === 'function_modifiers') - return modifiers?.children.some(child => child?.type === 'async') ?? false + const modifiers = named(node).find(child => child.type === 'function_modifiers') ?? node + return modifiers.children.some(child => child?.type === 'async') } const insideComment = insideOf({ line_comment: true, block_comment: true }) diff --git a/vscode/src/codeReader/rules/rustImpl.ts b/vscode/src/codeReader/rules/rustImpl.ts index 185462d..5180591 100644 --- a/vscode/src/codeReader/rules/rustImpl.ts +++ b/vscode/src/codeReader/rules/rustImpl.ts @@ -1,33 +1,41 @@ -import type { ImplBlock, KindBadge, LibraryRule, RuleCard, RuleContext, ThemeColor } from '../contract' -import { implRows, plainSay } from './rustRows' +import type { ImplBlock, KindBadge, LibraryRule, PropertyRow, RuleCard, TraitBlock } from '../contract' +import { implRows, plainSay, traitRows } from './rustRows' /** 和函数卡同一个黄,实现块看起来仍是函数那一挂。 */ -const COLOR: ThemeColor = { dark: '#dcdcaa', light: '#795e26' } -const BADGE: KindBadge = { label: '实现', color: COLOR, icon: 'square-function' } +const IMPL: KindBadge = { label: '实现', color: { dark: '#dcdcaa', light: '#795e26' }, icon: 'square-function' } +/** 和声明关键字同一个蓝,同特质这个种类在预览里的颜色。 */ +const TRAIT: KindBadge = { label: '特质', color: { dark: '#569cd6', light: '#0000ff' }, icon: 'network' } /** - * impl 的方法表:点方法名展开那张函数卡。 + * impl、trait 的方法表:点方法名展开那张函数卡,函数体各句在卡里。 * 方法挂在方法表的子行上(RuleSlot)。不看导入,Rust 文件都启用。 */ export const rustImplRule: LibraryRule = { id: 'rust', - label: 'Rust 实现', + label: 'Rust 实现与特质', languageIds: ['rust'], libraries: [], - claim: (block, context) => block.kind === 'impl' ? implCard(block, context) : undefined, + claim: (block, context) => { + const say = plainSay(context.doc) + if (block.kind === 'impl') + return methodsCard(block, 'rust.impl', IMPL, implRows(block, say)) + if (block.kind === 'trait') + return methodsCard(block, 'rust.trait', TRAIT, traitRows(block, say)) + return undefined + }, attributeNames: () => ({ attributes: {}, - values: { kind: { 'rust.impl': '实现' } }, + values: { kind: { 'rust.impl': '实现', 'rust.trait': '特质' } }, }), } -function implCard(block: ImplBlock, context: RuleContext): RuleCard { - const rows = implRows(block, plainSay(context.doc)) +/** 带方法表的卡片:子块里的每个函数挂在「方法」那一行讲它的子行上,跟 rows 里的顺序一一对上。 */ +function methodsCard(block: ImplBlock | TraitBlock, kind: string, badge: KindBadge, rows: PropertyRow[]): RuleCard { const row = rows.findIndex(item => item.label === '方法') const methods = block.children.filter(child => child.kind === 'function') return { - kind: 'rust.impl', - badge: BADGE, + kind, + badge, rows, ...(block.facts.name && { defines: { startIndex: block.facts.name.startIndex, endIndex: block.facts.name.endIndex } }), absorbs: [], diff --git a/vscode/src/codeReader/rules/rustLibs.test.ts b/vscode/src/codeReader/rules/rustLibs.test.ts index 69838a9..1926b16 100644 --- a/vscode/src/codeReader/rules/rustLibs.test.ts +++ b/vscode/src/codeReader/rules/rustLibs.test.ts @@ -65,6 +65,7 @@ describe('serde 与 thiserror', () => { ' 路径 | /agents/{id}', ' 路径参数 | id: Uuid', ' 返回: Result, StatusCode>', + ' return 32: 返回 todo!()', 'axum.route 35', ' 名称: health', ' 请求: GET /health', @@ -92,6 +93,7 @@ describe('serde 与 thiserror', () => { ' state | State | 窗口状态', ' name | String | 调用方传入', ' 返回: Result', + ' return 47: 返回 todo!()', ]) }) diff --git a/vscode/src/codeReader/rules/rustRows.ts b/vscode/src/codeReader/rules/rustRows.ts index a15a9f5..6a85f3c 100644 --- a/vscode/src/codeReader/rules/rustRows.ts +++ b/vscode/src/codeReader/rules/rustRows.ts @@ -1,7 +1,7 @@ import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, SourceSpan, TokenRole, TraitBlock } from '../contract' import type { RustVisibility } from './rustTree' import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows' -import { bodyFunctions, docSummary, isMethod, matchArms, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree' +import { docSummary, isFnItem, isMethod, oneLine, returnText, signatureParams, structFields, variantPayload } from './rustTree' /** 把类型、名字画成原文,读说明。翻译插件传入时,名字带源码下标。 */ export interface RustSay { @@ -182,10 +182,14 @@ export function enumRows(block: EnumBlock, say: RustSay): PropertyRow[] { ] } +/** + * 函数卡片的行;函数体各句是子块,不在这里讲。有可见性才有那一行(特质里、`impl Trait for` 里的方法和闭包没有); + * 没写返回类型的 `fn` 项返回 `()`,闭包没写时不知道,不出「返回」这一行。 + */ export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] { - const arms = matchArms(block.node) + const visible = block.facts.access ?? block.attributes?.access const doc = block.facts.doc && say.doc(block.facts.doc) - const { parameters } = block.facts + const { parameters, returnType } = block.facts const docs = paramDocs(doc, parameters.map(param => param.name.text)) const described = parameters.some(param => docs.of(param.name.text)) return [ @@ -193,7 +197,7 @@ export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] ...docRow(doc), ...unmatchedParamRows(docs.unmatched), ...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []), - visibilityRow(block), + ...(visible ? [visibilityRow(block)] : []), ...tableRow(isMethod(block.node) ? '参数' : '输入参数', ['名称', '类型', ...(described ? ['说明'] : [])], parameters.map(param => ({ cells: [ say.name(param.name, 'variable'), @@ -202,55 +206,46 @@ export function functionRows(block: FunctionBlock, say: RustSay): PropertyRow[] ], defines: range(param.name), }))), - { label: '返回', value: [plain(returnText(block.node))], ...returnNote(doc) }, + ...(returnType || isFnItem(block.node) ? [{ label: '返回', value: [plain(returnType ? oneLine(returnType.text) : '()')], ...returnNote(doc) }] : []), ...throwsRows(doc), - ...tableRow('分情况', ['分支', '做什么'], arms.map(arm => ({ - cells: [[plain(arm.pattern)], [hint(arm.does)]], - })), '支'), ] } export function traitRows(block: TraitBlock, say: RustSay): PropertyRow[] { - const methods = bodyFunctions(block.node) return [ nameRow(block.facts.name, say, 'type'), ...docRow(block.facts.doc && say.doc(block.facts.doc)), - ...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => { - const name = method.childForFieldName('name') - return { - cells: [ - name ? say.name(name, 'function') : [hint('匿名')], - [plain(signatureParams(method))], - [plain(returnText(method))], - ], - ...(name && { defines: range(name) }), - } - })), + ...methodRows(block, say), ] } export function implRows(block: ImplBlock, say: RustSay): PropertyRow[] { - const methods = block.children.filter((child): child is FunctionBlock => child.kind === 'function') const title: Inline[] = block.facts.name ? [...say.name(block.facts.name, 'type'), plain(' 的方法')] : [hint('实现')] return [ { label: '名称', value: title, ...(block.facts.name && { defines: range(block.facts.name) }) }, ...docRow(block.facts.doc && say.doc(block.facts.doc)), - ...tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => { - const name = method.facts.name - return { - cells: [ - name ? say.name(name, 'function') : [hint('匿名')], - [plain(signatureParams(method.node))], - [plain(returnText(method.node))], - ], - ...(name && { defines: range(name) }), - } - })), + ...methodRows(block, say), ] } +/** 特质、实现的方法表「名称 | 参数 | 返回」:子块里的函数每个一行,照源码顺序;[Rust 实现与特质规则](vscode/src/codeReader/rules/rustImpl.ts#rustImplRule) 把那张函数卡挂在这一行上。 */ +function methodRows(block: TraitBlock | ImplBlock, say: RustSay): PropertyRow[] { + const methods = block.children.filter((child): child is FunctionBlock => child.kind === 'function') + return tableRow('方法', ['名称', '参数', '返回'], methods.map((method) => { + const name = method.facts.name + return { + cells: [ + name ? say.name(name, 'function') : [hint('匿名')], + [plain(signatureParams(method.node))], + [plain(returnText(method.node))], + ], + ...(name && { defines: range(name) }), + } + })) +} + function nameRow(name: Node | undefined, say: RustSay, role: TokenRole): PropertyRow { return { label: '名称', @@ -268,11 +263,11 @@ function visibilityRow(block: CodeBlock): PropertyRow { return { label: '可见性', value: [keyword(visibilityWord(block))] } } -/** 没有行就不占一行。countUnit 默认「个」,分情况用「支」。 */ -function tableRow(label: string, columns: string[], items: PropertyItem[], countUnit = '个'): PropertyRow[] { +/** 没有行就不占一行。 */ +function tableRow(label: string, columns: string[], items: PropertyItem[]): PropertyRow[] { if (items.length === 0) return [] - return [{ label, value: [hint(`${items.length} ${countUnit}`)], columns, items }] + return [{ label, value: [hint(`${items.length} 个`)], columns, items }] } function hint(text: string): Inline { diff --git a/vscode/src/codeReader/rules/rustTree.ts b/vscode/src/codeReader/rules/rustTree.ts index 2c79928..079419f 100644 --- a/vscode/src/codeReader/rules/rustTree.ts +++ b/vscode/src/codeReader/rules/rustTree.ts @@ -61,6 +61,10 @@ export function rustVisibility(node: Node): RustVisibility { /** 公开写在 facts.access 上;其余写在块的 access 属性上,盖过 facts。 */ export function visibilityFacts(node: Node): { access?: Access, attributes?: BlockAttributes } { + // 特质里的项、给类型实现特质(`impl Trait for Type`)里的项不能写可见性,跟着特质 + const owner = node.parent?.parent + if (owner?.type === 'trait_item' || (owner?.type === 'impl_item' && owner.childForFieldName('trait'))) + return {} const visibility = rustVisibility(node) return visibility === 'public' ? { access: 'public' } : { attributes: { access: visibility } } } @@ -79,26 +83,6 @@ export function returnText(fn: Node): string { return returned ? oneLine(returned.text) : '()' } -/** 函数体里直接写的 match,每一支的模式和「做什么」。 */ -export function matchArms(fn: Node): { pattern: string, does: string }[] { - const body = fn.childForFieldName('body') - if (!body) - return [] - const arms: { pattern: string, does: string }[] = [] - for (const statement of named(body)) { - const expr = statement.type === 'expression_statement' ? named(statement)[0] : statement - if (expr?.type !== 'match_expression') - continue - const block = expr.childForFieldName('body') - for (const arm of named(block).filter(child => child.type === 'match_arm')) { - const pattern = arm.childForFieldName('pattern') - if (pattern) - arms.push({ pattern: oneLine(pattern.text), does: armDoes(arm) }) - } - } - return arms -} - /** 变体携带的数据:没带是空的,元组变体是类型,结构变体是字段原文。body 是变体的 body。 */ export function variantPayload(body: Node | undefined): string | undefined { if (!body) @@ -180,6 +164,11 @@ export function isMethod(node: Node): boolean { return parent?.type === 'declaration_list' && (parent.parent?.type === 'impl_item' || parent.parent?.type === 'trait_item') } +/** 是 `fn` 项(含 extern 块里只写签名的),不是赋给变量的闭包:项没写返回类型就是返回 `()`。 */ +export function isFnItem(node: Node): boolean { + return node.type === 'function_item' || node.type === 'function_signature_item' +} + /** 结构体的字段:名字、类型、可见性。元组结构体没有名字,不进这张表。 */ export function structFields(node: Node): { name: Node, type: Node, visibility: RustVisibility }[] { const body = node.childForFieldName('body') @@ -193,42 +182,3 @@ export function structFields(node: Node): { name: Node, type: Node, visibility: return name && type ? [{ name, type, visibility: rustVisibility(field) }] : [] }) } - -/** trait 或 impl 主体里的函数(含只写签名的)。 */ -export function bodyFunctions(node: Node): Node[] { - const body = node.childForFieldName('body') - return named(body).filter(child => child.type === 'function_item' || child.type === 'function_signature_item') -} -function armDoes(arm: Node): string { - const comment = firstComment(arm) - if (comment) - return firstSentence(commentPlain(comment)) || '—' - const value = arm.childForFieldName('value') - if (!value) - return '—' - if (value.type === 'return_expression') { - const returned = named(value)[0] - return returned ? `返回 ${oneLine(returned.text)}` : '返回' - } - if (value.type === 'block') { - const inner = firstComment(value) - if (inner) - return firstSentence(commentPlain(inner)) || '—' - const statement = named(value)[0] - return statement ? oneLine(statement.text) : '—' - } - return oneLine(value.text) -} - -function firstComment(node: Node): Node | undefined { - if (node.type === 'line_comment' || node.type === 'block_comment') - return node - for (const child of node.children) { - if (!child) - continue - const found = firstComment(child) - if (found) - return found - } - return undefined -} diff --git a/vscode/src/codeReader/translators/rustZh.ts b/vscode/src/codeReader/translators/rustZh.ts index 9358a8e..816670d 100644 --- a/vscode/src/codeReader/translators/rustZh.ts +++ b/vscode/src/codeReader/translators/rustZh.ts @@ -1,4 +1,4 @@ -import type { FactBlock, Inline, Node, PropertyRow, SourceDocument, SourceSpan, Translator } from '../contract' +import type { FactBlock, Inline, Node, PropertyRow, SourceDocument, SourceSpan, TextBlock, TextTranslation, Translator } from '../contract' import { attributeRows, enumRows, functionRows, implRows, importRows, moduleRows, plainSay, structRows, traitRows } from '../rules/rustRows' import { isCrateRoot } from '../rules/rustTree' import { createZhTemplateTranslator, KIND_ZH, ZH_ATTRIBUTE_NAMES } from './zhTemplate' @@ -58,7 +58,7 @@ const template = createZhTemplateTranslator({ }) /** - * Rust 的中文直译。属性表的用词按代码阅读的 Rust 原型:结构体、可见性、变体、分情况。 + * Rust 的中文直译。属性表的用词按代码阅读的 Rust 原型:结构体、可见性、变体。 * 关键字和注释仍走中文直译那一套。常量和变量的行不另写一套,走模板里现成的那套。 */ export const rustZhTranslator: Translator = { @@ -77,9 +77,21 @@ export const rustZhTranslator: Translator = { }, }, }, + translate, describe, } +/** 写了 `return` 的返回块:`return x;` 整句,或 match 一支的值 `=> return x`。 */ +const RETURN_WRITTEN = new Set(['expression_statement', 'return_expression']) + +/** 一层最后那个不带分号的式子是返回值,源码里没有 return 这个词,译文照写了的那样在前面补「返回」。 */ +function translate(block: TextBlock, document: SourceDocument): TextTranslation { + const translation = template.translate(block, document) + if (block.kind !== 'return' || RETURN_WRITTEN.has(block.node.type) || !Array.isArray(translation.text)) + return translation + return { ...translation, text: [{ text: '返回', role: 'keyword' }, { text: ' ', role: 'plain' }, ...translation.text] } +} + function describe(block: FactBlock, document: SourceDocument): PropertyRow[] { const say = plainSay(node => template.doc(node, document)) switch (block.kind) {