✨ 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 { 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
|
||||
}
|
||||
@@ -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 | 本模块 | 不输出')
|
||||
})
|
||||
})
|
||||
@@ -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