✨ feat(vscode): 代码阅读加 FastAPI 接口卡片:请求表(URL、方法、路径 / 查询参数、请求头、Cookie、请求体)、按状态码的响应表(成功、responses、函数里抛的异常、自动带的 422);别的文件里的异常状态码、类型别名经工作区索引和语言扩展查

This commit is contained in:
cruldra committed 2026-09-27 20:19:24 +08:00
1 parent 1dfc0c7b1e
commit 0798f7d1c7
18 files changed
+1745 -28

No files matched your search

+625
View File
@@ -0,0 +1,625 @@
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
import type { Call, LibraryNames } from './pythonTree'
import { callOf, genericOf, lastName, libraryName, stringContent, syntaxChildren, unwrapType } from './pythonTree'
import { hint, joined, name, range, word } from './segments'
/**
* FastAPI 库规则:挂在路由对象上的函数(`@router.get(...)` 这类装饰器,router 是同一文件模块顶层 `X = APIRouter(...)` / `X = FastAPI(...)`
* 赋值出来的)整块换成「接口」卡片,函数体照常是子块。卡片:名称 / 说明 / 标签 / 异步 / 依赖,然后请求表(一项一行)、响应表(一个状态码一行)。
* 参数放在请求的哪儿照 FastAPI 的规则认(见 placeOf);函数体里直接抛的异常按状态码归进响应表:HTTPException 看 status_code,
* 自定义的异常类看类上的 `status_code = N`,类写在别处时经工作区索引查(NAMES_INDEX,RuleContext.lookup)。
* 挂到应用上时再叠的前缀(include_router、settings.API_V1_PREFIX)在别的文件里,URL 不算。用词都在词表里,按翻译插件的显示语言挑。
*/
const RULE_ID = 'fastapi'
const KIND = 'fastapi.route'
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
const ICON = 'globe'
/** 认路由对象、参数标记、HTTPException 时看从这几个库导入的名字(HTTPException、status 也可以从 starlette 来)。 */
const NAME_LIBRARIES = ['fastapi', 'starlette']
/** 路由对象上挂接口的方法:`router.get`、`app.post` …… */
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'])
/** HTTP 方法的颜色:Swagger UI 的配色,看接口文档的人最熟;白底上照原色看不清,亮色主题取同色相深一些的。 */
const METHOD_COLORS: Record<string, ThemeColor> = {
GET: { dark: '#61affe', light: '#1b6ac9' },
POST: { dark: '#49cc90', light: '#18834f' },
PUT: { dark: '#fca130', light: '#b35f00' },
PATCH: { dark: '#50e3c2', light: '#0f8a70' },
DELETE: { dark: '#f93e3e', light: '#cf2020' },
HEAD: { dark: '#b36bff', light: '#9012fe' },
OPTIONS: { dark: '#4b92e0', light: '#0d5aa7' },
}
/** 成功(小于 400)的状态码的颜色。 */
const SUCCESS_COLOR: ThemeColor = { dark: '#2ea043', light: '#1a7f37' }
/** 参数上写的 FastAPI 标记(默认值 `= Query(...)`,或 `Annotated[T, Query(...)]` 里的)→ 放在请求的哪儿。 */
const MARKER_PLACES: Record<string, Place> = {
Path: 'path',
Query: 'query',
Header: 'header',
Cookie: 'cookie',
Body: 'body',
Form: 'body',
File: 'body',
Depends: 'dependency',
Security: 'dependency',
}
/** FastAPI 自己往里填、不是请求带来的参数类型。 */
const INJECTED = new Set(['Request', 'Response', 'WebSocket', 'HTTPConnection', 'BackgroundTasks', 'SecurityScopes'])
/** 简单类型:没写标记时是查询参数;别的类(Pydantic 模型)是请求体。 */
const SIMPLE = new Set(['int', 'float', 'str', 'bool', 'bytes', 'complex', 'UUID', 'datetime', 'date', 'time', 'timedelta', 'Decimal', 'Any', 'EmailStr', 'HttpUrl', 'AnyUrl', 'SecretStr'])
/** 包着别的类型的写法:里面都是简单类型时它也是(`list[str]`、`Optional[int]`)。 */
const WRAPPERS = new Set(['Optional', 'Union', 'list', 'List', 'set', 'Set', 'frozenset', 'FrozenSet', 'tuple', 'Tuple', 'Sequence'])
/** enum 库的枚举类型:父类是它们的类是简单类型(查询参数)。 */
const ENUM_BASES = new Set(['Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag'])
/** 类属性、状态码常量里的状态码:`status_code = 403`、`status.HTTP_403_FORBIDDEN`。 */
const STATUS_NAME = /^HTTP_(\d{3})_/
type Place = 'path' | 'query' | 'header' | 'cookie' | 'body' | 'dependency'
/** 一种显示语言的用词。 */
interface Words {
kind: string
labels: Record<'name' | 'doc' | 'tags' | 'async' | 'decorators' | 'dependencies' | 'request' | 'responses', string>
yes: string
no: string
requestHeads: string[]
responseHeads: string[]
items: Record<'url' | 'method' | 'path' | 'query' | 'header' | 'cookie' | 'body', string>
/** 成功那一行没写 response_description 时的说明 */
success: string
/** FastAPI 自动带的 422:内容、说明 */
validation: string
automatic: string
/** 几项之间(标签、依赖、内容)、几条消息之间 */
listSeparator: string
messageSeparator: string
/** 依赖注进哪个参数:「(param)」 */
injectedInto: (param: string) => string
}
const ZH: Words = {
kind: '接口',
labels: { name: '名称', doc: '说明', tags: '标签', async: '异步', decorators: '装饰器', dependencies: '依赖', request: '请求', responses: '响应' },
yes: '是',
no: '否',
requestHeads: ['项', '值'],
responseHeads: ['状态码', '内容', '说明'],
items: { url: 'URL', method: '方法', path: '路径参数', query: '查询参数', header: '请求头', cookie: 'Cookie', body: '请求体' },
success: '成功',
validation: '参数校验失败',
automatic: 'FastAPI 自动返回',
listSeparator: '、',
messageSeparator: ';',
injectedInto: param => `(${param})`,
}
/** 显示语言 → 用词;翻译插件的显示语言这里没有时用中文。 */
const WORDS: Record<string, Words> = { zh: ZH }
interface Env {
context: RuleContext
words: Words
/** fastapi、starlette 和从它们导入的名字 */
names: LibraryNames
}
// ─── 别的文件里的定义 ─────────────────────────────────────────────────────────
/** 状态码写法 → 三位数字:整数字面量、`HTTP_403_FORBIDDEN` 这种常量(`status.` 开头也算)。 */
function statusNumber(node: Node | null | undefined): string | undefined {
if (node?.type === 'integer')
return node.text
const match = STATUS_NAME.exec(lastName(node) ?? '')
return match?.[1]
}
/** 类主体里 `status_code = N`(带不带注解)写的状态码。 */
function classStatus(definition: Node): string | undefined {
for (const statement of syntaxChildren(definition.childForFieldName('body'))) {
const [assignment] = statement.type === 'expression_statement' ? syntaxChildren(statement) : []
if (assignment?.type === 'assignment' && assignment.childForFieldName('left')?.text === 'status_code') {
const status = statusNumber(assignment.childForFieldName('right'))
if (status)
return status
}
}
return undefined
}
/**
* `Annotated[T, ..., Query(...)]`:T,和里面的 FastAPI 标记(调用)。markerName 认调用的是哪个标记:
* 本文件按导入认(fastapi 里的名字),别的文件(建索引时)只看名字的最后一节。
*/
function annotatedMarker(node: Node | null | undefined, markerName: (call: Node) => string | undefined): { type?: Node, marker?: Node } | undefined {
const generic = node ? genericOf(node) : undefined
if (!generic || lastName(generic.head) !== 'Annotated')
return undefined
const [type, ...metadata] = generic.args.map(unwrapType)
return { type, marker: metadata.find(item => MARKER_PLACES[markerName(item) ?? ''] !== undefined) }
}
/** 调用的函数名的最后一节(建索引时认别的文件里的标记)。 */
function calleeName(node: Node): string | undefined {
return node.type === 'call' ? lastName(node.childForFieldName('function')) : undefined
}
/**
* 从一份文件的模块顶层认出别的文件要查的名字,值是:
* - 类上写了 `status_code = N` 的(异常类):状态码 N;
* - 父类是 enum 枚举类型的类:`Enum`(简单类型,查询参数);
* - `X = Annotated[T, Query(...)]` 这种类型别名:标记名(`Query`、`Path`、`Depends` ……)。
*/
function indexEntries(root: Node): IndexEntry[] {
const entries: IndexEntry[] = []
for (const statement of syntaxChildren(root)) {
const definition = statement.type === 'decorated_definition' ? statement.childForFieldName('definition') : statement
if (definition?.type === 'class_definition') {
const className = definition.childForFieldName('name')
const status = classStatus(definition)
const enumType = syntaxChildren(definition.childForFieldName('superclasses')).some(base => ENUM_BASES.has(lastName(base) ?? ''))
const value = status ?? (enumType ? 'Enum' : undefined)
if (className && value)
entries.push({ key: className.text, name: range(className), value })
continue
}
const [assignment] = statement.type === 'expression_statement' ? syntaxChildren(statement) : []
const target = assignment?.type === 'assignment' ? assignment.childForFieldName('left') : null
const marker = annotatedMarker(assignment?.childForFieldName('right'), calleeName)?.marker
if (target?.type === 'identifier' && marker)
entries.push({ key: target.text, name: range(target), value: calleeName(marker) })
}
return entries
}
const NAMES_INDEX: WorkspaceIndex = {
id: `${RULE_ID}.names`,
languageId: 'python',
include: '**/*.py',
mentions: source => /\bstatus_code\b|\bAnnotated\b|Enum\b/.test(source),
entries: root => indexEntries(root),
}
/** 名字(`Forbidden`、`exceptions.Forbidden` 的最后一节)的定义在索引里认出的值;没查到是 undefined。 */
function definedValue(node: Node | null | undefined, env: Env): string | undefined {
const target = node?.type === 'attribute' ? node.childForFieldName('attribute') : node
return target?.type === 'identifier' ? env.context.lookup(NAMES_INDEX, target)?.value : undefined
}
// ─── 读语法树 ─────────────────────────────────────────────────────────────────
/** fastapi、starlette(连子模块)自己和从它们导入的名字:本地名 → 原名。 */
function namesOf(imports: readonly ImportFacts[]): LibraryNames {
const names: LibraryNames = { modules: new Set(), imported: new Map() }
const ours = (module: string): boolean => NAME_LIBRARIES.some(library => module === library || module.startsWith(`${library}.`))
for (const facts of imports) {
const from = facts.sources.some(source => ours(source.text))
for (const item of facts.items) {
if (item.kind === 'module' && ours(item.module.text))
names.modules.add(item.alias?.text ?? item.module.text)
else if (item.kind === 'named' && from)
names.imported.set(item.alias?.text ?? item.name.text, item.name.text)
}
}
return names
}
/** 调用的是 fastapi / starlette 里的哪个名字(`Query(...)`、`fastapi.APIRouter(...)`)。 */
function calledName(node: Node | null | undefined, env: Env): string | undefined {
return node?.type === 'call' ? libraryName(node.childForFieldName('function'), env.names) : undefined
}
/** 列表、元组的各项;别的写法当一项。 */
function listItems(node: Node | undefined): Node[] {
if (!node)
return []
return node.type === 'list' || node.type === 'tuple' ? syntaxChildren(node) : [node]
}
/** 路由对象:同一文件模块顶层 `X = APIRouter(...)` / `X = FastAPI(...)` 赋值出来的,和它给挂在上面的接口叠的东西。 */
interface Router {
prefix?: Node
tags: Node[]
dependencies: Node[]
responses?: Node
}
function routersOf(root: Node, env: Env): Map<string, Router> {
const routers = new Map<string, Router>()
for (const statement of syntaxChildren(root)) {
const [assignment] = statement.type === 'expression_statement' ? syntaxChildren(statement) : []
const target = assignment?.type === 'assignment' ? assignment.childForFieldName('left') : null
const value = assignment?.childForFieldName('right')
const kind = calledName(value, env)
const call = callOf(value, new Map())
if (target?.type !== 'identifier' || !call || (kind !== 'APIRouter' && kind !== 'FastAPI'))
continue
routers.set(target.text, {
prefix: kind === 'APIRouter' ? call.keywords.get('prefix') : undefined,
tags: listItems(call.keywords.get('tags')),
dependencies: listItems(call.keywords.get('dependencies')),
responses: call.keywords.get('responses'),
})
}
return routers
}
/** 挂在路由对象上的一个接口:`@router.put("/{user_id}", ...)`。 */
interface Route {
decorator: Node
router: Router
method: string
call: Call
path?: Node
}
function routeOf(decorator: Node, routers: ReadonlyMap<string, Router>): Route | undefined {
const [expression] = syntaxChildren(decorator)
const callee = expression?.type === 'call' ? expression.childForFieldName('function') : null
const object = callee?.type === 'attribute' ? callee.childForFieldName('object') : null
const method = callee?.childForFieldName('attribute')?.text
const router = object?.type === 'identifier' ? routers.get(object.text) : undefined
const call = callOf(expression, new Map())
if (!router || !call || !method || !METHODS.has(method))
return undefined
return { decorator, router, method: method.toUpperCase(), call, path: call.positional[0] ?? call.keywords.get('path') }
}
/** 路径里的一段:`{参数}` 按变量着色,其余按字符串,带源码下标;写的不是字符串时交给翻译插件翻。 */
function pathSegments(node: Node | undefined, env: Env): Inline[] {
if (!node)
return []
const content = stringContent(node)
if (!content)
return env.context.translate(node)
const segments: Segment[] = []
let at = content.node?.startIndex ?? node.startIndex
for (const part of content.text.split(/(\{[^}]*\})/).filter(Boolean)) {
segments.push({ text: part, role: part.startsWith('{') ? 'variable' : 'string', offset: at })
at += part.length
}
return segments
}
/** 路径里的参数名:`{user_id}`、`{file_path:path}` 里冒号前面的。 */
function pathNames(parts: readonly (Node | undefined)[]): Set<string> {
const text = parts.map(part => stringContent(part)?.text ?? '').join('')
return new Set([...text.matchAll(/\{([^}:]+)(?::[^}]*)?\}/g)].map(match => match[1].trim()))
}
/** 一个参数放在请求的哪儿;FastAPI 自己往里填的(Request……)、`*args` 这种没有。 */
interface Placed {
place: Place
param: Node
/** 参数的类型(`Annotated[T, ...]` 取 T);没标注的没有 */
type?: Node
/** 依赖:Depends(x) 的 x */
dependency?: Node
}
/** 类型最外层的名字:`int`、`list[str]` 的 list、`sa.UUID` 的 UUID。 */
function headName(type: Node): string | undefined {
return lastName(genericOf(type)?.head ?? unwrapType(type))
}
/** 简单类型(FastAPI 当查询参数的):内置的标量、UUID、日期时间、Literal,枚举类(别处定义的经索引查),和只包着它们的 Optional / list / `X | None`。 */
function isSimple(node: Node, env: Env): boolean {
const type = unwrapType(node)
if (type.type === 'none')
return true
if (type.type === 'binary_operator')
return syntaxChildren(type).every(side => isSimple(side, env))
const generic = genericOf(type)
if (generic) {
const head = lastName(generic.head)
if (head === 'Literal')
return true
if (head === 'Annotated')
return generic.args.length > 0 && isSimple(generic.args[0], env)
return head !== undefined && WRAPPERS.has(head) && generic.args.every(arg => arg.type === 'ellipsis' || isSimple(arg, env))
}
const head = lastName(type)
return head !== undefined && (SIMPLE.has(head) || definedValue(type, env) === 'Enum')
}
/** 上传文件(`UploadFile`、`list[UploadFile]`):FastAPI 当表单里的文件,是请求体。 */
function isUpload(type: Node): boolean {
const generic = genericOf(type)
return headName(type) === 'UploadFile' || (generic !== undefined && WRAPPERS.has(lastName(generic.head) ?? '') && generic.args.some(isUpload))
}
/**
* 参数放在请求的哪儿,照 FastAPI 的规则:Depends / Security 是依赖;名字在路径里是路径参数;写了 Query / Header / Cookie / Body / Form / File
* 照写的(默认值 `= Query(...)` 或 `Annotated[T, Query(...)]`,别处写好的 `X = Annotated[T, Query(...)]` 经索引查);
* Request 这些 FastAPI 自己填的不算;上传文件、别的类(Pydantic 模型)是请求体;其余简单类型、没标注的是查询参数。
*/
function placeOf(parameter: ParameterFacts, inPath: ReadonlySet<string>, env: Env): Placed | undefined {
const param = parameter.name
if (param.type !== 'identifier')
return undefined
const annotated = annotatedMarker(parameter.type, call => calledName(call, env))
const type = annotated?.type ?? (parameter.type && unwrapType(parameter.type))
const markerCall = MARKER_PLACES[calledName(parameter.default, env) ?? ''] ? parameter.default : annotated?.marker
const marker = markerCall && calledName(markerCall, env)
if (markerCall && marker && MARKER_PLACES[marker] === 'dependency') {
const call = callOf(markerCall, new Map())
return { place: 'dependency', param, dependency: call?.positional[0] ?? call?.keywords.get('dependency') ?? type }
}
if (inPath.has(param.text))
return { place: 'path', param, type }
if (marker && MARKER_PLACES[marker])
return { place: MARKER_PLACES[marker], param, type }
const alias = type && (type.type === 'identifier' || type.type === 'attribute') ? definedValue(type, env) : undefined
if (alias && MARKER_PLACES[alias])
return MARKER_PLACES[alias] === 'dependency' ? { place: 'dependency', param, dependency: type } : { place: MARKER_PLACES[alias], param, type }
if (type && INJECTED.has(headName(type) ?? ''))
return undefined
if (type && isUpload(type))
return { place: 'body', param, type }
return !type || isSimple(type, env) ? { place: 'query', param, type } : { place: 'body', param, type }
}
/** 函数体里直接抛的异常(嵌套的函数、类、lambda 里的不算)。 */
function raisesIn(node: Node | null | undefined): Node[] {
const found: Node[] = []
const visit = (current: Node): void => {
for (const child of syntaxChildren(current)) {
if (child.type === 'raise_statement')
found.push(child)
else if (!['function_definition', 'class_definition', 'decorated_definition', 'lambda'].includes(child.type))
visit(child)
}
}
if (node)
visit(node)
return found
}
// ─── 写成属性表 ───────────────────────────────────────────────────────────────
/** 一串词拼成的文字(框写框头)。 */
function textOf(pieces: readonly Inline[]): string {
return pieces.map(piece => 'role' in piece ? piece.text : piece.label).join('')
}
/** 依赖的名字:写成一个名字时按函数着色,别的交给翻译插件翻。 */
function dependencyText(node: Node, env: Env): Inline[] {
return node.type === 'identifier' ? [name(node, 'function')] : env.context.translate(node)
}
/** `dependencies=[Depends(x), ...]` 里的各个 x。 */
function listedDependencies(items: readonly Node[], env: Env): Inline[][] {
return items.flatMap((item) => {
const call = MARKER_PLACES[calledName(item, env) ?? ''] === 'dependency' ? callOf(item, new Map()) : undefined
const dependency = call?.positional[0] ?? call?.keywords.get('dependency')
return dependency ? [dependencyText(dependency, env)] : []
})
}
/** 标签:字符串照写(不带引号,带下标),别的交给翻译插件翻。 */
function tagText(node: Node, env: Env): Inline[] {
const content = stringContent(node)
return content?.node ? [{ text: content.text, role: 'string', offset: content.node.startIndex }] : env.context.translate(node)
}
/** 「名称: 类型」一串,逗号隔开;没标注的只写名称。 */
function paramsText(placed: readonly Placed[], env: Env): Inline[] {
return joined(placed.map(({ param, type }) => [name(param, 'variable'), ...(type ? [{ text: ': ', role: 'plain' as const }, ...env.context.translate(type)] : [])]), { text: ', ', role: 'plain' })
}
/** 响应表里一个状态码的一行:状态码、内容、说明,同一个状态码的几处写在一起。 */
interface Status {
code: Inline[]
/** 三位数字的状态码按大小排,别的(`"4XX"`、`default`)排在后面 */
order: number
contents: Inline[][]
notes: Inline[][]
}
/** 响应表里的状态码:三位数字的(按大小排、上色),或照写的(`"5XX"`、交给翻译插件翻的表达式)。 */
type StatusCode = { key: string, number: number } | { key: string, cell: Inline[] }
function numericStatus(code: string): StatusCode {
return { key: code, number: Number(code) }
}
/** 状态码写法 → 状态码:`403`、`status.HTTP_403_FORBIDDEN`、`"403"` 是数字,别的字符串(`"5XX"`)照写,别的写法交给翻译插件翻。 */
function statusCodeOf(node: Node, env: Env): StatusCode {
const text = stringContent(node)?.text
const number = statusNumber(node) ?? (text !== undefined && /^\d{3}$/.test(text) ? text : undefined)
if (number)
return numericStatus(number)
return text !== undefined ? { key: text, cell: [{ text, role: 'string' }] } : { key: node.text, cell: env.context.translate(node) }
}
/** 响应表的各行:一个状态码一行,同一个状态码的几处写进同一行(内容、说明各自去重)。 */
class Statuses {
private readonly byKey = new Map<string, Status>()
add(status: StatusCode, content?: Inline[], note?: Inline[]): void {
let row = this.byKey.get(status.key)
if (!row) {
const code: Inline[] = 'cell' in status
? status.cell
: [status.number < 400 ? { text: status.key, role: 'number', color: SUCCESS_COLOR } : { text: status.key, role: 'string' }]
row = { code, order: 'number' in status ? status.number : Number.POSITIVE_INFINITY, contents: [], notes: [] }
this.byKey.set(status.key, row)
}
if (content && content.length > 0 && !row.contents.some(known => textOf(known) === textOf(content)))
row.contents.push(content)
if (note && note.length > 0 && !row.notes.some(known => textOf(known) === textOf(note)))
row.notes.push(note)
}
rows(): Status[] {
return [...this.byKey.values()].sort((a, b) => a.order - b.order)
}
}
/** `responses={404: {"model": X, "description": "…"}, ...}` 的各项:模型是内容,description 是说明;值不是字典时整个是内容。 */
function addResponses(node: Node | undefined, statuses: Statuses, env: Env): void {
if (node?.type !== 'dictionary')
return
for (const pair of syntaxChildren(node)) {
const key = pair.type === 'pair' ? pair.childForFieldName('key') : null
const value = pair.childForFieldName('value')
if (!key || !value)
continue
if (value.type !== 'dictionary') {
statuses.add(statusCodeOf(key, env), env.context.translate(value))
continue
}
const fields = new Map(syntaxChildren(value).flatMap((field) => {
const fieldKey = field.type === 'pair' ? stringContent(field.childForFieldName('key'))?.text : undefined
const fieldValue = field.childForFieldName('value')
return fieldKey !== undefined && fieldValue ? [[fieldKey, fieldValue] as const] : []
}))
const model = fields.get('model')
const description = stringContent(fields.get('description'))?.text
statuses.add(statusCodeOf(key, env), model && modelText(model, env), description ? [{ text: description, role: 'comment' }] : undefined)
}
}
/**
* 写在参数值里的响应模型(response_model=、responses 里的 "model"):写成一个名字时按类型着色(翻译插件翻表达式时把它当变量),
* 别的交给翻译插件翻。返回类型是注解,翻译插件本来就按类型翻。
*/
function modelText(node: Node, env: Env): Inline[] {
return node.type === 'identifier' ? [name(node, 'type')] : env.context.translate(node)
}
/** 抛出时带的消息:字符串照写(说明色),别的交给翻译插件翻。 */
function messageText(node: Node | undefined, env: Env): Inline[] | undefined {
if (!node)
return undefined
const content = stringContent(node)
return content ? [{ text: content.text, role: 'comment' }] : env.context.translate(node)
}
/** 函数体里直接抛的异常按状态码归进去:HTTPException 看 status_code、detail;别的类看索引里查到的类上的 status_code,消息是第一个参数(或 message=、detail=)。 */
function addRaises(body: Node | null | undefined, statuses: Statuses, env: Env): void {
for (const raise of raisesIn(body)) {
const [thrown] = syntaxChildren(raise)
if (!thrown)
continue
const callee = thrown.type === 'call' ? thrown.childForFieldName('function') : thrown
const call = callOf(thrown, new Map())
const className = callee?.type === 'attribute' ? callee.childForFieldName('attribute') : callee
if (className?.type !== 'identifier')
continue
if (libraryName(callee, env.names) === 'HTTPException') {
const status = call?.keywords.get('status_code') ?? call?.positional[0]
if (status)
statuses.add(statusCodeOf(status, env), [name(className, 'type')], messageText(call?.keywords.get('detail') ?? call?.positional[1], env))
continue
}
const status = definedValue(className, env)
if (status && /^\d{3}$/.test(status))
statuses.add(numericStatus(status), [name(className, 'type')], messageText(call?.positional[0] ?? call?.keywords.get('message') ?? call?.keywords.get('detail'), env))
}
}
/** 请求表的一行:项 | 值。 */
function requestItem(label: string, value: Inline[]): PropertyItem {
return { cells: [[hint(label)], value] }
}
function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
const { context, words } = env
const { labels } = words
const { facts } = block
const { router, call } = route
const separator = hint(words.listSeparator)
const inPath = pathNames([router.prefix, route.path])
const placed = facts.parameters.flatMap(parameter => placeOf(parameter, inPath, env) ?? [])
const at = (place: Place): Placed[] => placed.filter(item => item.place === place)
const body = at('body')
const url = [...pathSegments(router.prefix, env), ...pathSegments(route.path, env)]
const request: PropertyItem[] = [
requestItem(words.items.url, url),
requestItem(words.items.method, [{ text: route.method, role: 'keyword', ...(METHOD_COLORS[route.method] && { color: METHOD_COLORS[route.method] }) }]),
...(['path', 'query', 'header', 'cookie'] as const).flatMap(place => at(place).length > 0 ? [requestItem(words.items[place], paramsText(at(place), env))] : []),
...(body.length > 0 ? [requestItem(words.items.body, body.length === 1 && body[0].type ? context.translate(body[0].type) : paramsText(body, env))] : []),
]
const statuses = new Statuses()
const success = call.keywords.get('status_code')
const model = call.keywords.get('response_model')
const description = stringContent(call.keywords.get('response_description'))?.text
// 没写 response_model 时内容是返回类型;`-> None` 没有内容
const returned = facts.returnType && unwrapType(facts.returnType).type !== 'none' ? facts.returnType : undefined
statuses.add(
success ? statusCodeOf(success, env) : numericStatus('200'),
model ? (model.type === 'none' ? undefined : modelText(model, env)) : returned && context.translate(returned),
[{ text: description ?? words.success, role: 'comment' }],
)
addResponses(router.responses, statuses, env)
addResponses(call.keywords.get('responses'), statuses, env)
const definition = block.node.type === 'decorated_definition' ? block.node.childForFieldName('definition') : block.node
addRaises(definition?.childForFieldName('body'), statuses, env)
if (placed.some(item => item.place !== 'dependency'))
statuses.add(numericStatus('422'), [hint(words.validation)], [hint(words.automatic)])
const responses = statuses.rows()
const doc = facts.doc && context.doc(facts.doc)
const tags = [...router.tags, ...listItems(call.keywords.get('tags'))].map(tag => tagText(tag, env))
const decorators = facts.decorators.filter(other => other !== route.decorator)
const dependencies = [
...listedDependencies(router.dependencies, env),
...listedDependencies(listItems(call.keywords.get('dependencies')), env),
...at('dependency').flatMap(({ param, dependency }) => dependency
? [[...dependencyText(dependency, env), ...(param.text === '_' ? [] : [hint(words.injectedInto(param.text))])]]
: []),
]
const rows: PropertyRow[] = [
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'function')] }] : []),
...(doc ? [{ label: labels.doc, value: doc }] : []),
...(tags.length > 0 ? [{ label: labels.tags, value: joined(tags, separator) }] : []),
{ label: labels.async, value: [word(facts.async ? words.yes : words.no)] },
...(decorators.length > 0 ? [{ label: labels.decorators, value: joined(decorators.map(other => context.translate(other)), { text: '\n', role: 'plain' }) }] : []),
...(dependencies.length > 0 ? [{ label: labels.dependencies, value: joined(dependencies, separator) }] : []),
{
label: labels.request,
value: [hint(`${route.method} ${textOf(url)}`)],
columns: words.requestHeads,
items: request,
},
{
label: labels.responses,
value: [hint(responses.map(row => textOf(row.code)).join(' / '))],
columns: words.responseHeads,
items: responses.map(row => ({
cells: [row.code, joined(row.contents, separator), joined(row.notes, { text: words.messageSeparator, role: 'comment' })],
})),
},
]
return {
kind: KIND,
badge: { label: words.kind, color: COLOR, icon: ICON },
rows,
...(facts.name && { defines: range(facts.name) }),
absorbs: [],
}
}
export const fastapiRule: LibraryRule = {
id: RULE_ID,
label: 'FastAPI 接口',
languageIds: ['python'],
libraries: ['fastapi'],
claim: (block, context) => {
if (block.kind !== 'function' || block.facts.decorators.length === 0)
return undefined
const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH, names: namesOf(context.imports) }
const routers = routersOf(block.node.tree.rootNode, env)
for (const decorator of block.facts.decorators) {
const route = routeOf(decorator, routers)
if (route)
return routeCard(block, route, env)
}
return undefined
},
}