✨ feat(vscode): 代码阅读认 serde 和 thiserror
This commit is contained in:
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),
|
||||||
|
}
|
||||||
@@ -35,7 +35,9 @@ import { fastapiRule } from './rules/fastapi'
|
|||||||
import { pydanticRule } from './rules/pydantic'
|
import { pydanticRule } from './rules/pydantic'
|
||||||
import { reactRule } from './rules/react'
|
import { reactRule } from './rules/react'
|
||||||
import { rustImplRule } from './rules/rustImpl'
|
import { rustImplRule } from './rules/rustImpl'
|
||||||
|
import { serdeRule } from './rules/serde'
|
||||||
import { sqlalchemyRule } from './rules/sqlalchemy'
|
import { sqlalchemyRule } from './rules/sqlalchemy'
|
||||||
|
import { thiserrorRule } from './rules/thiserror'
|
||||||
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
|
import { bundledWasmDir, TreeSitterRuntime } from './runtime'
|
||||||
import { bashZhTranslator } from './translators/bashZh'
|
import { bashZhTranslator } from './translators/bashZh'
|
||||||
import { dockerfileZhTranslator } from './translators/dockerfileZh'
|
import { dockerfileZhTranslator } from './translators/dockerfileZh'
|
||||||
@@ -165,6 +167,8 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
|||||||
api.registerLibraryRule(fastapiRule),
|
api.registerLibraryRule(fastapiRule),
|
||||||
api.registerLibraryRule(reactRule),
|
api.registerLibraryRule(reactRule),
|
||||||
api.registerLibraryRule(rustImplRule),
|
api.registerLibraryRule(rustImplRule),
|
||||||
|
api.registerLibraryRule(serdeRule),
|
||||||
|
api.registerLibraryRule(thiserrorRule),
|
||||||
)
|
)
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
@@ -134,6 +134,8 @@ export function deriveNames(node: Node): string[] {
|
|||||||
for (const name of named(tree)) {
|
for (const name of named(tree)) {
|
||||||
if (name.type === 'identifier')
|
if (name.type === 'identifier')
|
||||||
names.push(name.text)
|
names.push(name.text)
|
||||||
|
else if (name.type === 'scoped_identifier')
|
||||||
|
names.push(name.childForFieldName('name')?.text ?? name.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return names
|
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 | 本模块 | 不输出')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user