✨ feat(vscode): 代码阅读把说明里的 @param / @return / @throws 拆到参数表、输出、异常表

This commit is contained in:
cruldra committed 2026-10-08 07:17:26 +08:00
1 parent c2982537d7
commit 0958506681
43 files changed
+1300 -323

No files matched your search

+51 -8
View File
@@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest'
import { toBlockViews } from '../blockView'
import { createPythonAdapter } from '../languages/python'
import { DocumentLookups } from '../ruleLookups'
import { flatSegments, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { flatSegments, inlineOf, MemoryIndexHost, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from '../translators/pythonZh'
import { WorkspaceIndexes } from '../workspaceIndex'
import { fastapiRule } from './fastapi'
@@ -47,7 +47,7 @@ async function route(source: string, fn: string, lookup?: RuleContext['lookup'])
function segmentsOf(row: PropertyRow | undefined): Segment[] {
const pieces: Inline[] = [
...(!row || 'markdown' in row.value ? [] : row.value),
...(row?.items ?? []).flatMap(item => item.cells.flat()),
...(row?.items ?? []).flatMap(item => item.cells.flatMap(inlineOf)),
]
return flatSegments(pieces)
}
@@ -97,20 +97,20 @@ describe('fastapi 库规则:接口卡片', () => {
const rows = properties(await route(ROUTES, 'update_user', known({ Forbidden: '403' })))
const request = rows.find(row => row.label === '请求')
const [url, method] = request?.items ?? []
expect(flatSegments(url.cells[1]).map(piece => [piece.text, piece.role])).toEqual([['/users', 'string'], ['/', 'string'], ['{', 'variable'], ['user_id', 'variable'], ['}', 'variable']])
for (const piece of flatSegments(url.cells[1]))
expect(flatSegments(inlineOf(url.cells[1])).map(piece => [piece.text, piece.role])).toEqual([['/users', 'string'], ['/', 'string'], ['{', 'variable'], ['user_id', 'variable'], ['}', 'variable']])
for (const piece of flatSegments(inlineOf(url.cells[1])))
expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text)
// 路径里的 user_id 讲的是 update_user 的参数 user_id:高亮参数的各处出现时跟着亮,⌘ + 点击跳到参数
const param = ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf('def update_user'))
expect(flatSegments(url.cells[1]).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: param, endIndex: param + 'user_id'.length })
expect(flatSegments(inlineOf(url.cells[1])).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: param, endIndex: param + 'user_id'.length })
expect(method.cells[1]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
const [ok, forbidden, validation] = rows.find(row => row.label === '响应')?.items ?? []
expect(ok.cells[0]).toEqual([{ text: '200', role: 'number', color: { dark: '#2ea043', light: '#1a7f37' } }])
const model = ROUTES.indexOf('response_model=UserDetailResponse', ROUTES.indexOf('@router.put')) + 'response_model='.length
expect(ok.cells[1]).toEqual([{ text: 'UserDetailResponse', role: 'type', offset: model }])
expect(forbidden.cells[0]).toEqual([{ text: '403', role: 'string' }])
expect(flatSegments(forbidden.cells[1])).toEqual([{ text: 'Forbidden', role: 'type', offset: ROUTES.indexOf('Forbidden("无权更新') }])
expect(flatSegments([...validation.cells[1], ...validation.cells[2]]).map(piece => piece.role)).toEqual(['hint', 'hint'])
expect(flatSegments(inlineOf(forbidden.cells[1]))).toEqual([{ text: 'Forbidden', role: 'type', offset: ROUTES.indexOf('Forbidden("无权更新') }])
expect(flatSegments([...inlineOf(validation.cells[1]), ...inlineOf(validation.cells[2])]).map(piece => piece.role)).toEqual(['hint', 'hint'])
// 名字都带下标、指回源码原文
for (const piece of rows.flatMap(segmentsOf).filter(piece => piece.offset !== undefined && piece.role !== 'string' && piece.role !== 'variable'))
expect(ROUTES.slice(piece.offset, (piece.offset ?? 0) + piece.text.length)).toBe(piece.text)
@@ -316,6 +316,49 @@ async function routerCard(source: string, name: string): Promise<BlockView> {
return found
}
describe('fastapi 库规则:说明里的块标签', () => {
const SOURCE = [
'from fastapi import APIRouter, Depends, Query',
'',
'router = APIRouter(prefix="/apps")',
'',
'@router.get("/{app_id}")',
'async def get_app(app_id: int, q: str | None = Query(None), db = Depends(get_db)) -> AppOut:',
' """取一个应用。',
'',
' @param app_id 应用 id',
' @param q 按名字搜',
' @param db 数据库会话',
' @param qq 写错的名字',
' @return 应用详情',
' @throws [AppNotFound] 没有这个应用',
' """',
].join('\n')
it('@param 对到请求表(一个参数一行,多一列说明),依赖参数不算对不上;@return 跟在响应后面;@throws 成异常表', async () => {
const lines = propertyLines(properties(await route(SOURCE, 'get_app')))
expect(lines).toEqual([
'名称: get_app',
'说明: 取一个应用。',
'对不上的参数: 1 个',
' qq | 写错的名字',
'路由器: router',
'异步: 是',
'依赖: get_db(db)',
'请求: GET /apps/{app_id}',
' URL | /apps/{app_id} | ',
' 方法 | GET | ',
' 路径参数 | app_id: 整数 | 应用 id',
' 查询参数 | q: 字符串 | 空 | 按名字搜',
'响应: 200 / 422 — 应用详情',
' 200 | AppOut | 成功',
' 422 | 参数校验失败 | FastAPI 自动返回',
'异常: 1 个',
' [AppNotFound] | 没有这个应用',
])
})
})
describe('fastapi 库规则:路由器卡片', () => {
it('luffy 的 router:名称 / 前缀 / 标签,接口表列这份文件里挂在它上面的接口(方法 / 完整路径 / 函数 / docstring 第一行)', async () => {
const card = await routerCard(ROUTES, 'router')
@@ -339,7 +382,7 @@ describe('fastapi 库规则:路由器卡片', () => {
expect(getUser.cells[2]).toEqual([{ text: 'get_user', role: 'function', offset: ROUTES.indexOf('get_user(') }])
const paramOf = (fn: string): number => ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf(`def ${fn}`))
for (const [row, fn] of [[getUser, 'get_user'], [updateUser, 'update_user']] as const)
expect(flatSegments(row.cells[1]).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: paramOf(fn), endIndex: paramOf(fn) + 'user_id'.length })
expect(flatSegments(inlineOf(row.cells[1])).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: paramOf(fn), endIndex: paramOf(fn) + 'user_id'.length })
expect(updateUser.cells[0]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
})