✨ feat(vscode): 代码阅读的调用嵌套:实参的值又是跨多行的调用时在值那一格里写它的一行、套它的实参表,一层层往下套;埋在式子里后面还跟着东西的调用括起来

This commit is contained in:
cruldra committed 2026-09-27 22:31:35 +08:00
1 parent dc100692b3
commit 285d711740
16 files changed
+471 -55

No files matched your search

+5
View File
@@ -344,6 +344,11 @@ export interface PropertyItem extends Defines {
cells: Inline[][]
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项);一层层可以一直往下套。 */
items?: PropertyItem[]
/**
* 排在这一行最后一格里、那格文字下面的表:实参表里一个实参的值里有跨几行写的调用时,是那个调用的实参表,
* 表里的行又可以带 table,一层层往下套。总是展开的。
*/
table?: PreviewTable
}
/**
@@ -0,0 +1,87 @@
# luffy 的两段节选:workspace_repo/service.py 的 WorkspaceRepoService.__init__、commerce/tasks.py 的 _async_check_pending_refunds
import base64
class WorkspaceRepoService:
def __init__(
self,
client: GiteaAdminClient,
*,
public_url: str,
internal_url: str,
admin_user: str,
admin_password: str,
) -> None:
self._client = client
self._public_url = public_url.rstrip("/")
self._internal_url = internal_url.rstrip("/")
self._admin_basic = base64.b64encode(
f"{admin_user}:{admin_password}".encode()
).decode()
async def _async_check_pending_refunds() -> None:
"""超时未回调的退款 cron 兜底: 主动查 → 调 reversal_hook → 推进 REFUNDED.
与异步回调 ``handle_alipay_refund_notify`` 共用 ``_finalize_refund``
helper, 保证账本侧 reversal 与订单状态机推进始终一对一. 此前仅写
``refund_trade_no`` 不调 reversal_hook, 会让"用户钱已退 + 账本 grant
仍在"成为双花漏洞.
金额来源说明: 当前 ``RefundQueryResult`` 不带 alipay 实际退款金额, cron
无法判断 unused 模式下的部分退款. 折中策略: 按订单金额 (snapshot) 走
``_finalize_refund``, full 模式严格相等会通过, unused 模式
``0 < refunded <= rmb_amount_minor`` 也通过. 等 driver 后续暴露
``refund_amount_minor`` 字段再切换.
"""
from luffy_agent.commerce.config import get_commerce_config
from luffy_agent.commerce.exceptions import UnsupportedPaymentMethodError
from luffy_agent.commerce.orders.repositories import order_repository
from luffy_agent.commerce.payments.service import _finalize_refund
config = get_commerce_config()
factory = get_pg_session_factory()
checked = 0
confirmed = 0
async with factory() as db:
orders = await order_repository.list_pending_refunds(
db, age_hours=24, limit=100
)
for order in orders:
checked += 1
try:
result = await config.get_payment_driver(
order.payment_method
).query_refund_status(
out_trade_no=str(order.id),
out_request_no=f"{order.id}-refund",
)
except UnsupportedPaymentMethodError:
logger.warning(
"check_pending_refunds: skip order %s, no driver for %s",
order.id,
order.payment_method,
)
continue
except Exception as exc: # noqa: BLE001
logger.exception(
"query_refund_status failed for order %s: %s", order.id, exc
)
continue
if not result.refunded:
continue
# 已加 with_for_update(skip_locked) 行锁, 与 refund-notify 互斥.
logger.warning("refund confirmed via active query, order=%s", order.id)
confirmed += 1
await _finalize_refund(
db,
order=order,
refund_trade_no=result.refund_trade_no,
refunded_amount_minor=order.rmb_amount_minor,
source="check-refund-cron",
)
logger.info(
@@ -0,0 +1,18 @@
// 本仓库 vscode/src/extension.ts 的 activate 节选
export function activate(context: ExtensionContext): SuperpowersApi {
context.subscriptions.push(
treeView,
commands.registerCommand('superpowers.openKanban', () => {
KanbanWebviewPanel.createOrShow(context)
}),
commands.registerCommand('superpowers.refresh', () => {
KanbanWebviewPanel.refresh()
}),
commands.registerCommand('superpowers.setGiteaToken', () => {
KanbanWebviewPanel.createOrShow(context)
KanbanWebviewPanel.requestEditAuth()
}),
)
return { codeReader: registerCodeReader(context) }
}
@@ -0,0 +1,86 @@
"""支付子域管理后台路由.
仅含管理后台用的支付流水列表/详情. URL prefix 与 tags 与拆分前完全一致
(``/admin/commerce/payments`` + ``commerce-admin-payments``), 保证对外契约不变.
共享 schema ``OrderResponse`` 与 repository 从父子域绝对 import.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from pathlib import Path
from typing import Annotated, Literal
from urllib.parse import urlparse
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, ConfigDict
from sqlalchemy.ext.asyncio import AsyncSession
from luffy_agent.commerce.bootstrap import _secret_text, jsapi_ready
from luffy_agent.commerce.orders.console.schemas import (
AdminOrderRow,
to_admin_order_row,
to_admin_order_rows,
)
from luffy_agent.commerce.orders.repositories import order_repository
from luffy_agent.commerce.payments.wechat_jsapi import repositories as jsapi_repo
from luffy_agent.commerce.payments.wechat_jsapi import service as jsapi_service
from luffy_agent.core.db import get_db
from luffy_agent.core.exceptions import AppException
from luffy_agent.core.params import Int4Query
from luffy_agent.core.security.fernet import FernetSecretService, MasterKeyMissingError
from luffy_agent.core.setting import get_settings
from luffy_agent.core.setting.schemas import Settings
from luffy_agent.user.auth.deps import ConsolePrincipal, require_permission
from luffy_agent.user.console.rbac.service import (
bind_console_audit_request,
console_rbac_service,
)
router = APIRouter(prefix="/admin/commerce/payments", tags=["commerce-admin-payments"])
class CredentialPresence(BaseModel):
mp_app_secret: bool
wechat_pay_api_v3_key: bool
wechat_pay_private_key: bool
jsapi_intent_master_key: bool
@router.get("/capability", response_model=PaymentCapabilityDiagnostic)
async def get_payment_capability(
request: Request,
db: AsyncSession = Depends(get_db),
_: object = Depends(require_permission("commerce.payments:view")),
) -> PaymentCapabilityDiagnostic:
"""admin: JSAPI/Native/H5 只读诊断. 不含密钥、openid、签名或密文."""
settings = get_settings()
oauth_host = urlparse(settings.WECHAT_MP_OAUTH_REDIRECT_URI).hostname or ""
allowed = list(settings.PAYMENT_RETURN_ALLOWED_ORIGINS)
request_origin = request.headers.get("origin")
pay_origin = request_origin if request_origin in allowed else None
hint_origins = [pay_origin] if pay_origin else allowed
private_key_path = settings.WECHAT_PAY_PRIVATE_KEY_PATH
count, earliest = await jsapi_repo.recovery_required_summary(db)
return PaymentCapabilityDiagnostic(
native_enabled=bool(settings.WECHAT_PAY_MCH_ID),
h5_enabled=settings.WECHAT_H5_ENABLED,
jsapi_enabled=settings.WECHAT_JSAPI_ENABLED,
jsapi_ready=jsapi_ready(settings),
pay_app_id_masked=_mask_app_id(settings.WECHAT_PAY_APP_ID),
mp_app_id_masked=_mask_app_id(settings.WECHAT_MP_APP_ID),
credentials_present=CredentialPresence(
mp_app_secret=bool(_secret_text(settings.WECHAT_MP_APP_SECRET)),
wechat_pay_api_v3_key=bool(settings.WECHAT_PAY_API_V3_KEY),
wechat_pay_private_key=bool(private_key_path)
and Path(private_key_path).is_file(),
jsapi_intent_master_key=_intent_key_present(settings),
),
oauth_callback_host=oauth_host,
pay_origin=pay_origin,
allowed_pay_origins=allowed,
jsapi_auth_dir_hint=[f"{origin}/" for origin in hint_origins if origin],
domain_whitelist="unknown_manual_check",
merchant_binding="unknown_manual_check",
recovery_required_count=count,
recovery_required_earliest=earliest,
)
+5 -2
View File
@@ -96,11 +96,14 @@ describe('中文名落到哪些名字上', () => {
expect(rendered.get(at(ROUTES, 'delete_item'))).toBe('delete_item')
})
it('renderedNames:一行译文下面那张表(跨几行写的调用的实参)里带下标的名字也算', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n )\n'
it('renderedNames:一行译文下面那张表(跨几行写的调用的实参)、值那一格里套着的表里带下标的名字也算', async () => {
const source = 'def f(user):\n return Detail(\n id=user.id,\n owner=Owner(\n name=user.name,\n ),\n )\n'
const rendered = renderedNames((await read(source)).blocks)
expect(rendered.get(at(source, 'id='))).toBe('id')
expect(rendered.get(at(source, 'user.id'))).toBe('user')
expect(rendered.get(at(source, 'Owner('))).toBe('Owner')
expect(rendered.get(at(source, 'name='))).toBe('name')
expect(rendered.get(at(source, 'user.name'))).toBe('user')
})
it('本文件定义的:定义处问各处出现,落在画出来的名字上的都换,定义处本身标 definition;长度对不上、没画出来的不换', async () => {
+1
View File
@@ -34,6 +34,7 @@ export function renderedNames(blocks: readonly BlockView[]): Map<number, string>
for (const item of list ?? []) {
item.cells.forEach(inline)
items(item.items)
items(item.table?.items)
}
}
const visit = (list: readonly BlockView[]): void => {
+14 -4
View File
@@ -1,5 +1,5 @@
import type { Tree } from '@vscode/tree-sitter-wasm'
import type { BlockKind, CodeBlock, Inline, LanguageAdapter, PreviewText, PropertyItem, PropertyRow, Segment } from './contract'
import type { BlockKind, CodeBlock, Inline, LanguageAdapter, PreviewTable, PreviewText, PropertyItem, PropertyRow, Segment } from './contract'
import type { FileEvent, IndexHost, WorkspaceFile } from './workspaceIndex'
import { readFileSync } from 'node:fs'
import * as path from 'node:path'
@@ -62,15 +62,25 @@ export function previewText(text: PreviewText): string {
return 'markdown' in text ? text.markdown : segmentText(text)
}
/** 属性表写成文字:每行 `名称: 值`,展开的子行比上一层多缩进两格、各列用 ` | ` 隔开。 */
export function propertyLines(rows: PropertyRow[]): string[] {
const itemLines = (items: PropertyItem[] | undefined, indent: string): string[] => (items ?? []).flatMap(item => [
/** 子行写成文字:各列用 ` | ` 隔开,下一层子行、排在最后一格里的表(PropertyItem.table)的各行比这一层多缩进两格。 */
function itemLines(items: PropertyItem[] | undefined, indent: string): string[] {
return (items ?? []).flatMap(item => [
`${indent}${item.cells.map(segmentText).join(' | ')}`,
...itemLines(item.items, `${indent} `),
...itemLines(item.table?.items, `${indent} `),
])
}
/** 属性表写成文字:每行 `名称: 值`,展开的子行比上一层多缩进两格(见 itemLines)。 */
export function propertyLines(rows: PropertyRow[]): string[] {
return rows.flatMap(row => [`${row.label}: ${previewText(row.value)}`, ...itemLines(row.items, ' ')])
}
/** 一行译文下面那张表写成文字,同 propertyLines 的子行,第一层不缩进。 */
export function tableLines(table: PreviewTable | undefined): string[] {
return itemLines(table?.items, '')
}
/** 语法树节点换成它的源码,没填的可选字段去掉。 */
export function nodesToText(value: unknown): unknown {
if (value instanceof Node)
@@ -3,7 +3,7 @@ import type { ZhTable } from './zhTemplate'
import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from '../languages/python'
import { createTypeScriptAdapter } from '../languages/typescript'
import { flatSegments, propertyLines, readFixture, segmentText, walk, WASM_DIR, withBlocks } from '../testSupport'
import { flatSegments, propertyLines, readFixture, segmentText, tableLines, walk, WASM_DIR, withBlocks } from '../testSupport'
import { pythonZhTranslator } from './pythonZh'
import { typescriptZhTranslator } from './typescriptZh'
import { createZhTemplateTranslator } from './zhTemplate'
@@ -1049,10 +1049,6 @@ describe('跨几行写的调用', () => {
}
}
function tableLines(table: PreviewTable | undefined): string[] {
return (table?.items ?? []).map(item => item.cells.map(segmentText).join(' | '))
}
const USER_DETAIL = [
'id | user.id',
'username | user.username',
@@ -1146,21 +1142,125 @@ describe('跨几行写的调用', () => {
expect(asked).not.toContain('file.read')
})
it('*x 写「展开」、**x 写「展开关键字」,「第 N 个」只数按位置传的;实参里的调用不再排成表', async () => {
it('*x 写「展开」、**x 写「展开关键字」,「第 N 个」只数按位置传的', async () => {
const source = [
'run(',
' first,',
' *rest,',
' second,',
' key=Inner(',
' x=1,',
' ),',
' key=value,',
' **options,',
')',
].join('\n')
const line = (await translateAll(py, 'python', pythonZhTranslator, source)).get(1)
expect(text(line?.segments)).toBe('调用 run · 5 个参数')
expect(tableLines(line?.table)).toEqual(['第 1 个 | first', '展开 | rest', '第 2 个 | second', 'key | Inner( x=1 )', '展开关键字 | options'])
expect(tableLines(line?.table)).toEqual(['第 1 个 | first', '展开 | rest', '第 2 个 | second', 'key | value', '展开关键字 | options'])
})
it('嵌套:实参的值是跨几行写的调用(可以带 await)时,值那一格写它的一行,它的实参表排在格子里;一行写完的调用、只是式子里有调用的照旧一行', async () => {
const payment = readFixture('paymentConsoleRoutes.py')
const { asked, calleeKind } = kinds({ PaymentCapabilityDiagnostic: 'class', CredentialPresence: 'class' })
const line = (await translateAll(py, 'python', pythonZhTranslator, payment, calleeKind)).get(64)
expect(text(line?.segments)).toBe('返回 新建 PaymentCapabilityDiagnostic · 15 个参数')
expect(tableLines(line?.table)).toEqual([
'native_enabled | bool(settings.WECHAT_PAY_MCH_ID)',
'h5_enabled | settings.WECHAT_H5_ENABLED',
'jsapi_enabled | settings.WECHAT_JSAPI_ENABLED',
'jsapi_ready | jsapi_ready(settings)',
'pay_app_id_masked | _mask_app_id(settings.WECHAT_PAY_APP_ID)',
'mp_app_id_masked | _mask_app_id(settings.WECHAT_MP_APP_ID)',
'credentials_present | 新建 CredentialPresence · 4 个参数',
' mp_app_secret | bool(_secret_text(settings.WECHAT_MP_APP_SECRET))',
' wechat_pay_api_v3_key | bool(settings.WECHAT_PAY_API_V3_KEY)',
// 跨了两行、却是式子不是一次调用:照旧一行
' wechat_pay_private_key | bool(private_key_path) 并且 Path(private_key_path).is_file()',
' jsapi_intent_master_key | _intent_key_present(settings)',
'oauth_callback_host | oauth_host',
'pay_origin | pay_origin',
'allowed_pay_origins | allowed',
'jsapi_auth_dir_hint | ⟦列表|遍历 hint_origins 的每个 origin,仅当 origin,取 f"{origin}/"⟧',
'domain_whitelist | "unknown_manual_check"',
'merchant_binding | "unknown_manual_check"',
'recovery_required_count | count',
'recovery_required_earliest | earliest',
])
expect(line?.table?.columns).toEqual(['参数', '值'])
// 里层和外层一样:问定义分新建和调用,名字带源码下标
expect(asked).toEqual(expect.arrayContaining(['PaymentCapabilityDiagnostic', 'CredentialPresence']))
const nested = line?.table?.items[6]
expect(nested?.table?.columns).toEqual(['参数', '值'])
expect(nested?.cells[1].find(piece => 'role' in piece && piece.text === 'CredentialPresence')).toEqual({ text: 'CredentialPresence', role: 'type', offset: payment.indexOf('CredentialPresence(', payment.indexOf('credentials_present=')) })
expect(nested?.table?.items[0].cells[0]).toEqual([{ text: 'mp_app_secret', role: 'variable', offset: payment.indexOf('mp_app_secret=') }])
const unresolved = (await translateAll(py, 'python', pythonZhTranslator, payment)).get(64)
expect(segmentText(unresolved?.table?.items[6].cells[1] ?? [])).toBe('调用 CredentialPresence · 4 个参数')
})
it('嵌套一层层往下套;await 的调用写「等待」', async () => {
const source = [
'return Outer(',
' inner=Middle(',
' leaf=Leaf(',
' value=1,',
' ),',
' flag=await fetch(',
' key,',
' ),',
' ),',
')',
].join('\n')
const { asked, calleeKind } = kinds({ Outer: 'class', Middle: 'class', Leaf: 'class' })
const line = (await translateAll(py, 'python', pythonZhTranslator, source, calleeKind)).get(1)
expect(text(line?.segments)).toBe('返回 新建 Outer · 1 个参数')
expect(tableLines(line?.table)).toEqual([
'inner | 新建 Middle · 2 个参数',
' leaf | 新建 Leaf · 1 个参数',
' value | 1',
' flag | 等待 调用 fetch · 1 个参数',
' 第 1 个 | key',
])
expect([...asked].sort()).toEqual(['Leaf', 'Middle', 'Outer', 'fetch'])
})
it('埋在式子里的跨几行调用也排:后面紧跟着同一个式子的别的部分(.x()、[0]、or)时整行括起来,后面是分隔符或什么都没有时不括', async () => {
const chained = readFixture('chainedCalls.py')
const rows = await describeAll(py, 'python', pythonZhTranslator, chained)
expect(propertyLines(rows.get(18) ?? []).slice(1)).toEqual([
'初始值: (调用 base64.b64encode · 1 个参数).decode()',
' 第 1 个 | f"{admin_user}:{admin_password}".encode()',
])
// 调用跨几行的是外面那次:被调用的里面那次调用照旧一行,挤成一行时圆括号贴着里面的内容
expect(propertyLines(rows.get(55) ?? []).slice(1, 2)).toEqual(['初始值: 等待 调用 config.get_payment_driver(order.payment_method).query_refund_status · 2 个参数'])
const source = [
'first = make(',
' a,',
')[0]',
'either = make(',
' a,',
') or fallback',
'other = fallback or make(',
' a,',
')',
'items = [',
' make(',
' a,',
' ),',
' b,',
']',
'run(',
' key=Model(',
' a=1,',
' ).model_dump(),',
')',
].join('\n')
const values = await describeAll(py, 'python', pythonZhTranslator, source)
const value = (line: number): string | undefined => propertyLines(values.get(line) ?? []).find(row => row.startsWith('初始值'))
expect(value(1)).toBe('初始值: (调用 make · 1 个参数)[0]')
expect(value(4)).toBe('初始值: (调用 make · 1 个参数) 或者 fallback')
expect(value(7)).toBe('初始值: fallback 或者 调用 make · 1 个参数')
expect(value(10)).toBe('初始值: [ 调用 make · 1 个参数, b ]')
const cell = (await translateAll(py, 'python', pythonZhTranslator, source)).get(16)
expect(tableLines(cell?.table)).toEqual(['key | (调用 Model · 1 个参数).model_dump()', ' a | 1'])
})
it('typeScript:new 写法就是新建,不带 new 的是调用,都不问定义;...x 写「展开」', async () => {
@@ -1184,6 +1284,26 @@ describe('跨几行写的调用', () => {
expect(asked).toEqual([])
})
it('typeScript 嵌套:实参是跨几行写的调用、new 时同样排在值那一格里', async () => {
const line = (await translateAll(ts, 'typescript', typescriptZhTranslator, readFixture('nestedCalls.ts.txt'))).get(3)
expect(text(line?.segments)).toBe('调用 context.subscriptions.push · 4 个参数')
expect(tableLines(line?.table)).toEqual([
'第 1 个 | treeView',
'第 2 个 | 调用 commands.registerCommand · 2 个参数',
' 第 1 个 | \'superpowers.openKanban\'',
' 第 2 个 | () => { KanbanWebviewPanel.createOrShow(context) }',
'第 3 个 | 调用 commands.registerCommand · 2 个参数',
' 第 1 个 | \'superpowers.refresh\'',
' 第 2 个 | () => { KanbanWebviewPanel.refresh() }',
'第 4 个 | 调用 commands.registerCommand · 2 个参数',
' 第 1 个 | \'superpowers.setGiteaToken\'',
' 第 2 个 | () => { KanbanWebviewPanel.createOrShow(context) KanbanWebviewPanel.requestEditAuth() }',
])
const source = 'const server = new Server(\n new Options(\n port,\n ),\n handler,\n)'
const rows = await describeAll(ts, 'typescript', typescriptZhTranslator, source)
expect(propertyLines(rows.get(1) ?? []).slice(-4)).toEqual(['值: 新建 Server · 2 个参数', ' 第 1 个 | 新建 Options · 1 个参数', ' 第 1 个 | port', ' 第 2 个 | handler'])
})
it('链式调用跨了几行、实参列表各自写在一行里的不排成表;回调主体不在块头部里的调用也不排', async () => {
const source = [
'const ids = items',
+52 -10
View File
@@ -89,6 +89,8 @@ interface Collector {
const CONTEXT_SEPARATOR = ' > '
const CLOSING = new Set([')', ']', '}'])
/** 跟在一个式子后面、不属于这个式子的标点:右括号、逗号、分号、冒号。 */
const SEPARATORS = new Set([...CLOSING, ',', ';', ':'])
const HEADER_OPENERS = new Set(['{', ':'])
/** 源码着色里代表名字的词性:着这几种色的具名叶子是标识符、属性名、类型名,能查定义。 */
const NAME_ROLES = new Set<TokenRole>(['variable', 'function', 'type'])
@@ -117,6 +119,25 @@ function withoutMarkers(node: Node, markers: readonly SourceRange[]): string {
return text + node.text.slice(cursor - node.startIndex)
}
function sameSpan(a: Node, b: Node): boolean {
return a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type
}
/**
* 在要翻的 root 里,node 后面还紧跟着包着它的式子的别的部分(`.model_dump()`、`[0]`、`or x`):从 node 往上找第一个后面有东西的节点,
* 那个东西不是右括号、逗号、分号、冒号(语法之外的注释不算)。
*/
function continued(node: Node, root: Node): boolean {
for (let current: Node | null = node; current && !sameSpan(current, root); current = current.parent) {
let next = current.nextSibling
while (next?.isExtra)
next = next.nextSibling
if (next)
return !SEPARATORS.has(next.type)
}
return false
}
function isComment(token: Token): boolean {
return token.text === '' && !token.group
}
@@ -245,9 +266,14 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return parts.every((part): part is string | Node => part != null) ? parts : undefined
}
/** 源码里上一个词到 index 之间有空白,下一个词前面就空一格。 */
/**
* 源码里上一个词到 index 之间有空白,下一个词前面就空一格;跨行写的一对圆括号挤成一行时贴着里面的内容
* (`f(\n x\n)` 写成 `f(x)`),方括号、花括号照旧空一格。
*/
function advance(out: Collector, index: number): void {
out.gap ||= /\s/.test(out.document.source.slice(out.cursor, index))
const between = out.document.source.slice(out.cursor, index)
const hugging = between.includes('\n') && (out.tokens.at(-1)?.raw === '(' || out.document.source[index] === ')')
out.gap ||= !hugging && /\s/.test(between)
out.cursor = index
}
@@ -344,12 +370,22 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return facts && facts.argumentList.startPosition.row !== facts.argumentList.endPosition.row ? facts : undefined
}
/**
* node 落在 span 里的那一段翻成一行译文,里面跨几行写的第一个调用排成一行「新建 / 调用 …」,实参摊成表(见 emitCall)。
* 一行译文的块、常量 / 变量的值、实参表里每个值各翻各的,各自排自己里面的那个调用。
*/
function expandedText(node: Node, span: SourceSpan, document: SourceDocument): { text: Inline[], table?: PreviewTable } {
const calls: CallSlot = {}
const text = joinTokens(tokensIn(node, span, document, calls))
return { text, ...(calls.table && { table: calls.table }) }
}
/**
* 实参表:参数 | 值,每个实参一行,照源码顺序。按名字传的写名字,按位置传的写「第 N 个」(只数按位置传的),
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs);值照常翻,里面的调用不再排成表。
* 展开的写「展开」(*args、...args)、「展开关键字」(**kwargs)。值照常翻;值里有跨几行写的调用时同样排成一行,
* 它的实参表排在值那一格里(PropertyItem.table),一层层往下套。
*/
function argumentTable(facts: CallFacts, span: SourceSpan, document: SourceDocument): PreviewTable {
const value = (node: Node): Inline[] => joinTokens(tokensIn(node, span, document))
let position = 0
const label = (arg: CallArgument): Inline[] => {
switch (arg.kind) {
@@ -364,18 +400,26 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
return [hint('展开关键字')]
}
}
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(arg => ({ cells: [label(arg), value(arg.value)] })) }
const row = (arg: CallArgument): PropertyItem => {
const { text, table } = expandedText(arg.value, span, document)
return { cells: [label(arg), text], ...(table && { table }) }
}
return { columns: ARGUMENT_COLUMNS, items: facts.arguments.map(row) }
}
/**
* 调用排成一行「新建 / 调用 被调用的名字 · N 个参数」,实参摊成表记在 calls 里。被调用的名字照常翻、带源码下标,
* 最后那个名字新建时按类型、调用时按函数着色,前面几节按变量着色。新建还是调用看写法(CallFacts.construct),
* 写法看不出来的问名字的定义:是类(含异常、枚举)就是新建,还没查到、查不到按调用算。
* 调用后面还紧跟着同一个式子的别的部分(见 continued)时整行括起来:「(新建 X · 3 个参数).model_dump()」。
*/
function emitCall(out: Collector, calls: CallSlot, node: Node, facts: CallFacts, span: SourceSpan): void {
calls.table = argumentTable(facts, span, out.document)
const construct = facts.construct ?? CONSTRUCTED.has(out.document.calleeKind?.(facts))
const wrapped = continued(node, out.root)
advance(out, node.startIndex)
if (wrapped)
emit(out, { text: '(', role: 'plain', translated: true, raw: '' })
emit(out, { text: construct ? '新建' : '调用', role: 'storage', translated: true, raw: '' })
out.gap = true
out.cursor = facts.callee.startIndex
@@ -387,6 +431,8 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
}
out.gap = true
emit(out, { text: `· ${facts.arguments.length} 个参数`, role: 'hint', translated: true, raw: '' })
if (wrapped)
emit(out, { text: ')', role: 'plain', translated: true, raw: '' })
out.cursor = node.endIndex
}
@@ -512,11 +558,7 @@ export function createZhTemplateTranslator(table: ZhTable): Translator {
const phrases: Phrases = {
translated: node => within(node, nodeSpan(node)),
within,
expanded: (node) => {
const calls: CallSlot = {}
const text = joinTokens(tokensIn(node, clipped(nodeSpan(node)), document, calls))
return { text, ...(calls.table && { table: calls.table }) }
},
expanded: node => expandedText(node, clipped(nodeSpan(node)), document),
entryCount: (literal, size) => {
const name = table.mapLiterals[literal.type]
return [hint(name ? `${name} ${size} 项` : `${size} 项`)]