✨ feat(vscode): 代码阅读认 serde 和 thiserror

This commit is contained in:
cruldra committed 2026-10-04 14:28:41 +08:00
1 parent 6d58b681cf
commit 44de0a1f70
6 files changed
+354

No files matched your search

@@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
/// 桌面端列表里的一行代理。
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentRow {
#[serde(rename = "agent_id")]
pub id: Uuid,
pub name: String,
#[serde(skip)]
secret: String,
}
use thiserror::Error;
/// 读写代理时出的错。
#[derive(Error, Debug)]
pub enum AgentError {
/// 这个 id 没有代理
#[error("agent {id} 不存在")]
NotFound { id: Uuid },
/// 调用上游失败
#[error("上游错误")]
Upstream(#[from] Other),
}
+4
View File
@@ -35,7 +35,9 @@ import { fastapiRule } from './rules/fastapi'
import { pydanticRule } from './rules/pydantic'
import { reactRule } from './rules/react'
import { rustImplRule } from './rules/rustImpl'
import { serdeRule } from './rules/serde'
import { sqlalchemyRule } from './rules/sqlalchemy'
import { thiserrorRule } from './rules/thiserror'
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
import { bashZhTranslator } from './translators/bashZh'
import { dockerfileZhTranslator } from './translators/dockerfileZh'
@@ -165,6 +167,8 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
api.registerLibraryRule(fastapiRule),
api.registerLibraryRule(reactRule),
api.registerLibraryRule(rustImplRule),
api.registerLibraryRule(serdeRule),
api.registerLibraryRule(thiserrorRule),
)
return api
}
+2
View File
@@ -134,6 +134,8 @@ export function deriveNames(node: Node): string[] {
for (const name of named(tree)) {
if (name.type === 'identifier')
names.push(name.text)
else if (name.type === 'scoped_identifier')
names.push(name.childForFieldName('name')?.text ?? name.text)
}
}
return names
@@ -0,0 +1,65 @@
import type { BlockView } from '../blockView'
import { describe, expect, it } from 'vitest'
import { toBlockViews } from '../blockView'
import { createRustAdapter } from '../languages/rust'
import { readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
import { rustZhTranslator } from '../translators/rustZh'
import { rustImplRule } from './rustImpl'
import { serdeRule } from './serde'
import { thiserrorRule } from './thiserror'
const adapter = createRustAdapter(WASM_DIR)
const rules = [rustImplRule, serdeRule, thiserrorRule]
function views(source: string): Promise<BlockView[]> {
return withBlocks(adapter, 'rust', source, blocks =>
toBlockViews(blocks, { source, language: adapter, markers: [] }, rustZhTranslator, {
rules,
report: (message) => {
throw new Error(message)
},
}))
}
describe('serde 与 thiserror', () => {
it('可序列化的结构体、错误枚举', async () => {
expect(viewLines(await views(readFixture('rustLibs.rs')))).toEqual([
'import 1',
' 来源: serde',
' 导入项: 2 个',
' Deserialize | —',
' Serialize | —',
'serde.struct 3',
' 名称: AgentRow ⟦可序列化|⟧',
' 说明: 桌面端列表里的一行代理。',
' 可见性: 公开',
' 选项: 字段名改驼峰',
' 字段: 3 个',
' id | Uuid | 公开 | 改名 agent_id',
' name | String | 公开 | —',
' secret | String | 本模块 | 不输出',
'import 14',
' 来源: thiserror',
' 导入项: 1 个',
' Error | —',
'thiserror.error 16',
' 名称: AgentError',
' 说明: 读写代理时出的错。',
' 变体: 2 个',
' NotFound | 这个 id 没有代理 | agent {id} 不存在',
' Upstream | 调用上游失败 | 上游错误 ⟦透传来源|⟧',
])
})
it('没导入 serde 时,带 derive 的结构体仍是结构体', async () => {
const source = '#[derive(Serialize)]\npub struct AgentRow {\n name: String,\n}\n'
const [card] = await views(source)
expect(card?.kind).toBe('class')
})
it('skip_serializing 也是不输出', async () => {
const source = 'use serde::Serialize;\n#[derive(Serialize)]\nstruct Row {\n #[serde(skip_serializing)]\n token: String,\n}\n'
const lines = viewLines(await views(source))
expect(lines).toContain(' token | String | 本模块 | 不输出')
})
})
+145
View File
@@ -0,0 +1,145 @@
import type { ClassBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor, TokenRole } from '../contract'
import type { rustVisibility } from '../languages/rust'
import { attributeName, deriveNames, docSentence, itemAttributes, leadingAttributes, oneLine, stringContent, structFields } from '../languages/rust'
import { named } from '../languages/shared'
import { visibilityWord } from '../rustRows'
/** 和结构体同一个青,序列化只是名称后的标签,不另换边框色。 */
const COLOR: ThemeColor = { dark: '#4ec9b0', light: '#267f99' }
const BADGE: KindBadge = { label: '可序列化', color: COLOR, icon: 'braces' }
const MARKS = new Set(['Serialize', 'Deserialize'])
/**
* 带 Serialize / Deserialize 的结构体。字段多一列「序列化」。
* 文件里导入了 serde 才启用。
*/
export const serdeRule: LibraryRule = {
id: 'serde',
label: 'serde 序列化',
languageIds: ['rust'],
libraries: ['serde'],
claim: block => block.kind === 'class' && block.node.type === 'struct_item' && marked(block) ? card(block) : undefined,
attributeNames: () => ({ attributes: {}, values: { kind: { 'serde.struct': '可序列化' } } }),
}
function marked(block: ClassBlock): boolean {
return deriveNames(block.node).some(name => MARKS.has(name))
}
function card(block: ClassBlock): RuleCard {
const fields = structFields(block.node)
const camel = renameAll(block.node) === 'camelCase'
const doc = docSentence(block.facts.doc)
const rows: PropertyRow[] = [
{
label: '名称',
value: block.facts.name ? [...copied(block.facts.name, 'type'), plain(' '), tag('可序列化')] : [tag('可序列化')],
...(block.facts.name && { defines: range(block.facts.name) }),
},
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
{ label: '可见性', value: [{ text: visibilityWord(block), role: 'keyword' as const }] },
...(camel ? [{ label: '选项', value: [keyword('字段名改驼峰')] }] : []),
...(fields.length > 0
? [{
label: '字段',
value: [hint(`${fields.length} 个`)],
columns: ['字段', '类型', '可见性', '序列化'],
items: fields.map(field => ({
cells: [
copied(field.name, 'variable'),
copied(field.type, 'type'),
[keyword(visibilityWordOf(field.visibility))],
[serdeCell(field.name)],
],
defines: range(field.name),
})),
}]
: []),
]
return {
kind: 'serde.struct',
badge: BADGE,
rows,
...(block.facts.name && { defines: range(block.facts.name) }),
absorbs: [],
}
}
function visibilityWordOf(visibility: ReturnType<typeof rustVisibility>): string {
switch (visibility) {
case 'public': return '公开'
case 'super': return '父模块'
case 'crate': return '本 crate'
default: return '本模块'
}
}
function serdeCell(fieldName: Node): Inline {
const field = fieldName.parent
if (!field)
return hint('—')
let rename: string | undefined
let skip = false
for (const attr of leadingAttributes(field)) {
if (attributeName(attr) !== 'serde')
continue
for (const piece of named(tokenTree(attr))) {
if (piece.type === 'identifier' && (piece.text === 'skip' || piece.text === 'skip_serializing'))
skip = true
if (piece.type === 'identifier' && piece.text === 'rename')
rename = stringContent(firstString(tokenTree(attr))) ?? rename
}
}
if (skip)
return hint('不输出')
if (rename)
return { text: `改名 ${rename}`, role: 'string' }
return hint('—')
}
function renameAll(node: Node): string | undefined {
for (const attr of itemAttributes(node)) {
if (attributeName(attr) !== 'serde')
continue
const names = named(tokenTree(attr))
if (!names.some(piece => piece.type === 'identifier' && piece.text === 'rename_all'))
continue
const value = stringContent(firstString(tokenTree(attr)))
if (value)
return value
}
return undefined
}
function tokenTree(attributeItem: Node): Node | undefined {
const attribute = named(attributeItem).find(child => child.type === 'attribute')
return attribute?.children.find(child => child?.type === 'token_tree') ?? undefined
}
function firstString(tree: Node | undefined): Node | undefined {
return tree?.children.find(child => child?.type === 'string_literal') ?? undefined
}
function copied(node: Node, role: TokenRole): Inline[] {
return [{ text: oneLine(node.text), role, offset: node.startIndex }]
}
function tag(label: string): SegmentGroup {
return { style: 'tag', label, segments: [], color: COLOR }
}
function plain(text: string): Inline {
return { text, role: 'plain' }
}
function keyword(text: string): Inline {
return { text, role: 'keyword' }
}
function hint(text: string): Inline {
return { text, role: 'hint' }
}
function range(node: Node): { startIndex: number, endIndex: number } {
return { startIndex: node.startIndex, endIndex: node.endIndex }
}
+113
View File
@@ -0,0 +1,113 @@
import type { EnumBlock, Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, SegmentGroup, ThemeColor } from '../contract'
import { attributeName, commentPlain, deriveNames, docSentence, firstSentence, stringContent, variantPayload } from '../languages/rust'
import { named } from '../languages/shared'
/** 错误卡用异常那种红。 */
const COLOR: ThemeColor = { dark: '#f14c4c', light: '#cd3131' }
const BADGE: KindBadge = { label: '错误', color: COLOR, icon: 'triangle-alert' }
/**
* derive Error 的枚举。变体表是:变体、何时、展示。
* 文件里导入了 thiserror 才启用。
*/
export const thiserrorRule: LibraryRule = {
id: 'thiserror',
label: 'thiserror 错误',
languageIds: ['rust'],
libraries: ['thiserror'],
claim: block => block.kind === 'enum' && deriveNames(block.node).includes('Error') ? card(block) : undefined,
attributeNames: () => ({ attributes: {}, values: { kind: { 'thiserror.error': '错误' } } }),
}
function card(block: EnumBlock): RuleCard {
const doc = docSentence(block.facts.doc)
const rows: PropertyRow[] = [
{
label: '名称',
value: [{ text: block.facts.name.text, role: 'type', offset: block.facts.name.startIndex }],
defines: range(block.facts.name),
},
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
{
label: '变体',
value: [hint(`${block.facts.members.length} 个`)],
columns: ['变体', '何时', '展示'],
items: block.facts.members.map((member) => {
const variant = member.name.parent?.type === 'enum_variant' ? member.name.parent : member.name
const display = errorText(variant)
const cells: Inline[][] = [
[{ text: member.name.text, role: 'type', offset: member.name.startIndex }],
[hint(whenText(variant) ?? '—')],
display
? [plain(display), ...(passesSource(variant) ? [plain(' '), tag('透传来源')] : [])]
: [hint(variantPayload(member.value) ?? '—')],
]
return { cells, defines: range(member.name) }
}),
},
]
return {
kind: 'thiserror.error',
badge: BADGE,
rows,
defines: range(block.facts.name),
absorbs: [],
}
}
/** 变体上面的文档注释,属性可以隔在中间。第一句是「何时」。 */
function whenText(variant: Node): string | undefined {
const docs: Node[] = []
let prev = variant.previousNamedSibling
while (prev?.type === 'attribute_item')
prev = prev.previousNamedSibling
while (prev && (prev.type === 'line_comment' || prev.type === 'block_comment')) {
docs.unshift(prev)
prev = prev.previousNamedSibling
}
if (docs.length === 0)
return undefined
const sentence = firstSentence(docs.map(commentPlain).join(' '))
return sentence || undefined
}
function errorText(variant: Node): string | undefined {
let prev = variant.previousNamedSibling
while (prev?.type === 'attribute_item') {
if (attributeName(prev) === 'error') {
const tree = tokenTree(prev)
const literal = tree?.children.find(child => child?.type === 'string_literal')
const text = stringContent(literal ?? undefined)
if (text)
return text
}
prev = prev.previousNamedSibling
}
return undefined
}
function passesSource(variant: Node): boolean {
const body = variant.childForFieldName('body')
return named(body).some(child => child.type === 'attribute_item' && (attributeName(child) === 'from' || attributeName(child) === 'source'))
}
function tokenTree(attributeItem: Node): Node | undefined {
const attribute = named(attributeItem).find(child => child.type === 'attribute')
return attribute?.children.find(child => child?.type === 'token_tree') ?? undefined
}
function tag(label: string): SegmentGroup {
return { style: 'tag', label, segments: [], color: COLOR }
}
function plain(text: string): Inline {
return { text, role: 'plain' }
}
function hint(text: string): Inline {
return { text, role: 'hint' }
}
function range(node: Node): { startIndex: number, endIndex: number } {
return { startIndex: node.startIndex, endIndex: node.endIndex }
}