✨ feat(prototype): 代码阅读原型加嵌套调用:实参的值本身是跨多行的调用时,值格里再放它的头一行和实参表;跨行的式子不算调用
This commit is contained in:
1 parent
8bc8db0be8
commit
dc100692b3
3 files changed
+76
-12
No files matched your search
@@ -27,7 +27,7 @@
|
||||
| `Prototype / 枚举` | luffy 的 `referral/models.py` 里的 `CommissionStatus` | 枚举卡片:名称 / 访问级别 / 继承,成员收成一张表(成员 / 值 / 说明),说明取行尾注释;Python 用库规则(导入了 enum 才启用),TypeScript 的 enum 由语言插件报结构,画成同一张卡片 |
|
||||
| `Prototype / 库规则 · Celery` | luffy 的 `backend/src/luffy_agent/celery_app.py` | 库规则(导入了 celery 才启用)两张卡片:「Celery 应用」名称 / 应用名 / 消息队列 / 结果存储;「定时任务」应用 + 任务表(任务 / 频率 / 说明),频率换算成「每 5 分钟」「每天」,说明取行尾注释,`**` 展开的另起一行 |
|
||||
| `Prototype / 库规则 · FastAPI 接口` | luffy 的 `user/routes.py` 里的 `router` 和 `update_user` | 路由器(`APIRouter(...)`)和接口是两张卡片。「路由器」卡片:名称 / 前缀 / 标签(写了依赖、默认响应再加行),接口表列这个文件里挂在它上面的全部接口(方法 / 完整路径 / 函数 / 说明),函数名 Cmd+单击跳到接口卡片。库规则(导入了 fastapi 才启用)把挂在路由对象上的函数整块换成「接口」卡片,函数体照常是子块:名称 / 说明 / 路由器 / 标签 / 异步 / 依赖(`Depends(...)` 注入的参数),然后两张表。请求表一项一行:URL(路由对象的 prefix + 装饰器里的路径)/ 方法 / 路径参数 / 查询参数 / 请求体,没有的项不列。响应表每个状态码一行(状态码 / 内容 / 说明):成功那行来自 `status_code` 和 `response_model`,`responses={…}` 里写的各占一行,函数体里直接抛的异常按异常类上的 `status_code` 归行、说明列出抛出时的消息,有参数时 FastAPI 自动带的 422 淡色。挂到应用上时再叠的前缀(`/api/v1`)在别的文件里,不算 |
|
||||
| `Prototype / 调用 · 构造`(`routes.py · 预览` 的块树里也是这样) | routes.py 的 `return UserDetailResponse(...)`、`user = await user_service.update_user(...)`、`avatar_url = await user_service.upload_avatar(...)` | 跨了多行写的调用把实参摊成一张表(参数 / 值,没名字的写「第 N 个」):返回里写「返回 新建 UserDetailResponse · 11 个参数」,变量的「初始值」写「等待 调用 user_service.update_user · 10 个参数」。被调用的名字是类就是「新建」,是函数就是「调用」(插件查定义,原型按首字母大写);一行写完的调用照旧一行 |
|
||||
| `Prototype / 调用 · 构造`(`routes.py · 预览` 的块树里也是这样) | routes.py 的 `return UserDetailResponse(...)`、`user = await user_service.update_user(...)`、`avatar_url = await user_service.upload_avatar(...)` | 跨了多行写的调用把实参摊成一张表(参数 / 值,没名字的写「第 N 个」):返回里写「返回 新建 UserDetailResponse · 11 个参数」,变量的「初始值」写「等待 调用 user_service.update_user · 10 个参数」。被调用的名字是类就是「新建」,是函数就是「调用」(插件查定义,原型按首字母大写);一行写完的调用照旧一行。第三块是嵌套:luffy `commerce/payments/console/routes.py` 的 `return PaymentCapabilityDiagnostic(...)`(15 个参数),`credentials_present` 的值又是跨多行的 `CredentialPresence(...)`,值格里再写「新建 CredentialPresence · 4 个参数」和它的实参表,再套也一样;实参里一行写完的调用、跨行的 `and` 表达式照旧是一行字 |
|
||||
| `Prototype / 块种类图标` | — | 每种块一个图标、颜色、名称的图例,库规则带的种类后面淡色写规则名;最上面一块「返回」停着鼠标、图标下面弹出名称提示 |
|
||||
| `Prototype / 遍历 · 推导式` | luffy 邀请码工具的 `"".join(secrets.choice(ALPHABET) for _ in range(length))`,外加带条件的列表、字典推导 | 写在表达式里的遍历原地套一个带循环图标和结果类型(生成器 / 列表 / 字典)的小框,框里按读的顺序重排:「遍历 range(length) 的每个 _,仅当 …,取 …」 |
|
||||
| `Prototype / 侧栏 · 复制菜单` | 同上 | 悬停行行尾第一个按钮「复制」(带小箭头),点开一组复制方式:文件名 / 文件名(不带后缀)/ 相对路径 / 绝对路径,右边淡色预览会复制出什么;文件夹没有「不带后缀」 |
|
||||
|
||||
+61
-8
@@ -998,20 +998,49 @@ function setText(root, name, characters) {
|
||||
// 调用在变量的初始值里,表放在「初始值」下面;在返回、语句里,块里先写一行「返回 新建 X」,下面是表。
|
||||
// 没名字的实参按位置写「第 N 个」;值照常译,实参里嵌套的调用不再展开。
|
||||
const CALL_ARG_KEY_W = 180;
|
||||
// 嵌套示例:luffy 的 backend/src/luffy_agent/commerce/payments/console/routes.py,credentials_present 的值又是一个跨多行的构造
|
||||
const NESTED_CALL_SAMPLE = [
|
||||
"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,",
|
||||
")",
|
||||
].join("\n");
|
||||
|
||||
/** 跨多行的调用拆出被调用的名字、有没有 await、各个实参;不是跨多行的调用返回 null。 */
|
||||
function parseCall(code) {
|
||||
if (!code.includes("\n")) return null;
|
||||
const match = /^(await\s+)?([\w.]+)\(([\s\S]*)\)$/.exec(code.trim());
|
||||
if (!match) return null;
|
||||
const text = code.trim();
|
||||
const match = /^(await\s+)?([\w.]+)\(([\s\S]*)\)$/.exec(text);
|
||||
// 开头那个括号要一直括到末尾:`bool(x)\n and Path(x).is_file()` 这种是式子,不是一次调用
|
||||
if (!match || matchingParen(text, text.indexOf("(")) !== text.length - 1) return null;
|
||||
const [, awaited, callee, inner] = match;
|
||||
const cuts = [-1, ...topLevelIndexes(inner, ","), inner.length];
|
||||
let position = 0;
|
||||
// 每个实参:[名字, 一行的值, 值本身又是跨多行的调用时拆好的那个调用]
|
||||
const args = cuts.slice(1).map((end, index) => inner.slice(cuts[index] + 1, end).trim()).filter(Boolean).map((part) => {
|
||||
const keyword = /^(\w+)\s*=\s*([\s\S]+)$/.exec(part);
|
||||
if (keyword) return [keyword[1], keyword[2].replace(/\s+/g, " ")];
|
||||
position += 1;
|
||||
return [`第 ${position} 个`, part.replace(/\s+/g, " ")];
|
||||
const raw = keyword ? keyword[2] : part;
|
||||
if (!keyword) position += 1;
|
||||
return [keyword ? keyword[1] : `第 ${position} 个`, raw.replace(/\s+/g, " "), parseCall(raw)];
|
||||
});
|
||||
return { awaited: Boolean(awaited), callee, construct: /^[A-Z]/.test(callee.split(".").pop()), args };
|
||||
}
|
||||
@@ -1058,7 +1087,11 @@ function argsTable(ctx, parent, args) {
|
||||
const header = ctx.lib.argRow.get("header").createInstance();
|
||||
header.name = "Header";
|
||||
append(table, header, true);
|
||||
for (const [name, value] of args) {
|
||||
for (const [name, value, nested] of args) {
|
||||
if (nested) {
|
||||
nestedArgRow(ctx, table, name, nested);
|
||||
continue;
|
||||
}
|
||||
const row = ctx.lib.argRow.get("row").createInstance();
|
||||
row.name = `Arg / ${name}`;
|
||||
append(table, row, true);
|
||||
@@ -1073,6 +1106,23 @@ function argsTable(ctx, parent, args) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 值本身是跨多行调用的那一行:参数格照旧,值格里先写这个调用的头一行,下面是它自己的实参表,再套也一样。 */
|
||||
function nestedArgRow(ctx, table, name, call) {
|
||||
const row = filled(ctx, table, `Arg / ${name}`, { dir: "HORIZONTAL" });
|
||||
row.strokes = paint(COLORS.borderStrong);
|
||||
row.strokeTopWeight = 1;
|
||||
row.strokeBottomWeight = row.strokeLeftWeight = row.strokeRightWeight = 0;
|
||||
const key = append(row, frame(ctx, "参数", { width: CALL_ARG_KEY_W, px: 8, py: 3 }));
|
||||
key.layoutSizingVertical = "FILL";
|
||||
key.strokes = paint(COLORS.borderStrong);
|
||||
key.strokeRightWeight = 1;
|
||||
key.strokeTopWeight = key.strokeBottomWeight = key.strokeLeftWeight = 0;
|
||||
richText(ctx, key, "Name", [{ text: name, color: name.startsWith("第 ") ? COLORS.desc : COLORS.tokenVariable }]);
|
||||
const value = filled(ctx, row, "值", { px: 8, py: 4, gap: 4 });
|
||||
richText(ctx, value, "Head", callHead(call));
|
||||
argsTable(ctx, value, call.args);
|
||||
}
|
||||
|
||||
/** Reader / CallBlock — Call=<被调用的名字>:返回、语句里跨多行的调用,块里一行「返回 新建 X」,下面是实参表。 */
|
||||
function callBlock(ctx, kind, call, name) {
|
||||
const { node, content } = cardShell(ctx, name, BLOCK_KINDS[kind], { kids: false });
|
||||
@@ -2137,11 +2187,13 @@ function createComponents(ctx, page) {
|
||||
if (call) calls.set(code, { kind: item.kind, call, at: [...(calls.has(code) ? calls.get(code).at : []), `${fileKey}:${lineRange(item.at)[0]}`] });
|
||||
});
|
||||
}
|
||||
calls.set(NESTED_CALL_SAMPLE, { kind: "return", call: parseCall(NESTED_CALL_SAMPLE), at: [] });
|
||||
const callBlocks = make("Reader / CallBlock", [...calls.keys()], 1, (code) => {
|
||||
const entry = calls.get(code);
|
||||
return callBlock(ctx, entry.kind, entry.call, `Call=${entry.call.callee}`);
|
||||
}, (item) => item.node);
|
||||
lib.callBlockAt = new Map([...calls].flatMap(([code, entry]) => entry.at.map((key) => [key, callBlocks.get(code)])));
|
||||
lib.nestedCall = callBlocks.get(NESTED_CALL_SAMPLE);
|
||||
lib.tree = make("Reader / BlockTree", fileKeys, 2, (fileKey) => blockTree(ctx, lib, fileKey), (item) => item.node);
|
||||
return { lib, sets };
|
||||
}
|
||||
@@ -2506,7 +2558,8 @@ async function build(api, stage = () => {}) {
|
||||
// 调用与构造:块树里这几块在画板下方被截掉,单独摆出来看
|
||||
const callBoard = frame(ctx, "Prototype / 调用 · 构造", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 8, fill: COLORS.editor });
|
||||
page.appendChild(callBoard);
|
||||
for (const [key, component] of [["routes:83", lib.card.get("routes:83").get("preview")], ["routes:45", lib.callBlockAt.get("routes:45")]]) {
|
||||
for (const [key, component] of [["routes:83", lib.card.get("routes:83").get("preview")], ["routes:45", lib.callBlockAt.get("routes:45")],
|
||||
["嵌套", lib.nestedCall]]) {
|
||||
const instance = component.node.createInstance();
|
||||
instance.name = `Block / ${key}`;
|
||||
append(callBoard, instance, true);
|
||||
@@ -2630,6 +2683,6 @@ async function run(api) {
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = { parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
|
||||
module.exports = { NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
|
||||
}
|
||||
if (typeof figma !== "undefined") void run(figma);
|
||||
+14
-3
@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
|
||||
const { NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
|
||||
const { fakeFigma } = require("../../figma-lab/fake-figma");
|
||||
|
||||
function action(node) { return node.reactions[0].actions[0]; }
|
||||
@@ -88,7 +88,7 @@ const EXPECTED_SETS = [
|
||||
["Function / upload_current_user_avatar", 2],
|
||||
["Variable / payload", 2],
|
||||
["Variable / avatar_url", 2],
|
||||
["Reader / CallBlock", 1],
|
||||
["Reader / CallBlock", 2],
|
||||
["Reader / BlockTree", 3],
|
||||
];
|
||||
|
||||
@@ -487,7 +487,7 @@ test("calls written over several lines spread their arguments into a table; one-
|
||||
const update = parseCall(SOURCES.routes.slice(82, 94).join("\n").replace(/^\s*user = /, ""));
|
||||
assert.equal(update.awaited, true);
|
||||
assert.equal(update.construct, false);
|
||||
assert.deepEqual(update.args.slice(0, 3), [["第 1 个", "db"], ["user_id", "user_id"], ["display_name", "body.display_name"]]);
|
||||
assert.deepEqual(update.args.slice(0, 3).map(([name, value]) => [name, value]), [["第 1 个", "db"], ["user_id", "user_id"], ["display_name", "body.display_name"]]);
|
||||
assert.equal(update.args.length, 10);
|
||||
assert.equal(parseCall("await user_service.get_user(db, user_id=user_id)"), null, "one line stays a line");
|
||||
|
||||
@@ -506,6 +506,17 @@ test("calls written over several lines spread their arguments into a table; one-
|
||||
assert.equal(named(named(variable, "Prop / 初始值"), "Value").characters, "等待 调用 user_service.update_user · 10 个参数");
|
||||
assert.equal(named(named(variable, "Arg / 第 1 个"), "Value").characters, "db");
|
||||
assert.ok(!named(lib.card.get("routes:43").get("preview").node, "Args"), "one-line call keeps its one-line value");
|
||||
// 嵌套:实参的值本身是跨多行的调用,值格里再放它的头一行和实参表;一行写完的实参照旧一行
|
||||
const nested = parseCall(NESTED_CALL_SAMPLE);
|
||||
assert.equal(nested.args.length, 15);
|
||||
const inner = nested.args.find(([name]) => name === "credentials_present")[2];
|
||||
assert.equal(inner.callee, "CredentialPresence");
|
||||
assert.equal(inner.args.length, 4);
|
||||
assert.equal(nested.args.find(([name]) => name === "jsapi_ready")[2], null);
|
||||
assert.equal(inner.args.find(([name]) => name === "wechat_pay_private_key")[2], null, "a multi-line `and` expression is not a call");
|
||||
const row = named(lib.nestedCall.node, "Arg / credentials_present");
|
||||
assert.equal(named(row, "Head").characters, "新建 CredentialPresence · 4 个参数");
|
||||
assert.equal(row.findAll((node) => node.name.startsWith("Arg / ")).length, 4);
|
||||
});
|
||||
|
||||
test("block kinds show as icons, the legend lists every kind with its name", async () => {
|
||||
|
||||
Reference in new issue
Block a user