Skip to content

fix(native): add Object.defineProperty accessor dispatch post-pass to native orchestrator#2024

Open
carlos-alm wants to merge 1 commit into
fix/issue-1884-computed-string-literal-keys-not-unwrapped-infrom
fix/issue-1887-native-engine-skips-object-defineproperty
Open

fix(native): add Object.defineProperty accessor dispatch post-pass to native orchestrator#2024
carlos-alm wants to merge 1 commit into
fix/issue-1884-computed-string-literal-keys-not-unwrapped-infrom
fix/issue-1887-native-engine-skips-object-defineproperty

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

For engine: 'native' full builds, buildGraph takes the tryNativeOrchestrator fast path, which never calls runPipelineStages — and therefore never calls buildDefinePropertyPostPass (stages/build-edges.ts). That post-pass exists specifically to resolve this.method() calls inside getter/setter functions registered via Object.defineProperty(obj, "x", { get: getter }), since the native Rust engine has no concept of definePropertyReceivers at all. Being architecturally unreachable on the orchestrator-success path meant it silently produced no edge at all (not just the wrong kind — see #1766) for the typed-instance-receiver case:

class Registry {
  bar() { return 1; }
}
const obj = new Registry();
function getter() { this.bar(); }
Object.defineProperty(obj, 'x', { get: getter });

Before this fix: buildGraph(dir, { engine: 'native', incremental: false }) produced no getter -> ... edge. WASM correctly resolved getter -> Registry.bar.

(The object-literal variant, e.g. const obj = { bar() {} }, already worked under native via Rust's own independent composite-pts-key mechanism — unaffected by this bug.)

Fix

Adds runPostNativeDefinePropertyDispatch, a new hybrid WASM re-parse post-pass wired into runPostNativePasses, structurally mirroring the existing runPostNativeThisDispatch (Phase 8.5) that already solves the identical "Rust doesn't persist X" problem for this/super dispatch:

  • Selects JS/TS candidate files: every known JS/TS file on a full build (queried from nodes, already populated by the native build), or changedFiles on an incremental build.
  • Narrows the set with a cheap raw-text pre-filter for the literal "defineProperty" substring before paying for a full WASM parse — avoiding the same +358% ms/file full-project-reparse regression selectThisDispatchFiles already guards against. The extractor only recognises the literal Object.defineProperty(...) call form, so this pre-filter can never produce a false negative.
  • WASM re-parses the narrowed set to recover definePropertyReceivers + call sites + typeMap, then resolves targets via resolveDefinePropertyAccessorTarget (call-resolver.ts) — the exact same function already shared by the WASM full-build path, the native fast-path inside runPipelineStages, and the incremental watch path, so all four call sites stay in lockstep.
  • Inserts new calls edges tagged technique = 'ts-native', matching the other three call sites (direct name/type-lookup resolution, not CHA virtual dispatch), and wires its target/affected-file sets into the existing role-reclassification and node/edge recount scoping.

Also extracts two small pieces of runPostNativeThisDispatch that are now shared by both hybrid post-passes: the caller-by-line prepared statement (prepareCallerByLineStmt) and the DB-backed CallNodeLookup construction (makePostNativeCallLookup). Renamed cleanupThisDispatchWasmTrees to cleanupWasmParseTrees since it's generic WASM-tree cleanup already reused by a third, unrelated call site (dropped-language backfill) even before this change.

No Rust changes: definePropertyReceivers is JS/TS-extractor-only with no native equivalent (confirmed zero occurrences in crates/codegraph-core/), so the fix is entirely in the TS-side post-pass orchestration, matching the issue's own root-cause analysis.

Test plan

  • New tests/integration/issue-1887-native-defineproperty-postpass.test.ts: engine-parity test (native full build now matches WASM), a dedicated native-vs-WASM edge-equality assertion, and an incremental-rebuild test that adds the accessor pattern to a previously-clean file.
  • Updated a stale comment in tests/integration/issue-1766-defineproperty-kind-filter-parity.test.ts that referenced this issue as still-unfixed.
  • npm test — full suite green except one pre-existing, already-tracked flaky test (Flaky cycle-node ordering in checkNoNewCycles full-suite run (tests/integration/check.test.ts) #1990, cycle-node ordering, unrelated) and confirmed unrelated to this change.
  • npm run lint — clean.
  • codegraph diff-impact --staged -T — impact contained entirely within native-orchestrator.ts's build pipeline, as expected.

Out-of-scope findings filed separately

While validating, found two pre-existing bugs unrelated to this fix (both confirmed to reproduce on the base branch with no src/ changes):

Closes #1887

Stacked on #1889 (base branch fix/issue-1884-computed-string-literal-keys-not-unwrapped-in) — only the native-orchestrator/types/test diff is this issue's change.

… native orchestrator

The native orchestrator's fast path (tryNativeOrchestrator) skips
runPipelineStages entirely on success, so buildDefinePropertyPostPass never
ran for native full builds -- this.method() calls inside getter/setter
functions registered via Object.defineProperty produced no edge at all when
the accessor target was a typed class instance (the object-literal variant
already worked via Rust's own composite-pts mechanism).

Adds runPostNativeDefinePropertyDispatch, a hybrid WASM re-parse post-pass
mirroring the existing runPostNativeThisDispatch for this/super dispatch:
selects JS/TS candidate files (full build: all known files; incremental:
changedFiles), narrows them with a cheap text pre-filter for the literal
"defineProperty" substring to avoid a full-project WASM re-parse, then
resolves targets via the same resolveDefinePropertyAccessorTarget already
shared by the WASM engine and the incremental watch path. Wired into
runPostNativePasses alongside this/super dispatch and CHA expansion.

Extracts the caller-by-line lookup and the DB-backed CallNodeLookup
construction (previously inlined only in runPostNativeThisDispatch) into
shared helpers now used by both hybrid post-passes.

Impact: 12 functions changed, 11 affected
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a silent regression in the engine: 'native' full-build path where Object.defineProperty(obj, "x", { get: getter }) accessor patterns produced no call edge at all (issue #1887), because tryNativeOrchestrator bypasses runPipelineStages — and therefore the buildDefinePropertyPostPass that the WASM path relies on.

  • New Phase 8.5b post-pass (runPostNativeDefinePropertyDispatch): mirrors the existing runPostNativeThisDispatch (Phase 8.5), using a cheap raw-text pre-filter for "defineProperty" before WASM re-parsing candidate files, then resolving targets via the shared resolveDefinePropertyAccessorTarget function already used by the three other build paths. Edges are inserted before Phase 8.6 CHA expansion so they are eligible for sibling-override fan-out.
  • Extractions (prepareCallerByLineStmt, makePostNativeCallLookup, renamed cleanupWasmParseTrees): small helpers now shared by both hybrid WASM re-parse post-passes and the dropped-language backfill path.
  • Tests: new integration test (issue-1887) covers engine-parity (WASM vs native full build) and incremental rebuild. Updated comment in the issue-1766 test to reflect that the object-literal case already resolved via Rust's composite-pts-key mechanism, not the new post-pass.

Confidence Score: 4/5

Safe to merge — the orchestrator change correctly mirrors the established Phase 8.5 pattern, shares the same resolution function as all other call sites, and the integration test validates end-to-end parity between engines.

The implementation is structurally sound and the logic path is well-tested. One test-robustness gap: the first describe.each(ENGINES) block runs the native iteration without an isNativeAvailable() guard, so on environments where the native binary is absent the native arm may pass silently via WASM fallback rather than skipping — the same guard the second block in the same file uses correctly.

tests/integration/issue-1887-native-defineproperty-postpass.test.ts — the native guard inconsistency in the first describe block.

Important Files Changed

Filename Overview
src/domain/graph/builder/stages/native-orchestrator.ts Adds runPostNativeDefinePropertyDispatch (Phase 8.5b), a new hybrid WASM re-parse post-pass for Object.defineProperty accessor receivers, plus prepareCallerByLineStmt/makePostNativeCallLookup extractions. Architecture mirrors the existing runPostNativeThisDispatch correctly: path relativization, seen-pair deduplication, WASM tree cleanup, and ordering before Phase 8.6 CHA are all consistent.
src/types.ts Adds definePropertyDispatchMs timing field to BuildResult, correctly placed alongside thisDispatchMs. No issues.
tests/integration/issue-1766-defineproperty-kind-filter-parity.test.ts Updates a stale comment to accurately reflect that the native definePropertyReceivers post-pass now runs (and agrees), but the composite-pts-key mechanism is what resolves the object-literal case. Comment-only change, no behavioral differences.
tests/integration/issue-1887-native-defineproperty-postpass.test.ts New regression test covering engine-parity (WASM+native full build) and incremental rebuild for the typed-instance-receiver accessor pattern. The incremental describe block correctly uses describe.skipIf(!isNativeAvailable()), but the first describe.each(ENGINES) block runs the native iteration unconditionally — on environments where native is unavailable this either produces a confusing failure or silently passes via WASM fallback.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant B as buildGraph (engine=native)
    participant TO as tryNativeOrchestrator
    participant PP as runPostNativePasses
    participant TD as Phase 8.5: runPostNativeThisDispatch
    participant DP as Phase 8.5b: runPostNativeDefinePropertyDispatch (NEW)
    participant CH as Phase 8.6: runPostNativeCha

    B->>TO: "full build, incremental=false"
    TO->>PP: native build succeeded
    PP->>TD: WASM re-parse (this/super dispatch)
    TD-->>PP: targetIds, affectedFiles
    PP->>DP: WASM re-parse (Object.defineProperty accessor dispatch)
    Note over DP: selectDefinePropertyDispatchFiles<br/>text pre-filter defineProperty<br/>parseFilesWasmForBackfill<br/>resolveDefinePropertyAccessorTarget
    DP-->>PP: targetIds, affectedFiles
    PP->>CH: CHA expansion (both prior passes edges eligible)
    CH-->>PP: chaEdgeCount, affectedFiles
    PP->>PP: role re-classify (scoped to all affectedFiles union)
    PP-->>TO: PostPassTimings (includes definePropertyDispatchMs)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant B as buildGraph (engine=native)
    participant TO as tryNativeOrchestrator
    participant PP as runPostNativePasses
    participant TD as Phase 8.5: runPostNativeThisDispatch
    participant DP as Phase 8.5b: runPostNativeDefinePropertyDispatch (NEW)
    participant CH as Phase 8.6: runPostNativeCha

    B->>TO: "full build, incremental=false"
    TO->>PP: native build succeeded
    PP->>TD: WASM re-parse (this/super dispatch)
    TD-->>PP: targetIds, affectedFiles
    PP->>DP: WASM re-parse (Object.defineProperty accessor dispatch)
    Note over DP: selectDefinePropertyDispatchFiles<br/>text pre-filter defineProperty<br/>parseFilesWasmForBackfill<br/>resolveDefinePropertyAccessorTarget
    DP-->>PP: targetIds, affectedFiles
    PP->>CH: CHA expansion (both prior passes edges eligible)
    CH-->>PP: chaEdgeCount, affectedFiles
    PP->>PP: role re-classify (scoped to all affectedFiles union)
    PP-->>TO: PostPassTimings (includes definePropertyDispatchMs)
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(native): add Object.defineProperty a..." | Re-trigger Greptile

}

const ENGINES: EngineMode[] = ['wasm', 'native'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Native engine not guarded by isNativeAvailable()

The first describe.each(ENGINES) block runs the native iteration unconditionally. On a machine where the native binary is absent, buildGraph(..., { engine: 'native' }) likely falls back to WASM, causing the test to pass — but without actually exercising the new runPostNativeDefinePropertyDispatch post-pass that the PR fixes. The second describe.skipIf(!isNativeAvailable()) block in the same file already gets this right. The first block should use the same guard to avoid either a false-pass (via silent WASM fallback) or a confusing failure (if native is a hard requirement).

Fix in Claude Code

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

14 functions changed11 callers affected across 4 files

  • selectThisDispatchFiles in src/domain/graph/builder/stages/native-orchestrator.ts:1030 (3 transitive callers)
  • prepareCallerByLineStmt in src/domain/graph/builder/stages/native-orchestrator.ts:1158 (5 transitive callers)
  • makePostNativeCallLookup in src/domain/graph/builder/stages/native-orchestrator.ts:1177 (4 transitive callers)
  • emitThisDispatchEdges in src/domain/graph/builder/stages/native-orchestrator.ts:1192 (3 transitive callers)
  • cleanupWasmParseTrees in src/domain/graph/builder/stages/native-orchestrator.ts:1251 (6 transitive callers)
  • runPostNativeThisDispatch in src/domain/graph/builder/stages/native-orchestrator.ts:1283 (3 transitive callers)
  • selectDefinePropertyDispatchFiles in src/domain/graph/builder/stages/native-orchestrator.ts:1385 (3 transitive callers)
  • runPostNativeDefinePropertyDispatch in src/domain/graph/builder/stages/native-orchestrator.ts:1433 (3 transitive callers)
  • PostPassTimings.definePropertyDispatchMs in src/domain/graph/builder/stages/native-orchestrator.ts:1539 (0 transitive callers)
  • formatNativeTimingResult in src/domain/graph/builder/stages/native-orchestrator.ts:1545 (4 transitive callers)
  • backfillNativeDroppedFiles in src/domain/graph/builder/stages/native-orchestrator.ts:1966 (4 transitive callers)
  • runPostNativePasses in src/domain/graph/builder/stages/native-orchestrator.ts:2189 (4 transitive callers)
  • tryNativeOrchestrator in src/domain/graph/builder/stages/native-orchestrator.ts:2527 (4 transitive callers)
  • BuildResult.phases in src/types.ts:1308 (0 transitive callers)

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.

1 participant