Skip to content

feat(loader): support constructed values and nested calls#3067

Open
luzhuang wants to merge 2 commits into
galacean:dev/2.0from
luzhuang:feat/runtime-type-constructor-args
Open

feat(loader): support constructed values and nested calls#3067
luzhuang wants to merge 2 commits into
galacean:dev/2.0from
luzhuang:feat/runtime-type-constructor-args

Conversation

@luzhuang

@luzhuang luzhuang commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add recursive constructor arguments to runtime $type values
  • allow runtime calls to target nested module owners
  • apply mutation props before calls and preserve positive infinity through a JSON-safe sentinel
  • restore particle curves and emission bursts through existing Engine public APIs

Boundary

This PR changes Loader only. It does not add particle collection setters or register the entire Math package. Editor source-v2 compilation is handled by companion PR galacean/editor#3793.

Verification

  • vitest run tests/src/loader/SceneFormatV2.test.ts -t ReflectionParser: 33 passed
  • touched Loader files: ESLint 0 errors
  • Prettier and git diff --check: passed

The full Loader type build is currently blocked by stale 1.4.5 workspace dependencies in this worktree; none of its diagnostics point to the touched Loader files.

Summary by CodeRabbit

  • New Features

    • Added support for invoking methods via nested mutation targets.
    • Added typed value construction with optional constructor arguments ($args) for polymorphic scene/resource loading.
    • Added JSON-safe handling for positive infinity.
  • Bug Fixes

    • Ensured properties are applied before related calls within the same mutation block.
    • Strengthened validation for method targets, typed constructor arguments, and burst numeric arguments.
    • Improved “method not found” errors to include the resolved target path context.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d43e6a4f-34b2-46e7-8510-184312e83afd

📥 Commits

Reviewing files that changed from the base of the PR and between ceba5a3 and 84b6141.

📒 Files selected for processing (3)
  • packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts
  • packages/loader/src/schema/CommonSchema.ts
  • tests/src/loader/SceneFormatV2.test.ts
💤 Files with no reviewable changes (1)
  • packages/loader/src/schema/CommonSchema.ts

Walkthrough

Reflection parsing now supports nested method targets, ordered property and call execution, recursive $type constructor arguments, and Burst argument validation, with expanded Scene Format V2 coverage.

Changes

Reflection parser updates

Layer / File(s) Summary
Schema contracts
packages/loader/src/schema/CommonSchema.ts
Adds TypeValue and extends CallSpec with an optional target path.
Mutation call targeting and ordering
packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts, tests/src/loader/SceneFormatV2.test.ts
Resolves nested call targets and applies mutation properties before calls, with coverage for asynchronous prop resolution and nested methods.
Typed constructor value resolution
packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts, tests/src/loader/SceneFormatV2.test.ts
Supports recursive $args, typed construction, Burst argument normalization and validation, and invalid $args rejection.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SceneFormatV2Test
  participant ReflectionParser
  participant CallOrderComponent
  SceneFormatV2Test->>ReflectionParser: parse mutation block
  ReflectionParser->>CallOrderComponent: apply resolved props
  ReflectionParser->>CallOrderComponent: invoke nested target method
  CallOrderComponent-->>SceneFormatV2Test: expose updated state
Loading

Possibly related PRs

  • galacean/engine#3066: Related loader, nested call-target, $type construction, schema, and test changes.

Suggested reviewers: zhuxudong, guolei1990

Poem

I hop through props, then calls align,
Nested paths now work just fine.
Constructors bloom, bursts shine bright,
Infinity twinkles through the night.
— A parser-loving rabbit 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main additions: constructed values and nested call targets.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: private package registry requires authentication. Disable ESLint in CodeRabbit settings or use public packages.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.47368% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.79%. Comparing base (b7bf6f7) to head (84b6141).

Files with missing lines Patch % Lines
...e-deserialize/resources/parser/ReflectionParser.ts 89.47% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3067      +/-   ##
===========================================
+ Coverage    79.61%   79.79%   +0.18%     
===========================================
  Files          904      904              
  Lines       101208   101313     +105     
  Branches     11404    11414      +10     
===========================================
+ Hits         80579    80847     +268     
+ Misses       20445    20284     -161     
+ Partials       184      182       -2     
Flag Coverage Δ
unittests 79.79% <89.47%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`:
- Around line 165-167: Move the standalone "$args" validation immediately after
obj is created in ReflectionParser, before branches handling "$ref", "$class",
"$entity", "$component", or "$signal", so any object containing "$args" without
"$type" is rejected. Add a test covering a mixed discriminator such as "$class"
with "$args" and assert it fails.
- Around line 43-57: Update the call traversal and invocation logic in
ReflectionParser to reject the property names "constructor", "prototype", and
"__proto__" in every call.target segment and in call.method before accessing or
invoking them. Return a rejected Promise using the existing validation-error
pattern, while preserving valid nested method resolution and invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5ce96ab-e890-4807-80ee-e87475a9bfd1

📥 Commits

Reviewing files that changed from the base of the PR and between b7bf6f7 and ceba5a3.

📒 Files selected for processing (3)
  • packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts
  • packages/loader/src/schema/CommonSchema.ts
  • tests/src/loader/SceneFormatV2.test.ts

Comment on lines +43 to +57
let target = instance;
if (call.target !== undefined) {
if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) {
return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`));
}
for (const key of call.target) target = target?.[key];
}
const method = target?.[call.method];
if (typeof method !== "function") {
return Promise.reject(new Error(`Call target does not have method "${call.method}"`));
const path = call.target?.length ? `${call.target.join(".")}.` : "";
return Promise.reject(new Error(`Call target does not have method "${path}${call.method}"`));
}

return Promise.all((call.args ?? []).map((arg) => this._resolveValue(arg)))
.then((resolvedArgs) => Promise.resolve(method.apply(instance, resolvedArgs)))
.then((resolvedArgs) => Promise.resolve(method.apply(target, resolvedArgs)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Block prototype and constructor traversal before invoking nested calls.

Loader-controlled paths can traverse function properties and reach Function, for example target: ["nested", "setValue"] with method: "constructor". Combined with call.result, this permits constructing and invoking arbitrary JavaScript.

Reject constructor, prototype, and __proto__ in both target segments and method names.

Proposed guard
+        const forbiddenKeys = new Set(["constructor", "prototype", "__proto__"]);
         let target = instance;
         if (call.target !== undefined) {
           if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) {
             return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`));
           }
+          if (call.target.some((key) => forbiddenKeys.has(key)) || forbiddenKeys.has(call.method)) {
+            return Promise.reject(new Error(`Call "${call.method}" contains a forbidden target key`));
+          }
           for (const key of call.target) target = target?.[key];
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`
around lines 43 - 57, Update the call traversal and invocation logic in
ReflectionParser to reject the property names "constructor", "prototype", and
"__proto__" in every call.target segment and in call.method before accessing or
invoking them. Return a rejected Promise using the existing validation-error
pattern, while preserving valid nested method resolution and invocation.

Comment on lines +165 to +167
if ("$args" in obj) {
return Promise.reject(new Error("$args requires $type"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate standalone $args before the other discriminator branches.

This check is unreachable for values containing $ref, $class, $entity, $component, or $signal, so malformed values such as { $class: "Foo", $args: [] } silently ignore $args. Move the check immediately after creating obj and add a mixed-discriminator test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`
around lines 165 - 167, Move the standalone "$args" validation immediately after
obj is created in ReflectionParser, before branches handling "$ref", "$class",
"$entity", "$component", or "$signal", so any object containing "$args" without
"$type" is rejected. Add a test covering a mixed discriminator such as "$class"
with "$args" and assert it fails.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ ceba5a347(首轮)— feat(loader): support constructed values and nested calls

总结

给 v2 场景 ReflectionParser 补三块运行时反序列化能力:① $type 支持递归解析的 $args 构造参数;② call.target 支持把方法调用打到嵌套属性路径的 owner 上;③ $number: "Infinity" 的 JSON-safe 正无穷哨兵;④ 把 parseMutationBlock 从「props/calls 并发无序」改成「props 先、calls 后」的确定性顺序。方向完全正确,是 #3061 那版错 base 的 d59c6392d 收敛到 dev/2.0 纯 Loader 改动后的干净版本。逐链核对无 P0/P1/P2,可合并。

逐链核对(非信 commit message / PR 描述)

  • props-before-calls 顺序是收紧而非回退:原 Promise.all([props, calls]) 的 JSDoc 明确写「without imposing ordering」(来自 #2959 284cce8e1),但那只是描述行为、未给出「必须无序」的理由。改成 props→calls 是确定性收紧:老行为并发交错,没有任何合法数据能依赖「calls 先于 props」,所以本改动严格更安全,且符合语义直觉(props 配置字段状态,calls 在其上动作,如 addBurst 需先配好 emission)。走 Git 历史透镜确认无矛盾——不是还原任何 fix:
  • 顺序测试反向可证伪should finish resolving props before executing calls 构造精巧——$ref 被 mock 成延迟 promise,await Promise.resolve() 放过一个微任务后才 resolve。新序下 call 捕获 "ready";revert 回 Promise.all 则 call 在 ref 解析前就跑、捕获 "",测试 fail。真守回归。
  • particle 恢复测试是端到端真链路Burst($args:[0.5, curve, 2, 0.1]) / ParticleCurve($args:[CurveKey,CurveKey]) / CurveKey($args:[0,1]) / ParticleCompositeCurve($args:[8]) 的构造签名逐一核对全部对齐真实构造函数,且经 call.target:["generator","emission"] + addBurst/clearBurst 走公开 API。这正是本特性的动机——editor 需要 round-trip 需构造参数 + 经方法(非纯 props)设置的粒子曲线/burst。reverse-falsifiable。
  • call.target 绑定正确method.apply(target, ...) 打到遍历后的 owner;call.target === undefinedtarget = instancetarget?.[call.method] 退化为原 instance?.[call.method],行为等价。错误消息 path 拼接在 target 校验之后,安全。
  • schema 新增类型非死代码TypeValue / SpecialNumberValueCommonSchema.ts 无 parser 消费者,但这与既有 ClassRef(同样零消费者)/Vec3Tuple/EntityRef 完全一致——该文件的职责就是声明 v2 格式的规范词汇表,不是必须被 import 才算活。按项目惯例非冗余。
  • 注释合规:更新后的 _resolveValue 优先级 JSDoc 已同步补入 $number(#4)/$type,$args?(#5) 重编号,与实际分支顺序一致;parseMutationBlock JSDoc 由「without imposing ordering」改为「Apply props before executing calls」——非 aspirational,与代码一致。CI 全绿(build×3 / codecov patch 88.37% + project 79.61% / prettier)。

已评估的 CodeRabbit 两条 finding(均非阻塞,据实校准严重度)

  • CR「Critical: prototype/constructor 遍历可达 RCE」→ 实为 P3 defensive、非新洞。核心事实:call.target:["fn"], method:"constructor" 能拿到 Function 进而 call.result 链式 RCE——但这条可达性早于本 PRparseCalls#2959 起就有 instance[call.method] + call.result → parseMutationBlock 递归,{method:"constructor", result:{calls:[{method:"constructor", result:{calls:[{method:"call", args:[...]}]}}]}} 早已可达 Function 并执行任意代码。本 PR 的 target 遍历只是便利路径,未引入新能力类。且 v2 场景数据是 editor 产出(与 glTF/scene 资产同信任级、等同 app 代码),非系统边界的不可信输入。CR 的 Critical 定级在此信任模型下过高。鉴于本 PR 恰好动到这段遍历代码,加一个 constructor/prototype/__proto__ 黑名单守卫是合理的 defense-in-depth,但属可选加固,不阻塞。

    const forbidden = new Set(["constructor", "prototype", "__proto__"]);
    if (call.target?.some((k) => forbidden.has(k)) || forbidden.has(call.method)) {
      return Promise.reject(new Error(`Call "${call.method}" contains a forbidden key`));
    }

问题

  • [P3] ReflectionParser.ts:165-167 — 独立 $args 校验位置偏后(CR Minor,据实保留为 P3)if ("$args" in obj) reject 排在 $ref/$class/$entity/$component/$signal 分支之后,故 { $class:"Foo", $args:[] } 会先命中 $class 分支、静默丢弃 $args。editor 不会产出这种混合 discriminator,属畸形输入容错的小缺口。若要收紧,把该校验挪到 obj 创建后、所有 discriminator 分支之前,并补一条 $class+$args 的混合用例。不阻塞。

  • [P3] ReflectionParser.ts — $number 仅覆盖正无穷。当前只支持 $number:"Infinity"(对应 Burst 无限循环 cycles),显式 reject NaN-Infinity 暂无字段需要故未支持——YAGNI 合理。仅记录:若将来出现需持久化 -Infinity 的字段(某些下界),此哨兵需扩展。不阻塞。

简化建议

无。代码干净,抽象克制($args 复用 _resolveValue 递归、call.target 与既有 parseCalls 无缝衔接),无过度设计。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ 84b614111(第二轮,增量)— fix(loader): decode infinite burst cycles

总结

自上轮 ceba5a347 起线性追加一个 commit(parent 就是 ceba5a347,非 force-push)。改动只做一件事:删掉上轮刚引入的通用 $number: "Infinity" 哨兵,改成在 _resolveValue$type 构造分支里对 $type === "Burst"args[2] 做位置化、字符串化的特判——"Infinity"Infinity,其余非 null 的非正整数 → reject。逻辑正确、反向证伪测试到位、CI 全绿(build×3 / codecov patch+project / e2e 4/4 / lint),可合并。但方向上把一个通用能力换成了单类硬编码特判,值得一议——记 [P2],不阻塞。

逐链核对(增量)

  • Burst.$args[2] 确是 cyclesBurst 构造签名 (time, count, cycles?, repeatInterval?)Burst.ts:51),args[2] 位置对齐。cycles === InfinityEmissionModule.ts:312 的真运行时哨兵(maxCycles = cycles===Infinity ? ceil((duration-burstTime)/repeatInterval) : cycles),JSON 无法表达 InfinityJSON.stringify(Infinity)==="null")故需 sentinel——需求真实。
  • 守卫逻辑穷举无洞"Infinity"Infinity ✓ / 2→过 ✓ / "NaN"→非"Infinity"非null且非number→throw ✓(测试覆盖)/ null|undefined!=null 短路跳过→构造函数 Math.max(cycles??1,1)=1 ✓ / 1.5/0/-Infinity→非正整数→throw ✓。
  • 反向证伪成立:测试改用 "Infinity" 串断言 bursts[0].cycles === Infinity,并新增 "NaN" 用例断言 rejects.toThrow(...);revert 特判即红。
  • Git 历史透镜无矛盾$number 是本 PR 上一 commit(ceba5a347)自己引入的,全仓无第二处引用、从未发版/被消费——删它不是还原任何 fix:,是作者重做自己刚加的机制。
  • 注释合规_resolveValue 优先级 JSDoc 从 10 条重编号回 9 条(去掉 $number),与实际分支顺序($ref3/$type4/$class5/$entity6/$component7/$signal8/plain 9)逐条一致,无残留。SpecialNumberValue interface 同步删除。

问题

  • [P2] ReflectionParser.ts:130-137 — 通用 Infinity 编码退化成 Burst 单类位置特判,是「类型枚举代替特征」+ 抽象泄漏。上轮的 $number: "Infinity" 虽然只覆盖正无穷(我上轮记为 P3-YAGNI),但它是格式层的通用 JSON-safe 数值编码——任何 prop / ctor arg 都能用。本轮把它换成 if ($type === "Burst" && args.length > 2)args[2] 的字符串特判,埋进了本该类型无关的 _resolveValue 分发器里,带来两点结构代价:

    • Infinity 本质是通用序列化问题,不是 Burst 问题。同仓 Joint.breakForce/breakTorque 默认就是 InfinityJoint.ts:27-28,对应 Unity「不可断裂关节」惯例)且是 public 可序列化属性(Joint.ts:157/171);ResourceManager.timeoutrequest.defaultTimeout 同样默认 Infinity。这些字段将来要经 v2 round-trip 时,通用 $number 本可覆盖,现在每个都得往 parser 里再加一条特判。新类型要 infinity → 必须改 parser——正是「加白名单,漏补即 bug」的信号。
    • 泛型分发器嵌入了单个引擎类的构造参数语义(硬编码「Burst 的第 3 个位置参是 cycles」)。Burst 构造顺序一旦调整,这里静默失效,且无编译期护栏。

    另外,"Infinity" 这个魔法串契约在 schema 里零文档——TypeValue.$args?: unknown[] 完全没提 Burst 的第三参接受字符串 "Infinity",producer(editor companion PR #3793)与 reader 全靠隐式约定。

    格式契约与 editor 共有、且处于 alpha 无兼容包袱——正是选对编码的最佳时机。建议保留通用 sentinel 方向(哪怕仍只实现正无穷),让「JSON-safe 特殊数值」成为格式层一等公民、与具体类解耦;Burst 侧只需在 $args 里放该 sentinel 即可,parser 不必认识 Burst。若坚持只服务 Burst,也至少把 "Infinity" 串契约写进 CommonSchema.tsTypeValue / Burst 相关注释,别让它只活在 parser 实现里。不阻塞。

已在上轮闭环、本轮不重提

  • CodeRabbit 在本 commit 重贴的两条(constructor/prototype 遍历可达 RCE = Critical;独立 $args 校验位置偏后 = Minor)我上轮均已评估:前者是早于本 PR 的既有可达性 + editor 信任模型下的可选加固(本轮 traversal 代码未变),后者我上轮已记为 P3。二者状态未变,不重复。

简化建议

若采纳上面 P2 的「恢复通用 sentinel」方向,_resolveValue 里那段 8 行 Burst 特判可整段删除,换成一条与类无关的 $number(或等价)分支,净复杂度更低且覆盖面更广——这本身就是更简的解。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants