✨ feat(vscode): 代码阅读认 axum、sqlx 和 tauri
This commit is contained in:
1 parent
44de0a1f70
commit
69608f9c70
9 files changed
+500
-3
No files matched your search
@@ -0,0 +1,227 @@
|
||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, ThemeColor } from '../contract'
|
||||
import { attributeName, docSentence, oneLine, returnText, stringContent } from '../languages/rust'
|
||||
import { named } from '../languages/shared'
|
||||
|
||||
/** 和 FastAPI 接口同一个蓝。 */
|
||||
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
||||
const BADGE: KindBadge = { label: '接口', color: COLOR, icon: 'globe' }
|
||||
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'])
|
||||
|
||||
/**
|
||||
* 处理函数上的 get / post 等路由属性,或 Router 上能看出来的 route。
|
||||
* 请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。
|
||||
* 文件里导入了 axum 才启用。
|
||||
*/
|
||||
export const axumRule: LibraryRule = {
|
||||
id: 'axum',
|
||||
label: 'axum 接口',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['axum'],
|
||||
claim: (block) => {
|
||||
if (block.kind === 'function') {
|
||||
const route = attributeRoute(block)
|
||||
return route ? card(block, route) : undefined
|
||||
}
|
||||
if (block.kind === 'statement') {
|
||||
const [route] = routesOn(block.node).filter(item => !handled(block.node, item.handler))
|
||||
return route ? routerCard(block.node, route) : undefined
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'axum.route': '接口' } } }),
|
||||
}
|
||||
|
||||
interface Route {
|
||||
method: string
|
||||
path: string
|
||||
handler?: string
|
||||
}
|
||||
|
||||
function attributeRoute(block: FunctionBlock): Route | undefined {
|
||||
for (const attr of block.facts.decorators) {
|
||||
const method = attributeName(attr)?.split('::').pop()
|
||||
if (!method || !METHODS.has(method))
|
||||
continue
|
||||
const path = attributeString(attr)
|
||||
if (path)
|
||||
return { method: method.toUpperCase(), path }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function card(block: FunctionBlock, route: Route): RuleCard {
|
||||
const doc = docSentence(block.facts.doc)
|
||||
const params = pathParams(route.path, block.node)
|
||||
const rows: PropertyRow[] = [
|
||||
nameRow(block.facts.name),
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
...(block.facts.async ? [{ label: '异步', value: [keyword('是')] }] : []),
|
||||
requestRow(route, params),
|
||||
{ label: '返回', value: [plain(returnText(block.node))] },
|
||||
]
|
||||
return {
|
||||
kind: 'axum.route',
|
||||
badge: BADGE,
|
||||
rows,
|
||||
...(block.facts.name && { defines: range(block.facts.name) }),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
/** 同一文件里的处理函数已经标了路由属性时,Router 上这句不再单独成卡。 */
|
||||
function handled(node: Node, handler: string | undefined): boolean {
|
||||
if (!handler)
|
||||
return false
|
||||
const root = node.tree.rootNode
|
||||
for (const item of named(root)) {
|
||||
if (item.type !== 'function_item' || item.childForFieldName('name')?.text !== handler)
|
||||
continue
|
||||
return itemAttributesHasRoute(item)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function routerCard(node: Node, route: Route & { handlerNode?: Node }): RuleCard {
|
||||
const fn = route.handler ? functionNamed(node, route.handler) : undefined
|
||||
const params = fn ? pathParams(route.path, fn) : []
|
||||
const doc = fn ? docSentence(leadingDoc(fn)) : undefined
|
||||
return {
|
||||
kind: 'axum.route',
|
||||
badge: BADGE,
|
||||
rows: [
|
||||
{ label: '名称', value: route.handler ? [plain(route.handler)] : [hint('路由')] },
|
||||
...(doc ? [{ label: '说明', value: { markdown: doc } }] : []),
|
||||
requestRow(route, params),
|
||||
{ label: '返回', value: [plain(fn ? returnText(fn) : '—')] },
|
||||
],
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
function requestRow(route: Route, params: string[]): PropertyRow {
|
||||
const items: PropertyItem[] = [
|
||||
{ cells: [[plain('方法')], [keyword(route.method)]] },
|
||||
{ cells: [[plain('路径')], [{ text: route.path, role: 'string' }]] },
|
||||
...(params.length > 0 ? [{ cells: [[plain('路径参数')], params.map(text => plain(text)).flatMap((part, index) => index === 0 ? [part] : [plain(', '), part])] }] : []),
|
||||
]
|
||||
return {
|
||||
label: '请求',
|
||||
value: [keyword(route.method), plain(` ${route.path}`)],
|
||||
columns: ['项', '值'],
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
function routesOn(node: Node, found: (Route & { handlerNode?: Node })[] = []): (Route & { handlerNode?: Node })[] {
|
||||
if (node.type === 'call_expression') {
|
||||
const callee = node.childForFieldName('function')
|
||||
const field = callee?.type === 'field_expression' ? callee.childForFieldName('field')?.text : undefined
|
||||
if (field === 'route') {
|
||||
const args = named(node.childForFieldName('arguments'))
|
||||
const pathNode = args.find(arg => arg.type === 'string_literal')
|
||||
const call = args.find(arg => arg.type === 'call_expression')
|
||||
const method = call?.childForFieldName('function')?.text.split('::').pop()
|
||||
const handlerNode = named(call?.childForFieldName('arguments'))[0]
|
||||
const path = stringContent(pathNode)
|
||||
if (path && method && METHODS.has(method)) {
|
||||
found.push({
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
...(handlerNode?.type === 'identifier' && { handler: handlerNode.text, handlerNode }),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of named(node))
|
||||
routesOn(child, found)
|
||||
return found
|
||||
}
|
||||
|
||||
/** 路径里的 {名字} 对上参数。Path(id): Path<Uuid> 写成 id: Uuid。 */
|
||||
function pathParams(path: string, fn: Node): string[] {
|
||||
const names = [...path.matchAll(/\{(\w+)\}/g)].map(match => match[1] ?? '')
|
||||
return names.flatMap((name) => {
|
||||
const param = parameterNamed(fn, name)
|
||||
if (!param)
|
||||
return [`${name}`]
|
||||
const type = innerType(param.childForFieldName('type'))
|
||||
return [type ? `${name}: ${type}` : name]
|
||||
})
|
||||
}
|
||||
|
||||
function parameterNamed(fn: Node, name: string): Node | undefined {
|
||||
const list = fn.childForFieldName('parameters')
|
||||
return named(list).find((param) => {
|
||||
if (param.type !== 'parameter' && param.type !== 'self_parameter')
|
||||
return false
|
||||
const pattern = param.childForFieldName('pattern') ?? param
|
||||
return namedDeep(pattern).some(node => (node.type === 'identifier' || node.type === 'self') && node.text === name)
|
||||
})
|
||||
}
|
||||
|
||||
function innerType(type: Node | null): string | undefined {
|
||||
if (!type)
|
||||
return undefined
|
||||
if (type.type === 'generic_type' && type.childForFieldName('type')?.text === 'Path') {
|
||||
const arg = named(type.childForFieldName('type_arguments'))[0]
|
||||
if (arg)
|
||||
return oneLine(arg.text)
|
||||
}
|
||||
return oneLine(type.text)
|
||||
}
|
||||
|
||||
function itemAttributesHasRoute(fn: Node): boolean {
|
||||
let prev = fn.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item') {
|
||||
const method = attributeName(prev)?.split('::').pop()
|
||||
if (method && METHODS.has(method))
|
||||
return true
|
||||
prev = prev.previousNamedSibling
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function functionNamed(node: Node, name: string): Node | undefined {
|
||||
return named(node.tree.rootNode).find(item => item.type === 'function_item' && item.childForFieldName('name')?.text === name)
|
||||
}
|
||||
|
||||
function leadingDoc(fn: Node): Node | undefined {
|
||||
let prev = fn.previousNamedSibling
|
||||
while (prev?.type === 'attribute_item')
|
||||
prev = prev.previousNamedSibling
|
||||
return prev && (prev.type === 'line_comment' || prev.type === 'block_comment') ? prev : undefined
|
||||
}
|
||||
|
||||
function attributeString(attributeItem: Node): string | undefined {
|
||||
const attribute = named(attributeItem).find(child => child.type === 'attribute')
|
||||
const tree = attribute?.children.find(child => child?.type === 'token_tree')
|
||||
return stringContent(tree?.children.find(child => child?.type === 'string_literal') ?? undefined)
|
||||
}
|
||||
|
||||
function namedDeep(node: Node): Node[] {
|
||||
return [node, ...named(node).flatMap(namedDeep)]
|
||||
}
|
||||
|
||||
function nameRow(name: Node | undefined): PropertyRow {
|
||||
return {
|
||||
label: '名称',
|
||||
value: name ? [{ text: name.text, role: 'function', offset: name.startIndex }] : [hint('匿名')],
|
||||
...(name && { defines: range(name) }),
|
||||
}
|
||||
}
|
||||
|
||||
function keyword(text: string): Inline {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
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