✨ feat(vscode): 函数体里 let 的初始值、返回值也出 sqlx 查询和 axum 路由卡片
Rust 库规则共用 valueCard:语句看整句、let 看初始值、返回块看返回的值;认领 let 时第一行「变量」是变量名,也是卡片的定义;sqlx 认 query / query_as / query_scalar 函数连着 .bind 的写法,query_scalar! 不再把 SQL 当返回类型
This commit is contained in:
1 parent
324df3d5bd
commit
95559b160b
4 files changed
+217
-29
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import type { FunctionBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||
import { returnNote, throwsRows, writtenParamRows } from './docRows'
|
||||
import { returnValue } from './rustRows'
|
||||
import { returnValue, valueCard } from './rustRows'
|
||||
import { attributeName, docSummary, named, stringContent } from './rustTree'
|
||||
|
||||
/** 和 FastAPI 接口同一个蓝。 */
|
||||
@@ -9,7 +9,7 @@ const BADGE: KindBadge = { label: '接口', color: COLOR, icon: 'globe' }
|
||||
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'])
|
||||
|
||||
/**
|
||||
* 处理函数上的 get / post 等路由属性,或 Router 上能看出来的 route。
|
||||
* 处理函数上的 get / post 等路由属性,或语句、变量的初始值、返回值里 Router 上能看出来的 route。
|
||||
* 请求表和 FastAPI 接口一样:方法、路径、路径参数,再加返回。
|
||||
* 文件里导入了 axum 才启用。
|
||||
*/
|
||||
@@ -23,11 +23,10 @@ export const axumRule: LibraryRule = {
|
||||
const route = attributeRoute(block)
|
||||
return route ? card(block, route, context) : undefined
|
||||
}
|
||||
if (block.kind === 'statement') {
|
||||
const [route] = routesOn(block.node).filter(item => !handled(block.node, item.handler))
|
||||
return route ? routerCard(block.node, route, context) : undefined
|
||||
}
|
||||
return undefined
|
||||
return valueCard(block, context, (value) => {
|
||||
const [route] = routesOn(value).filter(item => !handled(value, item.handler))
|
||||
return route ? routerCard(value, route, context) : undefined
|
||||
})
|
||||
},
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'axum.route': '接口' } } }),
|
||||
}
|
||||
|
||||
@@ -161,3 +161,121 @@ describe('serde 与 thiserror', () => {
|
||||
expect(lines).toContain(' token | String | 本模块 | 不输出')
|
||||
})
|
||||
})
|
||||
|
||||
describe('函数体里的 sqlx 查询、axum 路由', () => {
|
||||
const source = [
|
||||
'use axum::Router;',
|
||||
'use axum::routing::get;',
|
||||
'use sqlx::PgPool;',
|
||||
'',
|
||||
'async fn load(pool: PgPool, id: Uuid, team: String) -> Result<(), sqlx::Error> {',
|
||||
' // 按 id 取一行。',
|
||||
' let row = sqlx::query!("SELECT name FROM agents WHERE id = $1", id).fetch_one(&pool).await?;',
|
||||
' let rows = sqlx::query_as::<_, AgentRow>("SELECT id, name FROM agents WHERE team = $1").bind(team).fetch_all(&pool).await?;',
|
||||
' let Some(total) = sqlx::query_scalar!("SELECT count(*) FROM agents").fetch_one(&pool).await? else {',
|
||||
' return Ok(());',
|
||||
' };',
|
||||
' Ok(())',
|
||||
'}',
|
||||
'',
|
||||
'async fn names(pool: PgPool) -> Result<Vec<Record>, sqlx::Error> {',
|
||||
' sqlx::query!("SELECT name FROM agents").fetch_all(&pool).await',
|
||||
'}',
|
||||
'',
|
||||
'fn app(state: AppState) -> Router {',
|
||||
' let probe = Router::new().route("/health", get(health));',
|
||||
' Router::new().route("/agents", get(list)).route("/agents/{id}", get(show)).with_state(state)',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
it('let 的初始值、末尾不带分号的返回值是查询或路由时出库卡片,变量名是第一行「变量」,let … else 的 else 照常是子块', async () => {
|
||||
const list = (await views(source)).filter(block => block.kind !== 'import')
|
||||
expect(viewLines(list)).toEqual([
|
||||
'function 5',
|
||||
' 名称: load',
|
||||
' 异步: 是',
|
||||
' 可见性: 本模块',
|
||||
' 输入参数: 3 个',
|
||||
' pool | PgPool',
|
||||
' id | Uuid',
|
||||
' team | String',
|
||||
' 返回: Result<(), sqlx::Error>',
|
||||
' sqlx.query 6',
|
||||
' 变量: row',
|
||||
' 注释: 按 id 取一行。',
|
||||
' SQL: SELECT name FROM agents WHERE id = $1',
|
||||
' 参数: $1 = id',
|
||||
' 返回类型: —',
|
||||
' sqlx.query 8',
|
||||
' 变量: rows',
|
||||
' SQL: SELECT id, name FROM agents WHERE team = $1',
|
||||
' 参数: $1 = team',
|
||||
' 返回类型: AgentRow',
|
||||
' sqlx.query 9',
|
||||
' 变量: Some(total)',
|
||||
' SQL: SELECT count(*) FROM agents',
|
||||
' 返回类型: —',
|
||||
' else 9: 否则',
|
||||
' return 10: 返回 Ok(());',
|
||||
' return 12: 返回 Ok(())',
|
||||
'function 15',
|
||||
' 名称: names',
|
||||
' 异步: 是',
|
||||
' 可见性: 本模块',
|
||||
' 输入参数: 1 个',
|
||||
' pool | PgPool',
|
||||
' 返回: Result<Vec<Record>, sqlx::Error>',
|
||||
' sqlx.query 16',
|
||||
' SQL: SELECT name FROM agents',
|
||||
' 返回类型: —',
|
||||
'function 19',
|
||||
' 名称: app',
|
||||
' 可见性: 本模块',
|
||||
' 输入参数: 1 个',
|
||||
' state | AppState',
|
||||
' 返回: Router',
|
||||
' axum.route 20',
|
||||
' 变量: probe',
|
||||
' 名称: health',
|
||||
' 请求: GET /health',
|
||||
' 方法 | GET',
|
||||
' 路径 | /health',
|
||||
' 返回: —',
|
||||
' axum.route 21',
|
||||
' 名称: show',
|
||||
' 请求: GET /agents/{id}',
|
||||
' 方法 | GET',
|
||||
' 路径 | /agents/{id}',
|
||||
' 返回: —',
|
||||
])
|
||||
})
|
||||
|
||||
it('认领的块留着变量、返回的概念;变量名是卡片和「变量」一行的定义', async () => {
|
||||
const list = await views(source)
|
||||
const [load, names, app] = list.filter(block => block.kind === 'function')
|
||||
const [row] = load?.children ?? []
|
||||
const at = source.indexOf('row =')
|
||||
expect(row?.concepts.map(concept => concept.id)).toEqual(['variable', 'sqlx.query'])
|
||||
expect(row?.defines).toEqual({ startIndex: at, endIndex: at + 'row'.length })
|
||||
expect(propertyRow(row, '变量').defines).toEqual({ startIndex: at, endIndex: at + 'row'.length })
|
||||
expect(names?.children[0]?.concepts.map(concept => concept.id)).toEqual(['return', 'sqlx.query'])
|
||||
expect(app?.children[1]?.concepts.map(concept => concept.id)).toEqual(['return', 'axum.route'])
|
||||
})
|
||||
|
||||
it('函数写法的 SQL 是拼出来的变量时照翻成一行,每个 .bind 依次是 $1、$2', async () => {
|
||||
const lines = viewLines(await views([
|
||||
'use sqlx::PgPool;',
|
||||
'async fn rename(pool: PgPool, sql: String, id: Uuid, name: String) {',
|
||||
' sqlx::query(&sql).bind(id).bind(&name).execute(&pool).await?;',
|
||||
'}',
|
||||
'',
|
||||
].join('\n')))
|
||||
expect(lines.slice(lines.indexOf(' sqlx.query 3'))).toEqual([
|
||||
' sqlx.query 3',
|
||||
' SQL: &sql',
|
||||
' 参数: $1 = id、$2 = &name',
|
||||
' 返回类型: —',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, RuleContext, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { AttributeBlock, AttributeFacts, AttributeItem, CfgPredicate, ClassBlock, CodeBlock, Doc, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, Inline, ModuleBlock, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { RustVisibility } from './rustTree'
|
||||
import { paramDocCell, paramDocs, returnNote, throwsRows, unmatchedParamRows } from './docRows'
|
||||
import { docSummary, isFnItem, isMethod, signatureParams, structFields, variantPayload } from './rustTree'
|
||||
@@ -291,3 +291,29 @@ export function payloadValue(body: Node | undefined, say: RustSay): Inline[] {
|
||||
const parts = variantPayload(body)
|
||||
return parts.length > 0 ? joined(parts.map(part => say.verbatim(part)), plain(', ')) : [hint('—')]
|
||||
}
|
||||
|
||||
/**
|
||||
* Rust 库规则认函数体里的一块时看它算出的值:语句看整句,变量看初始值,返回块看返回的值,交给 `card` 认成卡片。
|
||||
*
|
||||
* 认领变量时变量名排在卡片第一行「变量」,卡片和这一行的定义范围都是它,同没认领时的变量块;
|
||||
* `let … else` 的 else、实参里的闭包这些子块不并进卡片,照常是它的子块。
|
||||
*
|
||||
* @param block 要认的那一块
|
||||
* @param context 把变量名翻成一行用
|
||||
* @param card 从值里认出卡片,认不出是 `undefined`
|
||||
* @return 认出的卡片;别的种类的块、没写初始值的变量是 `undefined`
|
||||
*/
|
||||
export function valueCard(block: CodeBlock, context: RuleContext, card: (value: Node) => RuleCard | undefined): RuleCard | undefined {
|
||||
if (block.kind === 'statement' || block.kind === 'return')
|
||||
return card(block.node)
|
||||
if (block.kind !== 'variable')
|
||||
return undefined
|
||||
const [declarator, ...rest] = block.facts.declarators
|
||||
if (!declarator?.value || rest.length > 0)
|
||||
return undefined
|
||||
const found = card(declarator.value)
|
||||
if (!found)
|
||||
return undefined
|
||||
const defines = range(declarator.name)
|
||||
return { ...found, rows: [{ label: '变量', value: context.translate(declarator.name), defines }, ...found.rows], defines }
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
import type { Inline, KindBadge, LibraryRule, Node, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||
import { valueCard } from './rustRows'
|
||||
import { macroInvocation, named, oneLine, stringContent } from './rustTree'
|
||||
|
||||
/** 查询卡用字符串那种橙。 */
|
||||
const COLOR: ThemeColor = { dark: '#ce9178', light: '#a31515' }
|
||||
const BADGE: KindBadge = { label: '查询', color: COLOR, icon: 'database' }
|
||||
const MACROS = new Set(['query', 'query_as', 'query_scalar'])
|
||||
/** sqlx 发查询的宏和同名的函数。 */
|
||||
const QUERIES = new Set(['query', 'query_as', 'query_scalar'])
|
||||
|
||||
/** 一次查询写明的:SQL、依次传给 `$1`、`$2`… 的参数,写明了才有的返回类型。 */
|
||||
interface Query {
|
||||
sql?: Node
|
||||
params: Node[]
|
||||
returned?: Node
|
||||
}
|
||||
|
||||
/**
|
||||
* sqlx::query! / query_as! 这类宏。等宽 SQL、参数、返回类型;前面的注释同别的块,是卡片的「注释」一行。
|
||||
* sqlx 的查询:`query!` / `query_as!` / `query_scalar!` 这类宏,或 `query` / `query_as` / `query_scalar` 函数连着 `.bind(参数)`。
|
||||
* 等宽 SQL、参数、返回类型;前面的注释同别的块,是卡片的「注释」一行。语句、变量的初始值、返回值里有查询都认。
|
||||
* 文件里导入了 sqlx 才启用。
|
||||
*/
|
||||
export const sqlxRule: LibraryRule = {
|
||||
@@ -15,23 +25,16 @@ export const sqlxRule: LibraryRule = {
|
||||
label: 'sqlx 查询',
|
||||
languageIds: ['rust'],
|
||||
libraries: ['sqlx'],
|
||||
claim: (block, context) => {
|
||||
const macro = block.kind === 'statement' ? findQuery(block.node) : undefined
|
||||
return macro ? card(macro, context) : undefined
|
||||
},
|
||||
claim: (block, context) => valueCard(block, context, (value) => {
|
||||
const query = findQuery(value)
|
||||
return query && card(query, context)
|
||||
}),
|
||||
attributeNames: () => ({ attributes: {}, values: { kind: { 'sqlx.query': '查询' } } }),
|
||||
}
|
||||
|
||||
function card(macro: Node, context: RuleContext): RuleCard {
|
||||
const call = macroInvocation(macro)
|
||||
const args = call?.args ?? []
|
||||
const typed = call?.name.split('::').pop() !== 'query'
|
||||
const sqlNode = args[typed ? 1 : 0]
|
||||
const sql = stringContent(sqlNode) ?? ''
|
||||
const params = args.slice(typed ? 2 : 1)
|
||||
const returned = typed ? args[0] : undefined
|
||||
function card({ sql, params, returned }: Query, context: RuleContext): RuleCard {
|
||||
const rows: PropertyRow[] = [
|
||||
{ label: 'SQL', value: [{ text: sql, role: 'string', ...(sqlNode && { offset: sqlNode.startIndex }) }] },
|
||||
{ label: 'SQL', value: sqlText(sql, context) },
|
||||
...(params.length > 0
|
||||
? [{ label: '参数', value: [{ text: params.map((param, index) => `$${index + 1} = ${oneLine(param.text)}`).join('、'), role: 'plain' as const }] }]
|
||||
: []),
|
||||
@@ -40,12 +43,19 @@ function card(macro: Node, context: RuleContext): RuleCard {
|
||||
return { kind: 'sqlx.query', badge: BADGE, rows, absorbs: [] }
|
||||
}
|
||||
|
||||
function findQuery(node: Node): Node | undefined {
|
||||
if (node.type === 'macro_invocation') {
|
||||
const last = node.childForFieldName('macro')?.text.split('::').pop()
|
||||
if (last && MACROS.has(last))
|
||||
return node
|
||||
}
|
||||
/** 写成字符串的 SQL 是去掉引号的原文;拼出来再传进来的(`&sql`)照翻成一行。 */
|
||||
function sqlText(sql: Node | undefined, context: RuleContext): Inline[] {
|
||||
const text = stringContent(sql)
|
||||
if (sql && text === undefined)
|
||||
return context.translate(sql)
|
||||
return [{ text: text ?? '', role: 'string', ...(sql && { offset: sql.startIndex }) }]
|
||||
}
|
||||
|
||||
/** 外层先认,找到的第一个查询。 */
|
||||
function findQuery(node: Node): Query | undefined {
|
||||
const query = macroQuery(node) ?? callQuery(node)
|
||||
if (query)
|
||||
return query
|
||||
for (const child of named(node)) {
|
||||
const found = findQuery(child)
|
||||
if (found)
|
||||
@@ -54,6 +64,41 @@ function findQuery(node: Node): Node | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** `query!(SQL, 参数…)`、`query_scalar!(SQL, 参数…)`,`query_as!` 在 SQL 前面多一个返回类型。 */
|
||||
function macroQuery(node: Node): Query | undefined {
|
||||
const call = macroInvocation(node)
|
||||
const name = call?.name.split('::').pop()
|
||||
if (!call || !name || !QUERIES.has(name))
|
||||
return undefined
|
||||
const typed = name === 'query_as'
|
||||
const returned = typed ? call.args[0] : undefined
|
||||
return { sql: call.args[typed ? 1 : 0], params: call.args.slice(typed ? 2 : 1), ...(returned && { returned }) }
|
||||
}
|
||||
|
||||
/**
|
||||
* `query(SQL)`、`query_as::<_, 返回类型>(SQL)` 这类函数,后面连着的每个 `.bind(参数)` 依次是一个参数。
|
||||
* 尖括号里第一个是数据库、第二个是返回类型;`query` 和没写尖括号的没有返回类型。
|
||||
*/
|
||||
function callQuery(node: Node): Query | undefined {
|
||||
if (node.type !== 'call_expression')
|
||||
return undefined
|
||||
const callee = node.childForFieldName('function')
|
||||
const [first] = named(node.childForFieldName('arguments'))
|
||||
if (callee?.type === 'field_expression' && callee.childForFieldName('field')?.text === 'bind') {
|
||||
const inner = callee.childForFieldName('value')
|
||||
const query = inner ? callQuery(inner) : undefined
|
||||
return query && { ...query, params: first ? [...query.params, first] : query.params }
|
||||
}
|
||||
const generic = callee?.type === 'generic_function' ? callee : undefined
|
||||
const path = generic ? generic.childForFieldName('function') : callee
|
||||
const name = path?.type === 'scoped_identifier' ? path.childForFieldName('name')?.text : path?.type === 'identifier' ? path.text : undefined
|
||||
if (!name || !QUERIES.has(name))
|
||||
return undefined
|
||||
const types = named(generic?.childForFieldName('type_arguments')).filter(type => type.type !== 'lifetime')
|
||||
const returned = name === 'query' ? undefined : types[1]
|
||||
return { sql: first, params: [], ...(returned && { returned }) }
|
||||
}
|
||||
|
||||
function hint(text: string): Inline {
|
||||
return { text, role: 'hint' }
|
||||
}
|
||||
Reference in new issue
Block a user